common/Hardware/WaterMeter/Genesis/GenesisCore/GenesisConfigReader.cs
2026-04-23 17:50:07 +02:00

362 lines
14 KiB
C#

using System.Text.RegularExpressions;
namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore
{
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Applications;
using Registers;
using Registers.DataTypes;
using Registers.Json;
using WaterMeterRegisters;
/// <summary>
/// JSON filer reader for generation of register lists
/// </summary>
public class GenesisConfigurationReader
{
/// <summary>
/// Registers defined in configuration.json file
/// </summary>
public List<IRegister> ConfigRegistersDefinitions { get; }
= new List<IRegister>();
/// <summary>
/// Applications defined in configuration.json file
/// </summary>
public List<ApplicationDefinition> ConfigApplicationDefinitions { get; }
= new List<ApplicationDefinition>();
/// <summary>
/// Information collected during 'configuration.json' read
/// </summary>
public readonly InterfaceInfo InterfaceInfo = new InterfaceInfo();
/// <summary>
/// Ctor
/// </summary>
public GenesisConfigurationReader() { }
/// <summary>
/// Ctor
/// </summary>
/// <param name="configFilePath">the file path for configuration.json as interface description to the meter</param>
public GenesisConfigurationReader(String configFilePath)
{
if (string.IsNullOrEmpty(configFilePath))
{
throw new ApplicationException("JsonFileRegisterReader needs at least one file path");
}
if (!File.Exists(configFilePath))
{
throw new ApplicationException($" {configFilePath} doesn't exists");
}
BuildRegisterList(File.ReadAllText(configFilePath));
}
/// <summary>
/// Convert file content to register list
/// </summary>
/// <param name="configurationJson"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
/// <remarks date="??" author="Stoyan Slatev">
/// - Initial.
/// </remarks>
/// <remarks date="2025-Aug-05" author="Thomas Wiedebusch">
/// - AppId from Byte to UInt16 including typecast for Byte[] return. To external, it will be used as Byte
/// as before this change. But being able to parse the configuration.json with OPTICALINTERFACE using the
/// AppId 256, which exceeds the byte range as this isn't a real application, but will be used to identify
/// the interface (configuration.json) version.
/// </remarks>
/// <remarks date="2025-Aug-07" author="Thomas Wiedebusch">
/// - Extract the max supported versions for EMEA and NA to check the supported FW.
/// </remarks>
/// <remarks date="2025-Sep-30" author="Thomas Wiedebusch">
/// - Supported application list introduced.
/// </remarks>
/// <remarks date="2025-Oct-10" author="Thomas Wiedebusch">
/// - Data size introduced including the new 'ByteArray' type and size to get rid of new defined 'uintxx_t'
/// data types.
/// </remarks>
public void BuildRegisterList(String configurationJson)
{
IDictionary<String, AppSection> sections;
try
{
sections = JsonConvert
.DeserializeObject<IDictionary<String, AppSection>>(configurationJson, SerializerSettings)
?? new Dictionary<String, AppSection>();
}
catch (Exception)
{
sections = new Dictionary<String, AppSection>();
}
foreach (var sectionKVP in sections)
{
var appId = sectionKVP.Value.Id;
var appName = sectionKVP.Key;
var appVersion = sectionKVP.Value.Version.Last;
if (appName.Equals("OPTICALINTERFACE"))
{
var builds = sectionKVP.Value.Builds;
var emeaBuilds = builds["emea"];
var naBuilds = builds["na"];
InterfaceInfo.SupportedFwVersions = new List<String>();
foreach (var build in emeaBuilds)
{
InterfaceInfo.SupportedFwVersions.Add(build.FW);
}
foreach (var build in naBuilds)
{
InterfaceInfo.SupportedFwVersions.Add(build.FW);
}
InterfaceInfo.InterfaceVersion = GenesisMeter.BuildFwVersionStringFromDec(appVersion);
continue;
}
ConfigApplicationDefinitions.Add(new ApplicationDefinition
{
AppId = appId,
AppName = appName,
AppVersion = appVersion,
});
var section = sectionKVP.Value;
var registers = section.Registers;
if (registers is null)
{
continue;
}
foreach (var registerKVP in registers)
{
var register = registerKVP.Value;
if (register is null)
{
continue;
}
var details = register.Details;
if (details is null)
{
continue;
}
var regId = register.Id;
foreach (var detail in details)
{
var values = detail.Values;
ConfigRegistersDefinitions.Add(new RegisterDefinition
{
AppAddress = appId,
AppName = appName,
DataType = GetType(detail.Type),
DataSize = GetSize(detail.Type),
Default = values?.Default is Int64 d ? d : default(Int64?),
IsAvailable = false,
Maximum = values?.Maximum is Int64 max ? max : default(Int64?),
Minimum = values?.Minimum is Int64 min ? min : default(Int64?),
RegAddressInApp = regId,
// RegisterAddress = new byte[] { appId, regId },
RegisterDetail = detail,
RegisterName = registerKVP.Key,
RestoreCapability = new StaticType(detail.StaticType)
});
}
}
}
}
/// <summary>
/// Settings
/// </summary>
private static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings
{
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
Converters =
{
AccessConverter.Singleton,
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
},
};
/// <summary>
/// Convert data types from interface 'configuration.json' to CLR type or array
/// </summary>
/// <param name="value"></param>
/// <returns>type</returns>
/// <exception cref="Exception"></exception>
/// <remarks date="??" author="Stoyan Slatev">
/// - Initial.
/// </remarks>
/// <remarks date="2025-Oct-10" author="Thomas Wiedebusch">
/// - Parsed all 'uintxx_t' which are not based on CLR types to byte-array
/// </remarks>
private static Type GetType(String value)
{
// Catch all uintxx_t to split to standard CLR or to array
var lowStrValue = value.ToLower();
if (lowStrValue.Contains("uint") && lowStrValue.Contains("_t"))
{
switch (lowStrValue)
{
case "uint8_t": return typeof(Byte);
case "uint16_t": return typeof(UInt16);
case "uint32_t": return typeof(UInt32);
case "uint64_t": return typeof(UInt64);
default:
return typeof(ByteArray);
}
}
// Catch the signed standard CLR and some special types
switch (lowStrValue)
{
// CLR types
case "bool_t": return typeof(Boolean);
case "int8_t": return typeof(SByte);
case "int16_t": return typeof(Int16);
case "int32_t": return typeof(Int32);
case "int64_t": return typeof(Int64);
case "string": return typeof(String);
// Special types defined in FW interface file 'configuration.json'
case "rpc": return typeof(Rpc);
case "time_t": return typeof(TimeT);
case "status_t": return typeof(StatusT);
case "enum8": return typeof(Enum8);
default:
throw new Exception($"Data type {value} in unknown!");
}
}
/// <summary>
/// Build the data size
/// </summary>
/// <param name="value"></param>
/// <returns>size</returns>
/// <remarks date="2025-Oct-10" author="Thomas Wiedebusch">
/// - Initial: - parsed all 'uintxx_t' which are not based on CLR types to byte-array
/// </remarks>
private static Int32 GetSize(String value)
{
// Catch all uintxx_t to split to standard CLR or to array
var lowStrValue = value.ToLower();
if (lowStrValue.Contains("uint") && lowStrValue.Contains("_t"))
{
switch (lowStrValue)
{
case "uint8_t": return sizeof(Byte);
case "uint16_t": return sizeof(UInt16);
case "uint32_t": return sizeof(UInt32);
case "uint64_t": return sizeof(UInt64);
default:
// Extract the size from the string
var dataSizeStr = Regex.Replace(lowStrValue, "[^0-9]", string.Empty);
if (Int32.TryParse(dataSizeStr, out var dataSize))
return dataSize / 8;
throw new Exception($"Data size {lowStrValue} cannot be converted!");
}
}
// Catch the signed standard CLR and some special types
switch (lowStrValue)
{
// CLR types
case "bool_t": return sizeof(Boolean);
case "int8_t": return sizeof(SByte);
case "int16_t": return sizeof(Int16);
case "int32_t": return sizeof(Int32);
case "int64_t": return sizeof(Int64);
// Take just some value as the string length is unknown
case "string": return 6 * 4;
// Special types defined in FW interface file 'configuration.json'
// Remote procedure call is always a 4 byte value
case "rpc": return sizeof(UInt32);
// Time will be in seconds since 01. Jan 2000 00:00:00 UTC as signed Int32
case "time_t": return sizeof(Int32);
case "status_t": return sizeof(UInt32);
case "enum8": return sizeof(Byte);
default:
throw new Exception($"Data type {value} in unknown!");
}
}
}
/// <summary>
///
/// </summary>
public class AccessConverter : JsonConverter
{
/// <summary>
///
/// </summary>
/// <param name="t"></param>
/// <returns></returns>
public override Boolean CanConvert(Type t) => t == typeof(Access) || t == typeof(Access?);
/// <summary>
///
/// </summary>
/// <param name="reader"></param>
/// <param name="t"></param>
/// <param name="existingValue"></param>
/// <param name="serializer"></param>
/// <returns></returns>
public override Object ReadJson(JsonReader reader, Type t, Object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}
var value = serializer.Deserialize<String>(reader);
return Enum.TryParse(value, true, out Access access) ? access : default(Access);
}
/// <summary>
///
/// </summary>
/// <param name="writer"></param>
/// <param name="untypedValue"></param>
/// <param name="serializer"></param>
public override void WriteJson(JsonWriter writer, Object untypedValue, JsonSerializer serializer)
{
if (untypedValue is Access access)
{
writer.WriteValue(access.ToString());
}
else
{
throw new Exception("Cannot marshal type Lvl");
}
}
/// <summary>
///
/// </summary>
public static readonly AccessConverter Singleton = new AccessConverter();
}
}