Skip to content

Contracts Analyzers

Voltimax.Iot.Contracts.Analyzers contains two Roslyn incremental source generators that produce the platform's core metric and device asset types at compile time.

Metrics Source Generator

Transforms JSON metric schemas into strongly-typed C# code. The JSON files are the single source of truth for all metric definitions across the platform.

Setup

xml
<ItemGroup>
  <PackageReference Include="Voltimax.Iot.Contracts.Analyzers"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
  <AdditionalFiles Include="schemas/metrics/metrics.json" />
  <AdditionalFiles Include="schemas/metrics/registries/*.json" />
</ItemGroup>

Input Schema

The generator reads from two kinds of JSON files:

metrics.json - global configuration and enum definitions:

json
{
  "registryPath": "./registries",
  "enums": [
    {
      "name": "Unit",
      "description": "Units of measurement",
      "values": [
        { "value": "Watt", "description": "Watt (W)" }
      ]
    }
  ]
}

Registry files (e.g., registries/battery.json) - domain-specific metric definitions:

json
{
  "id": "battery",
  "name": "Battery",
  "description": "Battery pack measurements",
  "metrics": [
    {
      "name": "PackVoltage",
      "key": "pack_voltage",
      "unit": "Volt",
      "description": "Pack DC voltage"
    }
  ]
}

See Data Model for the full schema reference.

Generated Output

The generator produces the following types in namespace Voltimax.Iot.Contracts.Metrics:

Enums

Unit and MetricProfile enums from the enums array in metrics.json:

csharp
public enum Unit
{
    Unknown,
    Watt,
    Volt,
    Ampere,
    // ...
}

MetricDomain

Static class with domain name constants:

csharp
public static class MetricDomain
{
    public const string Battery = "battery";
    public const string Energy = "energy";
    // ...

    public static readonly string[] AllDomains = [...];
}

{Domain}MetricDefinitions

Static field per metric, carrying all metadata:

csharp
public static class BatteryMetricDefinitions
{
    public static readonly Metric PackVoltage = new(...);
    public static readonly Metric PackCurrent = new(...);
    // ...
}

{Domain}MetricValues

Wide-format POCO with JSON serialization attributes for telemetry:

csharp
public class BatteryMetricValues
{
    [JsonPropertyName("pack_voltage")]
    public double? PackVoltage { get; set; }

    [JsonPropertyName("pack_current")]
    public double? PackCurrent { get; set; }
    // ...

    public IEnumerable<MetricValue> ToEnumerable() { ... }
}

{Domain}MetricCatalog

Lookup structures using frozen collections:

csharp
public static class BatteryMetricCatalog
{
    public static FrozenSet<Metric> All { get; }
    public static FrozenDictionary<string, Metric> ById { get; }
    public static bool TryGetById(string id, out Metric metric);
    public static int Count => 45;
    public static string Category => "Battery";
}

MetricCatalog (Global)

Aggregates all domain catalogs:

csharp
public static class MetricCatalog
{
    public static FrozenSet<Metric> All { get; }
    public static FrozenDictionary<string, FrozenSet<Metric>> ByCategory { get; }
    public static FrozenDictionary<string, Metric> ById { get; }
    public static bool TryGetById(string id, out Metric metric);
    public static FrozenSet<Metric> GetByCategory(string category);
    public static IReadOnlyList<string> Categories { get; }
}

TelemetryFrame

Top-level telemetry container with a property per domain:

csharp
public class TelemetryFrame
{
    public DateTime Timestamp { get; set; }
    public string GatewayId { get; set; }
    public AssetInfo Asset { get; set; }
    public BatteryMetricValues? Battery { get; set; }
    public EnergyMetricValues? Energy { get; set; }
    // ...
}

Plus TelemetryFrameExtensions for format conversion.

Adding a New Metric

  1. Edit the appropriate registry file in schemas/metrics/registries/ (e.g., battery.json)
  2. Add the metric definition with name, key, unit, description
  3. Optionally add scopes, profile, or counter
  4. Build - the generator picks up the change automatically
json
{
  "name": "CellTemperatureMax",
  "key": "cell_temperature_max",
  "unit": "DegreeCelsius",
  "description": "Maximum cell temperature",
  "profile": "Standard"
}

No CLI command needed. The generated code updates on the next build.


Device Asset Source Generator

Transforms YAML device definitions into abstract C# base classes for Modbus devices. Each YAML file describes a physical device's register layout and produces a base class with typed read/write methods.

INFO

The Contracts Analyzers project handles the schema-level asset generation. The more protocol-specific generators (with full ReadAsync/WriteAsync implementations, scaling, and enum casting) live in Voltimax.Edge.Control.Analyzers.

Setup

xml
<ItemGroup>
  <AdditionalFiles Include="schemas/assets/modbus/*.yaml" />
</ItemGroup>

Input Schema

YAML device definitions in schemas/assets/:

yaml
device:
  id: EastronSdm630
  manufacturer: Eastron
  model: SDM630-Modbus
  description: Three-phase energy meter
  assetTypes: [Grid, Building, Solar]

protocol:
  type: modbus
  addressing: oneBased
  registerPacking: wordHighFirst
  byteOrder: bigEndian

registers:
  input:
    voltages:
      - id: phase1Voltage
        address: 30001
        type: float32
        access: read
        unit: volt

Generated Output

For each device YAML file, the generator produces an abstract base class:

  • Register address constants
  • Typed read/write method signatures for each register
  • Device-specific enum types from enumRef fields
  • XML documentation with address, unit, and scaling metadata

Adding a New Device

  1. Create a YAML file in schemas/assets/modbus/ following the schema format
  2. Build - the generator creates the base class automatically
  3. Create a concrete implementation extending the generated base class