Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 45 additions & 10 deletions Modspec.Model/Generation/ModspecModelGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using Modspec.Model.Extensions;

Expand All @@ -23,12 +24,21 @@ public class ModelGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var pipeline = context.AdditionalTextsProvider
.Where(static (file) => Path.GetFileName(file.Path).EndsWith("json"))
.Select(static (model, cancellationToken) =>
var jsonFiles = context.AdditionalTextsProvider
.Where(static (file) => Path.GetFileName(file.Path).EndsWith("json"));

var generateChangeDetection = context.AnalyzerConfigOptionsProvider
.Select(static (provider, _) =>
{
provider.GlobalOptions.TryGetValue("build_property.ModspecGenerateChangeDetectionFactory", out string? value);
return String.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
});

var pipeline = jsonFiles.Combine(generateChangeDetection)
.Select(static (pair, cancellationToken) =>
{
string path = model.Path;
ModelCompiler.TryGenerate(model.Path, out string? code);
string path = pair.Left.Path;
ModelCompiler.TryGenerate(path, pair.Right, out string? code);
return (path, code);
})
.Where(static (pair) => !String.IsNullOrEmpty(pair.code));
Expand All @@ -43,7 +53,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

private class ModelCompiler
{
public static bool TryGenerate(string path, [NotNullWhen(true)] out string? result)
public static bool TryGenerate(string path, bool generateChangeDetectionFactory, [NotNullWhen(true)] out string? result)
{
result = default;
Schema? schema;
Expand Down Expand Up @@ -77,7 +87,8 @@ namespace {schema.Name};
List<string> bufferInitialisers = [];
List<string> fieldInitialisers = [];
List<ConstructorParameter> constructorParams = [];
WriteGroups(schema.Groups, mainWriter, appendixWriter, bufferInitialisers, fieldInitialisers, constructorParams);
List<string> bitfieldPointsWithLevels = [];
WriteGroups(schema.Groups, mainWriter, appendixWriter, bufferInitialisers, fieldInitialisers, constructorParams, bitfieldPointsWithLevels: bitfieldPointsWithLevels);

foreach (RepeatingGroup repeatingGroup in schema.RepeatingGroups)
{
Expand Down Expand Up @@ -137,6 +148,11 @@ namespace {schema.Name};
mainWriter.WriteLine("}");
mainWriter.WriteLine();

if (generateChangeDetectionFactory && bitfieldPointsWithLevels.Count > 0)
{
WriteChangeDetectionFactory(schema.Name, bitfieldPointsWithLevels, appendixWriter);
}

result = mainWriter.ToString() + appendixWriter.ToString();
return true;
}
Expand Down Expand Up @@ -168,7 +184,7 @@ private static void WriteFieldsAndConstructor(string name, StringWriter mainWrit
mainWriter.WriteLine($"{indent}\t}}");
}

private static void WriteGroups(IReadOnlyCollection<Group> groups, StringWriter mainWriter, StringWriter appendixWriter, List<string> bufferInitialisers, List<string> fieldInitialisers, List<ConstructorParameter> constructorParams, string indent = "", string readOffsetField = "")
private static void WriteGroups(IReadOnlyCollection<Group> groups, StringWriter mainWriter, StringWriter appendixWriter, List<string> bufferInitialisers, List<string> fieldInitialisers, List<ConstructorParameter> constructorParams, string indent = "", string readOffsetField = "", List<string>? bitfieldPointsWithLevels = null)
{
foreach (Group group in groups)
{
Expand All @@ -185,7 +201,7 @@ private static void WriteGroups(IReadOnlyCollection<Group> groups, StringWriter
// supplied count of elements, rather than max size of array)
throw new InvalidOperationException($"An array must be the last (or only) element in a group.");
}
WritePoint(point, bufferName, group.Table, mainWriter, appendixWriter, fieldInitialisers, constructorParams, ref maxOffset, ref bufferSize, indent);
WritePoint(point, bufferName, group.Table, mainWriter, appendixWriter, fieldInitialisers, constructorParams, ref maxOffset, ref bufferSize, indent, bitfieldPointsWithLevels);
}
if (String.IsNullOrEmpty(bufferSize))
{
Expand All @@ -209,7 +225,7 @@ private static void WriteGroups(IReadOnlyCollection<Group> groups, StringWriter
}
}

private static void WritePoint(Point point, string bufferName, Table table, StringWriter mainWriter, StringWriter appendixWriter, List<string> fieldInitialisers, List<ConstructorParameter> constructorParams, ref int maxOffset, ref string bufferSize, string indent = "")
private static void WritePoint(Point point, string bufferName, Table table, StringWriter mainWriter, StringWriter appendixWriter, List<string> fieldInitialisers, List<ConstructorParameter> constructorParams, ref int maxOffset, ref string bufferSize, string indent = "", List<string>? bitfieldPointsWithLevels = null)
{
string type;
string readMethod;
Expand Down Expand Up @@ -337,6 +353,7 @@ private static void WritePoint(Point point, string bufferName, Table table, Stri
appendixWriter.WriteLine();
if (isFlags && masksByLevel.Count > 0)
{
bitfieldPointsWithLevels?.Add(point.Name);
appendixWriter.WriteLine($"public static class {point.Name}Extensions");
appendixWriter.WriteLine("{");
appendixWriter.WriteLine($"\tpublic static Level GetLevel(this {point.Name} self)");
Expand Down Expand Up @@ -421,6 +438,24 @@ private static void WritePoint(Point point, string bufferName, Table table, Stri
maxOffset += point.SizeInBytes * (point.Count?.MaxValue ?? 1);
}

private static void WriteChangeDetectionFactory(string schemaName, List<string> points, StringWriter writer)
{
string clientName = $"{schemaName}Client";
writer.WriteLine($"public static class {schemaName}ChangeDetection");
writer.WriteLine("{");
writer.WriteLine($"\tpublic static BitfieldChangeDetector<{clientName}> CreateDetector()");
writer.WriteLine("\t{");
writer.WriteLine($"\t\treturn new BitfieldChangeDetector<{clientName}>()");
for (int i = 0; i < points.Count; i++)
{
string terminator = i < points.Count - 1 ? "" : ";";
writer.WriteLine($"\t\t\t.Track(c => c.{points[i]}, v => v.GetLevel()){terminator}");
}
writer.WriteLine("\t}");
writer.WriteLine("}");
writer.WriteLine();
}

private static string ToFieldName(string name)
{
Span<char> result = stackalloc char[name.Length + 1];
Expand Down
79 changes: 79 additions & 0 deletions Modspec.Test/BitfieldChangeDetector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Simplified BitfieldChangeDetector for testing the generated factory.
* The real implementation lives in the consuming project
* and may use more sophisticated change tracking.
*/
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Modspec.Model;

public class BitfieldChangeDetector<TClient>
{
private readonly List<ITracker> _trackers = [];

public BitfieldChangeDetector<TClient> Track<T>(Func<TClient, T> getter, Func<T, Level> getLevel) where T : struct, Enum
{
_trackers.Add(new Tracker<T>(getter, getLevel));
return this;
}

public async ValueTask CheckAsync(TClient client, Func<ulong, string, Level, ValueTask> onChanged)
{
bool anyChanged = false;
ulong combinedCode = 0;
Level highestLevel = Level.None;
List<string>? descriptions = null;

foreach (ITracker tracker in _trackers)
{
bool changed = tracker.TryCheck(client, out ulong code, out string desc, out Level level);
anyChanged |= changed;
combinedCode |= code;
if (level > highestLevel) highestLevel = level;
if (level != Level.None)
{
descriptions ??= [];
descriptions.Add(desc);
}
}

if (anyChanged)
{
string combined = descriptions is not null
? String.Join(", ", descriptions)
: String.Empty;
await onChanged(combinedCode, combined, highestLevel);
}
}

private interface ITracker
{
bool TryCheck(TClient client, out ulong code, out string desc, out Level level);
}

private class Tracker<T> : ITracker where T : struct, Enum
{
private ulong _previous;
private readonly Func<TClient, T> _getter;
private readonly Func<T, Level> _getLevel;

public Tracker(Func<TClient, T> getter, Func<T, Level> getLevel)
{
_getter = getter;
_getLevel = getLevel;
}

public bool TryCheck(TClient client, out ulong code, out string desc, out Level level)
{
T current = _getter(client);
code = Convert.ToUInt64(current);
bool changed = code != _previous;
_previous = code;
level = _getLevel(current);
desc = current.ToString();
return changed;
}
}
}
5 changes: 5 additions & 0 deletions Modspec.Test/Modspec.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<VSTestLogger>junit</VSTestLogger>
<IsTestProject>true</IsTestProject>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<ModspecGenerateChangeDetectionFactory>true</ModspecGenerateChangeDetectionFactory>
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -40,4 +41,8 @@
<AdditionalFiles Include="somebms.json" />
</ItemGroup>

<ItemGroup>
<CompilerVisibleProperty Include="ModspecGenerateChangeDetectionFactory" />
</ItemGroup>

</Project>
64 changes: 64 additions & 0 deletions Modspec.Test/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,70 @@ public void TestErrorLevels()
Assert.That(errors1.GetLevel(), Is.EqualTo(Level.Emergency));
}

[Test]
public void TestChangeDetectionFactoryCreatesDetector()
{
BitfieldChangeDetector<SomeBmsClient> detector = SomeBmsChangeDetection.CreateDetector();
Assert.That(detector, Is.Not.Null);
}

[Test]
public async Task TestChangeDetectionFactoryDetectsChange()
{
MockModbusClient mockClient = new MockModbusClient();
SomeBmsClient bmsClient = new SomeBmsClient(mockClient, 2, 2, 480, 100);
BitfieldChangeDetector<SomeBmsClient> detector = SomeBmsChangeDetection.CreateDetector();

// initial read with no errors — check twice to confirm no spurious changes
await bmsClient.ReadWarningsErrorsEmergenciesAsync();
bool called = false;
await detector.CheckAsync(bmsClient, (code, desc, level) =>
{
called = true;
return ValueTask.CompletedTask;
});
Assert.That(called, Is.False);

// introduce an error and re-read
mockClient.DiscreteInputs.Span[1] = 0b10000000; // StringTerminalDischargeOverCurrentError
await bmsClient.ReadWarningsErrorsEmergenciesAsync();

Level reportedLevel = Level.None;
called = false;
await detector.CheckAsync(bmsClient, (code, desc, level) =>
{
called = true;
reportedLevel = level;
return ValueTask.CompletedTask;
});
Assert.That(called, Is.True);
Assert.That(reportedLevel, Is.EqualTo(Level.Error));
}

[Test]
public async Task TestChangeDetectionFactoryReportsHighestLevel()
{
MockModbusClient mockClient = new MockModbusClient();
SomeBmsClient bmsClient = new SomeBmsClient(mockClient, 2, 2, 480, 100);
BitfieldChangeDetector<SomeBmsClient> detector = SomeBmsChangeDetection.CreateDetector();

// prime the detector
await bmsClient.ReadWarningsErrorsEmergenciesAsync();
await detector.CheckAsync(bmsClient, (_, _, _) => ValueTask.CompletedTask);

// set both a warning (bit 0) and an emergency (bit 2) on StringErrors1
mockClient.DiscreteInputs.Span[1] = 0b00000101;
await bmsClient.ReadWarningsErrorsEmergenciesAsync();

Level reportedLevel = Level.None;
await detector.CheckAsync(bmsClient, (code, desc, level) =>
{
reportedLevel = level;
return ValueTask.CompletedTask;
});
Assert.That(reportedLevel, Is.EqualTo(Level.Emergency));
}

[Test]
public async Task TestRangeValidation()
{
Expand Down
Loading