diff --git a/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs b/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs
index af3614b73..a1a4510b5 100644
--- a/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs
+++ b/GenesisCordonelInterface/API/InterfaceOutsideToGCI.cs
@@ -12,21 +12,39 @@ using static GenesisCordonelInterface.API.PublicModels;
namespace GenesisCordonelInterface.API
{
///
- /// Outside-facing facade for Genesis Cordonel Interface.
- /// Exposes only selected operations intended for external callers.
+ /// Public-facing facade for external applications integrating with
+ /// Genesis Cordonel Interface.
///
+ ///
+ /// 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 .
+ ///
public class InterfaceOutsideToGCI
{
+ ///
+ /// Internal GCI implementation used by this public facade.
+ ///
public readonly InterfaceGCIToLaatzen _innerMeterAPI;
+ ///
+ /// Occurs when meter batch status information changes.
+ ///
public event Action> MeterBatchStatusChanged;
+ ///
+ /// Initializes a new instance of the public GCI facade.
+ ///
public InterfaceOutsideToGCI()
{
_innerMeterAPI = new InterfaceGCIToLaatzen();
}
- //
+ // Laatzen ToolBox actions
#region ================================== PORT DETECTION ==================================
@@ -66,6 +84,20 @@ namespace GenesisCordonelInterface.API
#region ================================== INIT/UPDATE/GET slot ==================================
+ ///
+ /// Initializes a meter slot using the provided slot configuration.
+ ///
+ ///
+ /// Slot initialization request containing slot id, configuration source,
+ /// password source, request port and streaming port.
+ ///
+ /// Cancellation token used to cancel the asynchronous operation.
+ ///
+ /// Result describing whether the slot was created, updated, already existed or failed.
+ ///
+ ///
+ /// Thrown when is null.
+ ///
public async Task InitSlotAsync(
GciInitSlotRequest request,
CancellationToken token = default)
@@ -86,6 +118,20 @@ namespace GenesisCordonelInterface.API
return result;
}
+ ///
+ /// Updates configuration of an existing meter slot.
+ ///
+ ///
+ /// Slot configuration request containing updated configuration source,
+ /// password source and port settings.
+ ///
+ /// Cancellation token used to cancel the asynchronous operation.
+ ///
+ /// Result describing whether the slot update succeeded or failed.
+ ///
+ ///
+ /// Thrown when is null.
+ ///
public async Task UpdateSlotAsync(
GciInitSlotRequest request,
CancellationToken token = default)
@@ -106,6 +152,18 @@ namespace GenesisCordonelInterface.API
return result;
}
+ ///
+ /// Gets information about one meter slot.
+ ///
+ /// Slot id to query. Must be greater than zero.
+ /// Cancellation token used to cancel the asynchronous operation.
+ ///
+ /// Slot information including existence, connection state, login state,
+ /// PCB id and configured communication ports.
+ ///
+ ///
+ /// Thrown when is invalid.
+ ///
public async Task GetSlotAsync(
int slotId,
CancellationToken token = default)
@@ -118,6 +176,13 @@ namespace GenesisCordonelInterface.API
return result;
}
+ ///
+ /// Gets information about all currently initialized meter slots.
+ ///
+ /// Cancellation token used to cancel the asynchronous operation.
+ ///
+ /// Collection of slot information records for all known meters.
+ ///
public async Task GetAllSlotsAsync(
CancellationToken token = default)
{
@@ -126,6 +191,17 @@ namespace GenesisCordonelInterface.API
return result;
}
+ ///
+ /// Cleans one meter slot and releases its runtime resources.
+ ///
+ /// Slot id to clean. Must be greater than zero.
+ /// Cancellation token used to cancel the asynchronous operation.
+ ///
+ /// Result describing whether the slot cleanup succeeded or failed.
+ ///
+ ///
+ /// Thrown when is invalid.
+ ///
public async Task CleanSlotAsync(
int slot,
CancellationToken token = default)
@@ -140,6 +216,13 @@ namespace GenesisCordonelInterface.API
return result;
}
+ ///
+ /// Cleans all initialized meter slots and releases related runtime resources.
+ ///
+ /// Cancellation token used to cancel the asynchronous operation.
+ ///
+ /// Result describing whether cleanup of all slots succeeded or failed.
+ ///
public async Task CleanAllSlotsAsync(
CancellationToken token = default)
{
@@ -152,6 +235,17 @@ namespace GenesisCordonelInterface.API
#endregion
#region ================================== PASSWORD ==================================
+
+ ///
+ /// Sets runtime password for the meter assigned to the specified slot.
+ ///
+ /// Slot id. Must be greater than zero.
+ /// Password to assign to the meter.
+ /// Cancellation token used to cancel the asynchronous operation.
+ /// Result containing password update status.
+ ///
+ /// Thrown when slot id is invalid or password is empty.
+ ///
public async Task SetPasswordAsync(
int slot,
string password,
@@ -173,6 +267,16 @@ namespace GenesisCordonelInterface.API
#endregion
#region ================================== LOGIN ==================================
+
+ ///
+ /// Logs in to the meter assigned to the specified slot.
+ ///
+ /// Slot id. Must be greater than zero.
+ /// Cancellation token used to cancel the asynchronous operation.
+ /// Login result containing login state and status message.
+ ///
+ /// Thrown when is invalid.
+ ///
public async Task LoginOneSlotAsync(
int slot,
CancellationToken token = default)
@@ -189,6 +293,15 @@ namespace GenesisCordonelInterface.API
#region ================================== CONNECTION ==================================
+ ///
+ /// Connects the meter assigned to the specified slot.
+ ///
+ /// Slot id. Must be greater than zero.
+ /// Cancellation token used to cancel the asynchronous operation.
+ /// Connection result containing connection state and status message.
+ ///
+ /// Thrown when is invalid.
+ ///
public async Task ConnectOneSlotAsync(
int slot,
CancellationToken token = default)
@@ -203,6 +316,15 @@ namespace GenesisCordonelInterface.API
return result;
}
+ ///
+ /// Disconnects the meter assigned to the specified slot.
+ ///
+ /// Slot id. Must be greater than zero.
+ /// Cancellation token used to cancel the asynchronous operation.
+ /// Disconnect result containing final connection state and status message.
+ ///
+ /// Thrown when is invalid.
+ ///
public async Task DisconnectAsync(
int slot,
CancellationToken token = default)
@@ -219,6 +341,16 @@ namespace GenesisCordonelInterface.API
#endregion
#region ================================== PCB ==================================
+
+ ///
+ /// Reads PCB identifier from the meter assigned to the specified slot.
+ ///
+ /// Slot id. Must be greater than zero.
+ /// Cancellation token used to cancel the asynchronous operation.
+ /// Result containing PCB id and read status.
+ ///
+ /// Thrown when is invalid.
+ ///
public async Task GetPcbIdAsync(
int slot,
CancellationToken token = default)
@@ -234,6 +366,18 @@ namespace GenesisCordonelInterface.API
#region ================================== READ ==================================
+ ///
+ /// Reads a firmware register value from the meter assigned to the specified slot.
+ ///
+ /// Slot id. Must be greater than zero.
+ /// Register identifier to read.
+ /// Cancellation token used to cancel the asynchronous operation.
+ ///
+ /// Register read result containing raw register value and operation status.
+ ///
+ ///
+ /// Thrown when slot id or register name is invalid.
+ ///
public async Task ReadRegisterAsync(
int slot,
string registerName,
@@ -258,6 +402,25 @@ namespace GenesisCordonelInterface.API
#region ================================== WRITE ==================================
+ ///
+ /// Writes a value to a firmware register.
+ ///
+ /// Slot id. Must be greater than zero.
+ /// Register identifier to write.
+ /// Value to write.
+ ///
+ /// Indicates whether configuration should be permanently stored.
+ ///
+ ///
+ /// Indicates whether firmware system state should be refreshed after write.
+ ///
+ /// Cancellation token used to cancel the asynchronous operation.
+ ///
+ /// Register write result describing write status.
+ ///
+ ///
+ /// Thrown when slot id or register name is invalid.
+ ///
public async Task WriteRegisterAsync(
int slot,
string registerName,
@@ -290,6 +453,18 @@ namespace GenesisCordonelInterface.API
#region ================================== Password ==================================
+ ///
+ /// Updates meter password for the specified slot.
+ ///
+ /// Slot id. Must be greater than zero.
+ /// New password.
+ /// Cancellation token used to cancel the asynchronous operation.
+ ///
+ /// Password update result.
+ ///
+ ///
+ /// Thrown when slot id or password is invalid.
+ ///
public async Task SetMeterPasswordAsync(
int slot,
string password,
@@ -313,16 +488,36 @@ namespace GenesisCordonelInterface.API
#region ================================== DEBUG STATUS ==================================
+ ///
+ /// Gets runtime diagnostic information for all active workers.
+ ///
+ ///
+ /// Collection containing worker state, queue information,
+ /// current operation and activity timestamps.
+ ///
public List GetWorkerDebugStatuses()
{
return _innerMeterAPI.GetWorkerDebugStatuses();
}
+ ///
+ /// Gets runtime diagnostic information for all meter slots.
+ ///
+ ///
+ /// Collection containing slot state, connection state,
+ /// selected state and communication configuration.
+ ///
public List GetMeterBatchDebugStatuses()
{
return _innerMeterAPI.GetMeterBatchDebugStatuses();
}
+ ///
+ /// Raises meter batch status change notification.
+ ///
+ ///
+ /// Intended to notify external consumers after changes in meter state.
+ ///
public void RaiseMeterBatchStatusChanged()
{
var statuses = GetMeterBatchDebugStatuses();
@@ -332,121 +527,54 @@ namespace GenesisCordonelInterface.API
handler(statuses);
}
- // ----------------------------------------------------
#endregion
#region ================================== SLOT SELECTION ==================================
- // ----------------------------------------------------
+ ///
+ /// Sets selection state for a slot.
+ ///
+ /// Slot id.
+ /// Selection state.
public void SetSlotSelected(int slot, bool selected)
{
_innerMeterAPI.SetSlotSelected(slot, selected);
RaiseMeterBatchStatusChanged();
}
+ ///
+ /// Determines whether the specified slot is selected.
+ ///
+ /// Slot id.
+ ///
+ /// True if slot is selected; otherwise false.
+ ///
public bool IsSlotSelected(int slot)
{
return _innerMeterAPI.IsSlotSelected(slot);
}
+ ///
+ /// Gets all selected slot identifiers.
+ ///
+ ///
+ /// Ordered collection of selected slot ids.
+ ///
public List GetSelectedSlots()
{
return _innerMeterAPI.GetSelectedSlots();
}
- // ----------------------------------------------------
- #endregion
-
- #region ================================== SLOT PORT CONFIG ==================================
- // ----------------------------------------------------
-
- /*public void SetSlotRequestPort(int slot, string portName)
- {
- lock (_portLock)
- {
- if (string.IsNullOrWhiteSpace(portName))
- {
- _requestPorts.Remove(slot);
- }
- else
- {
- _requestPorts[slot] = new GciPortConfig
- {
- PortName = portName,
- Type = "Serial"
- };
- }
- }
-
- RaiseMeterBatchStatusChanged();
- }
-
- public void SetSlotStreamingPort(int slot, string portName)
- {
- lock (_portLock)
- {
- if (string.IsNullOrWhiteSpace(portName))
- {
- _streamingPorts.Remove(slot);
- }
- else
- {
- _streamingPorts[slot] = new GciPortConfig
- {
- PortName = portName,
- Type = "Serial"
- };
- }
- }
-
- RaiseMeterBatchStatusChanged();
- }
-
- public GciPortConfig? GetSlotRequestPort(int slot)
- {
- lock (_portLock)
- {
- GciPortConfig port;
- if (_requestPorts.TryGetValue(slot, out port))
- return port;
-
- return null;
- }
- }
-
- public GciPortConfig? GetSlotStreamingPort(int slot)
- {
- lock (_portLock)
- {
- GciPortConfig port;
- if (_streamingPorts.TryGetValue(slot, out port))
- return port;
-
- return null;
- }
- }*/
-
- // ----------------------------------------------------
- #endregion
-
- #region ================================== METER BATCH SETUP ==================================
-
- public void ReloadSlotSetup()
- {
- _innerMeterAPI.ReloadSlotSetup();
- RaiseMeterBatchStatusChanged();
- }
-
- public void SaveSlotSetup(List data)
- {
- _innerMeterAPI.SaveSlotSetup(data);
- RaiseMeterBatchStatusChanged();
- }
-
#endregion
#region ================================== Register names ==================================
+ ///
+ /// Gets all available firmware register identifiers.
+ ///
+ ///
+ /// Ordered collection of register names.
+ ///
public List GetAllRegisterNames()
{
return _innerMeterAPI.GetAllRegisterNames();
@@ -454,7 +582,7 @@ namespace GenesisCordonelInterface.API
#endregion
- // Preadjustment
+ // Laatzen Preadjustment processes
#region ================================== PreAdjustment ==================================
public Task PreAdjustment_DetectAsync(
diff --git a/GenesisCordonelInterface/Config/gci_config.json b/GenesisCordonelInterface/Config/gci_config.json
new file mode 100644
index 000000000..617517733
--- /dev/null
+++ b/GenesisCordonelInterface/Config/gci_config.json
@@ -0,0 +1,35 @@
+{
+ "_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 values from CSV",
+ "DataSource": "Relative path resolved from application directory",
+ "QueryTemplate": "CSV/JSON syntax: SELECT [ReturnColumn] WHERE [MatchColumn]=QUERYPARAM or SELECT COLUMN(1) WHERE COLUMN(0)=QUERYPARAM"
+ },
+
+ "Name": "PreAdjustmentCalibrationParams",
+ "Type": "LocalCsv",
+ "DataSource": "Data\\preadjustment_params.csv",
+ "QueryTemplate": "SELECT [Offset] WHERE [PcbId] = QUERYPARAM"
+ }
+ }
+}
diff --git a/GenesisCordonelInterface/Core/DataStorage/Config/GciConfig.cs b/GenesisCordonelInterface/Core/DataStorage/Config/GciConfig.cs
new file mode 100644
index 000000000..c4cf686c0
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Config/GciConfig.cs
@@ -0,0 +1,32 @@
+namespace GenesisCordonelInterface.Core.DataStorage.Config
+{
+ ///
+ /// 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":
+ /// {
+ /// ...
+ /// }
+ /// }
+ ///
+ public class GciConfig
+ {
+ ///
+ /// Data storage configuration section.
+ ///
+ /// Contains definitions for all configured readers and writers,
+ /// such as:
+ ///
+ /// - MeterLoginPasswords
+ /// - PreAdjustmentCalibrationParams
+ /// - future storage providers
+ ///
+ public GciDataStorageConfig DataStorage { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/DataStorage/Config/GciConfigLoader.cs b/GenesisCordonelInterface/Core/DataStorage/Config/GciConfigLoader.cs
new file mode 100644
index 000000000..b88db0d3b
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Config/GciConfigLoader.cs
@@ -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
+{
+ ///
+ /// 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=...
+ ///
+ public static class GciConfigLoader
+ {
+ ///
+ /// Loads default GCI configuration from:
+ ///
+ /// Config\gci_config.json
+ ///
+ ///
+ /// Loaded GCI configuration.
+ ///
+ public static GciConfig LoadDefault()
+ {
+ string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
+
+ string configPath = Path.Combine(
+ baseDirectory,
+ "Config",
+ "gci_config.json");
+
+ return Load(configPath, baseDirectory);
+ }
+
+ ///
+ /// Loads GCI configuration from specified file.
+ ///
+ ///
+ /// Path to configuration json file.
+ ///
+ ///
+ /// Base directory used for resolving relative paths.
+ ///
+ ///
+ /// Loaded configuration object.
+ ///
+ 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(json);
+
+ if (config == null)
+ throw new InvalidOperationException(
+ "GCI config could not be loaded.");
+
+ NormalizeDataStoragePaths(
+ config,
+ baseDirectory ?? Path.GetDirectoryName(configPath));
+
+ return config;
+ }
+
+ ///
+ /// Normalizes paths for all configured data storage entries.
+ ///
+ /// Converts relative file paths into absolute paths.
+ /// Database connection strings remain untouched.
+ ///
+ ///
+ /// Loaded configuration object.
+ ///
+ ///
+ /// Base path used for relative resolution.
+ ///
+ private static void NormalizeDataStoragePaths(
+ GciConfig config,
+ string baseDirectory)
+ {
+ if (config.DataStorage == null)
+ return;
+
+ NormalizePath(
+ config.DataStorage.MeterLoginPasswords,
+ baseDirectory);
+
+ NormalizePath(
+ config.DataStorage.PreAdjustmentCalibrationParams,
+ baseDirectory);
+ }
+
+ ///
+ /// Converts relative file paths into absolute paths.
+ ///
+ /// Applies only to:
+ /// - LocalCsv
+ /// - RemoteCsv
+ /// - LocalJson
+ /// - RemoteJson
+ ///
+ /// Does not modify:
+ /// - Database connection strings
+ /// - REST URLs
+ ///
+ ///
+ /// Storage configuration.
+ ///
+ ///
+ /// Base path for resolution.
+ ///
+ 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));
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/DataStorage/Config/GciDataStorageConfig.cs b/GenesisCordonelInterface/Core/DataStorage/Config/GciDataStorageConfig.cs
new file mode 100644
index 000000000..526fa2e24
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Config/GciDataStorageConfig.cs
@@ -0,0 +1,30 @@
+using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
+
+namespace GenesisCordonelInterface.Core.DataStorage.Config
+{
+ ///
+ /// 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
+ ///
+ public class GciDataStorageConfig
+ {
+ ///
+ /// Configuration for meter login password lookup.
+ ///
+ public DataStorageConfig MeterLoginPasswords { get; set; }
+
+ ///
+ /// Configuration for pre-adjustment calibration data lookup.
+ ///
+ public DataStorageConfig PreAdjustmentCalibrationParams { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Contracts/IDataStorageReader..cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Contracts/IDataStorageReader..cs
new file mode 100644
index 000000000..342f37170
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Contracts/IDataStorageReader..cs
@@ -0,0 +1,49 @@
+namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts
+{
+ ///
+ /// 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.
+ ///
+ public interface IDataStorageReader
+ {
+ ///
+ /// Reads data from configured storage using provided query.
+ ///
+ ///
+ /// Query object containing lookup parameters.
+ ///
+ ///
+ /// Storage-specific result object.
+ ///
+ object GetData(Models.DataQuery query);
+
+ ///
+ /// Tests whether configured data source is accessible.
+ ///
+ ///
+ /// Enables detailed diagnostic output.
+ ///
+ ///
+ /// Diagnostic result of source test.
+ ///
+ Models.ReaderDiagnosticResult TestSource(bool enableDiagnostics);
+
+ ///
+ /// Tests whether configured query is valid for the data source.
+ ///
+ ///
+ /// Enables detailed diagnostic output.
+ ///
+ ///
+ /// Diagnostic result of query test.
+ ///
+ Models.ReaderDiagnosticResult TestQuery(bool enableDiagnostics);
+ }
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/DataStorageReaderFactory.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/DataStorageReaderFactory.cs
new file mode 100644
index 000000000..0613a1153
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/DataStorageReaderFactory.cs
@@ -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
+{
+ ///
+ /// 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
+ ///
+ public static class DataStorageReaderFactory
+ {
+ ///
+ /// Creates appropriate reader implementation
+ /// according to storage configuration.
+ ///
+ ///
+ /// Data storage configuration loaded from gci_config.json.
+ ///
+ ///
+ /// Configured storage reader implementation.
+ ///
+ ///
+ /// Configuration is null.
+ ///
+ ///
+ /// Unsupported storage type.
+ ///
+ 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}'");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataQuery.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataQuery.cs
new file mode 100644
index 000000000..47762bb88
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataQuery.cs
@@ -0,0 +1,58 @@
+using System.Collections.Generic;
+
+namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
+{
+ ///
+ /// 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.
+ ///
+ public class DataQuery
+ {
+ ///
+ /// Collection of query values.
+ ///
+ /// Single item:
+ ///
+ /// QueryParams[0]
+ ///
+ /// represents one lookup.
+ ///
+ /// Multiple items may be processed
+ /// as batch requests.
+ ///
+ public List QueryParams { get; }
+ = new List();
+ }
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataStorageConfig.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataStorageConfig.cs
new file mode 100644
index 000000000..2bc89bc64
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataStorageConfig.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
+{
+ ///
+ /// Configuration for one storage source.
+ /// Loaded from gci_config.json
+ ///
+ public class DataStorageConfig
+ {
+ ///
+ /// Logical storage name.
+ /// Example:
+ /// MeterLoginPasswords
+ /// CalibrationParameters
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Storage implementation type.
+ ///
+ public DataStorageType Type { get; set; }
+
+ ///
+ /// Connection string,
+ /// file path,
+ /// URL etc.
+ ///
+ public string DataSource { get; set; }
+
+ ///
+ /// Query template.
+ /// Example:
+ ///
+ /// SQL:
+ /// SELECT Password
+ /// FROM Passwords
+ /// WHERE PcbId=QUERYPARAM
+ ///
+ /// CSV:
+ /// SELECT [Password]
+ /// WHERE [PcbId]=QUERYPARAM
+ ///
+ public string QueryTemplate { get; set; }
+ }
+}
diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataStorageType.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataStorageType.cs
new file mode 100644
index 000000000..a28659ae9
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DataStorageType.cs
@@ -0,0 +1,79 @@
+namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
+{
+ ///
+ /// 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"
+ ///
+ public enum DataStorageType
+ {
+ ///
+ /// REST API endpoint.
+ ///
+ /// Example:
+ ///
+ /// https://api.company.com/passwords
+ ///
+ RestApi,
+
+ ///
+ /// Database located on remote server.
+ ///
+ /// Example:
+ ///
+ /// SQL server on network machine.
+ ///
+ RemoteDatabase,
+
+ ///
+ /// Database located on local machine.
+ ///
+ /// Example:
+ ///
+ /// SQL LocalDB
+ /// SQLite
+ /// local SQL Server instance
+ ///
+ LocalDatabase,
+
+ ///
+ /// JSON source stored remotely.
+ ///
+ RemoteJson,
+
+ ///
+ /// JSON file stored locally.
+ ///
+ LocalJson,
+
+ ///
+ /// CSV source stored remotely.
+ ///
+ RemoteCsv,
+
+ ///
+ /// CSV file stored locally.
+ ///
+ LocalCsv
+ }
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DatabaseSearchResult.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DatabaseSearchResult.cs
new file mode 100644
index 000000000..2f69501ee
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/DatabaseSearchResult.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
+{
+ public class DatabaseSearchResult
+ {
+ public bool Found { get; set; }
+
+ public string Query { get; set; }
+
+ public Dictionary Values { get; set; }
+
+ public DatabaseSearchResult()
+ {
+ Values = new Dictionary();
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/ReaderDiagnosticResult.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/ReaderDiagnosticResult.cs
new file mode 100644
index 000000000..b456631c2
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Models/ReaderDiagnosticResult.cs
@@ -0,0 +1,141 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
+{
+ ///
+ /// 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.
+ ///
+ public class ReaderDiagnosticResult
+ {
+ ///
+ /// Indicates whether operation completed successfully.
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// Human readable summary message.
+ ///
+ /// Example:
+ ///
+ /// "Connection successful."
+ ///
+ /// or:
+ ///
+ /// "Column not found."
+ ///
+ public string Message { get; set; }
+
+ ///
+ /// Detailed diagnostic output lines.
+ ///
+ /// Used for debugging and troubleshooting.
+ ///
+ public List Diagnostics { get; set; }
+
+ ///
+ /// Optional operation payload.
+ ///
+ /// Example:
+ ///
+ /// loaded CSV lines
+ /// SQL test value
+ /// parsed content
+ ///
+ public object Data { get; set; }
+
+ ///
+ /// Creates empty diagnostic result.
+ ///
+ public ReaderDiagnosticResult()
+ {
+ Diagnostics = new List();
+ }
+
+ ///
+ /// Creates successful diagnostic result.
+ ///
+ ///
+ /// Success message.
+ ///
+ ///
+ /// Successful result object.
+ ///
+ public static ReaderDiagnosticResult SuccessResult(
+ string message = "OK")
+ {
+ return new ReaderDiagnosticResult
+ {
+ Success = true,
+ Message = message
+ };
+ }
+
+ ///
+ /// Creates failed diagnostic result.
+ ///
+ ///
+ /// Failure message.
+ ///
+ ///
+ /// Failed result object.
+ ///
+ public static ReaderDiagnosticResult Failure(
+ string message)
+ {
+ return new ReaderDiagnosticResult
+ {
+ Success = false,
+ Message = message
+ };
+ }
+
+ ///
+ /// Converts diagnostic information into display text.
+ ///
+ /// Output contains:
+ ///
+ /// diagnostic lines
+ /// +
+ /// summary message
+ ///
+ ///
+ /// Formatted text representation.
+ ///
+ 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/CsvDataStorageReader.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/CsvDataStorageReader.cs
new file mode 100644
index 000000000..004d6414a
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/CsvDataStorageReader.cs
@@ -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
+{
+ ///
+ /// 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()
+ ///
+ public class CsvDataStorageReader : IDataStorageReader
+ {
+ private readonly DataStorageConfig config;
+
+ private string resolvedPath;
+ private string[] loadedLines;
+ private string[] loadedHeaders;
+
+ ///
+ /// Creates CSV data storage reader using provided configuration.
+ ///
+ ///
+ /// Data storage configuration loaded from gci_config.json.
+ ///
+ public CsvDataStorageReader(DataStorageConfig config)
+ {
+ this.config = config ?? throw new ArgumentNullException(nameof(config));
+ }
+
+ ///
+ /// Reads values from CSV using configured QueryTemplate.
+ ///
+ ///
+ /// Query parameters used for lookup.
+ ///
+ ///
+ /// ReaderDiagnosticResult containing lookup result.
+ ///
+ 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;
+ }
+
+ ///
+ /// Validates CSV source accessibility and content.
+ ///
+ ///
+ /// Enables detailed diagnostic output.
+ ///
+ ///
+ /// Diagnostic result.
+ ///
+ public ReaderDiagnosticResult TestSource(bool enableDiagnostics)
+ {
+ return ConnectToSource(enableDiagnostics);
+ }
+
+ ///
+ /// Validates configured QueryTemplate against CSV header.
+ ///
+ ///
+ /// Enables detailed diagnostic output.
+ ///
+ ///
+ /// Diagnostic result.
+ ///
+ 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;
+ }
+
+ ///
+ /// Opens CSV file and loads its content.
+ /// Validates file existence and header.
+ ///
+ ///
+ /// Enables detailed diagnostic output.
+ ///
+ ///
+ /// Source validation result.
+ ///
+ 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;
+ }
+
+
+ ///
+ /// Executes configured QueryTemplate against loaded CSV data.
+ ///
+ ///
+ /// Query values used for matching.
+ ///
+ ///
+ /// Enables detailed diagnostic output.
+ ///
+ ///
+ /// Query result.
+ ///
+ 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 matchedValues = new List();
+
+ 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;
+ }
+
+ ///
+ /// Resolves column index either by name or by explicit index.
+ ///
+ ///
+ /// CSV header columns.
+ ///
+ ///
+ /// Parsed QueryTemplate column definition.
+ ///
+ ///
+ /// Zero-based column index.
+ ///
+ 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.");
+ }
+
+ ///
+ /// Splits CSV line using common separators.
+ /// Supports:
+ /// ';'
+ /// ','
+ /// '\t'
+ ///
+ ///
+ /// Input CSV line.
+ ///
+ ///
+ /// Parsed columns.
+ ///
+ 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 };
+ }
+
+ ///
+ /// Adds diagnostic line when diagnostics are enabled.
+ ///
+ private void Log(
+ ReaderDiagnosticResult result,
+ bool enableDiagnostics,
+ string message)
+ {
+ if (!enableDiagnostics || result == null)
+ return;
+
+ result.Diagnostics.Add(message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/DatabaseDataStorageReader.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/DatabaseDataStorageReader.cs
new file mode 100644
index 000000000..acf2ccee3
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/DatabaseDataStorageReader.cs
@@ -0,0 +1,273 @@
+using System;
+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
+{
+ ///
+ /// 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
+ ///
+ /// The placeholder is internally converted to SQL parameter @value.
+ ///
+ public class DatabaseDataStorageReader : IDataStorageReader
+ {
+ ///
+ /// Data storage configuration containing connection string and query template.
+ ///
+ private readonly DataStorageConfig config;
+
+ ///
+ /// Creates SQL Server data storage reader using provided configuration.
+ ///
+ ///
+ /// Data storage configuration loaded from gci_config.json.
+ ///
+ public DatabaseDataStorageReader(DataStorageConfig config)
+ {
+ this.config = config ?? throw new ArgumentNullException(nameof(config));
+ }
+
+ ///
+ /// Executes configured SQL query and returns first matching row.
+ ///
+ ///
+ /// Query object containing lookup parameter.
+ ///
+ ///
+ /// DatabaseSearchResult containing returned SQL columns and values.
+ ///
+ 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();
+
+ using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.SingleRow))
+ {
+ DatabaseSearchResult result = new DatabaseSearchResult
+ {
+ Query = sqlText
+ };
+
+ if (!reader.Read())
+ {
+ result.Found = false;
+ return result;
+ }
+
+ result.Found = true;
+
+ for (int i = 0; i < reader.FieldCount; i++)
+ {
+ object value = reader.GetValue(i);
+ result.Values[reader.GetName(i)] =
+ value == DBNull.Value ? null : value;
+ }
+
+ return result;
+ }
+ }
+ }
+
+ ///
+ /// Tests whether SQL Server connection can be opened.
+ ///
+ ///
+ /// Enables detailed diagnostic output.
+ ///
+ ///
+ /// Diagnostic result of SQL connection test.
+ ///
+ 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;
+ }
+
+ ///
+ /// Tests whether configured SQL query can be prepared and executed.
+ ///
+ ///
+ /// Enables detailed diagnostic output.
+ ///
+ ///
+ /// Diagnostic result of query execution test.
+ ///
+ 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;
+ }
+
+ ///
+ /// Converts configured QueryTemplate to executable SQL text.
+ ///
+ ///
+ /// SQL query template containing QUERYPARAM placeholder.
+ ///
+ ///
+ /// SQL text with QUERYPARAM replaced by @value parameter.
+ ///
+ 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");
+ }
+
+ ///
+ /// Extracts first query parameter value.
+ ///
+ ///
+ /// Data query containing query parameters.
+ ///
+ ///
+ /// First query parameter value.
+ ///
+ 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];
+ }
+
+ ///
+ /// Adds lookup parameter to SQL command.
+ ///
+ ///
+ /// SQL command.
+ ///
+ ///
+ /// Query parameter value.
+ ///
+ private static void AddQueryParameter(SqlCommand command, object value)
+ {
+ command.Parameters.Clear();
+
+ SqlParameter parameter = command.Parameters.Add("@value", SqlDbType.Variant);
+ parameter.Value = value ?? DBNull.Value;
+ }
+
+ ///
+ /// Adds diagnostic line when diagnostics are enabled.
+ ///
+ ///
+ /// Diagnostic result object.
+ ///
+ ///
+ /// Indicates whether diagnostics are enabled.
+ ///
+ ///
+ /// Diagnostic message.
+ ///
+ private static void Log(
+ ReaderDiagnosticResult result,
+ bool enableDiagnostics,
+ string message)
+ {
+ if (!enableDiagnostics || result == null)
+ return;
+
+ result.Diagnostics.Add(message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/JsonDataStorageReader.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/JsonDataStorageReader.cs
new file mode 100644
index 000000000..206dc4774
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/JsonDataStorageReader.cs
@@ -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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/RestApiDataStorageReader.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/RestApiDataStorageReader.cs
new file mode 100644
index 000000000..0c1f012af
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Providers/RestApiDataStorageReader.cs
@@ -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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Searching/SearchOrderDefinition.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Searching/SearchOrderDefinition.cs
new file mode 100644
index 000000000..36a1ceebe
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Searching/SearchOrderDefinition.cs
@@ -0,0 +1,125 @@
+using System;
+
+namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Searching
+{
+ ///
+ /// Parsed representation of QueryTemplate expression.
+ ///
+ /// Example:
+ ///
+ /// SELECT [Password] WHERE [PcbId] = QUERYPARAM
+ ///
+ /// becomes:
+ ///
+ /// SelectColumn:
+ /// [Password]
+ ///
+ /// WhereColumn:
+ /// [PcbId]
+ ///
+ public class SearchOrderDefinition
+ {
+ ///
+ /// Column to be returned from lookup result.
+ ///
+ /// Example:
+ ///
+ /// SELECT [Password]
+ ///
+ /// returns:
+ ///
+ /// [Password]
+ ///
+ public ColumnReference SelectColumn { get; set; }
+
+ ///
+ /// Column used for row matching.
+ ///
+ /// Example:
+ ///
+ /// WHERE [PcbId] = QUERYPARAM
+ ///
+ /// uses:
+ ///
+ /// [PcbId]
+ ///
+ public ColumnReference WhereColumn { get; set; }
+ }
+
+ ///
+ /// Represents one column definition inside QueryTemplate.
+ ///
+ /// Supports two forms:
+ ///
+ /// [ColumnName]
+ ///
+ /// or:
+ ///
+ /// COLUMN(number)
+ ///
+ /// Examples:
+ ///
+ /// [Password]
+ ///
+ /// COLUMN(2)
+ ///
+ public class ColumnReference
+ {
+ ///
+ /// Column name used in named lookup mode.
+ ///
+ /// Example:
+ ///
+ /// [Password]
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Zero-based column index used in indexed lookup mode.
+ ///
+ /// Example:
+ ///
+ /// COLUMN(2)
+ ///
+ public int? Index { get; set; }
+
+ ///
+ /// Indicates whether column uses name-based lookup.
+ ///
+ public bool HasName
+ {
+ get
+ {
+ return !string.IsNullOrWhiteSpace(Name);
+ }
+ }
+
+ ///
+ /// Indicates whether column uses index-based lookup.
+ ///
+ public bool HasIndex
+ {
+ get
+ {
+ return Index.HasValue;
+ }
+ }
+
+ ///
+ /// Returns human readable representation.
+ ///
+ ///
+ /// Column expression text.
+ ///
+ public override string ToString()
+ {
+ if (HasName)
+ return "[" + Name + "]";
+
+ if (HasIndex)
+ return "COLUMN(" + Index.Value + ")";
+
+ return "";
+ }
+ }
+}
\ No newline at end of file
diff --git a/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Searching/SearchOrderParser.cs b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Searching/SearchOrderParser.cs
new file mode 100644
index 000000000..89612e828
--- /dev/null
+++ b/GenesisCordonelInterface/Core/DataStorage/Reading/Common/Searching/SearchOrderParser.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Searching
+{
+ ///
+ /// 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.
+ ///
+ public static class SearchOrderParser
+ {
+ ///
+ /// Full QueryTemplate validation pattern.
+ ///
+ /// Expected syntax:
+ ///
+ /// SELECT [Column] WHERE [Column] = QUERYPARAM
+ ///
+ private static readonly Regex FullPattern = new Regex(
+ @"^\s*SELECT\s+(?