feat: add sitestream.proto definition and generated gRPC stubs
Define the SiteStreamService proto for real-time instance event streaming (attribute value changes, alarm state changes) from site nodes to central. Add pre-generated C# stubs following the existing LmxProxy pattern, gRPC NuGet packages with FrameworkReference for ASP.NET Core server types, and proto roundtrip tests.
This commit is contained in:
52
src/ScadaLink.Communication/Protos/sitestream.proto
Normal file
52
src/ScadaLink.Communication/Protos/sitestream.proto
Normal file
@@ -0,0 +1,52 @@
|
||||
syntax = "proto3";
|
||||
option csharp_namespace = "ScadaLink.Communication.Grpc";
|
||||
package sitestream;
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
service SiteStreamService {
|
||||
rpc SubscribeInstance(InstanceStreamRequest) returns (stream SiteStreamEvent);
|
||||
}
|
||||
|
||||
message InstanceStreamRequest {
|
||||
string correlation_id = 1;
|
||||
string instance_unique_name = 2;
|
||||
}
|
||||
|
||||
message SiteStreamEvent {
|
||||
string correlation_id = 1;
|
||||
oneof event {
|
||||
AttributeValueUpdate attribute_changed = 2;
|
||||
AlarmStateUpdate alarm_changed = 3;
|
||||
}
|
||||
}
|
||||
|
||||
enum Quality {
|
||||
QUALITY_UNSPECIFIED = 0;
|
||||
QUALITY_GOOD = 1;
|
||||
QUALITY_UNCERTAIN = 2;
|
||||
QUALITY_BAD = 3;
|
||||
}
|
||||
|
||||
enum AlarmStateEnum {
|
||||
ALARM_STATE_UNSPECIFIED = 0;
|
||||
ALARM_STATE_NORMAL = 1;
|
||||
ALARM_STATE_ACTIVE = 2;
|
||||
}
|
||||
|
||||
message AttributeValueUpdate {
|
||||
string instance_unique_name = 1;
|
||||
string attribute_path = 2;
|
||||
string attribute_name = 3;
|
||||
string value = 4;
|
||||
Quality quality = 5;
|
||||
google.protobuf.Timestamp timestamp = 6;
|
||||
}
|
||||
|
||||
message AlarmStateUpdate {
|
||||
string instance_unique_name = 1;
|
||||
string alarm_name = 2;
|
||||
AlarmStateEnum state = 3;
|
||||
int32 priority = 4;
|
||||
google.protobuf.Timestamp timestamp = 5;
|
||||
}
|
||||
@@ -7,15 +7,18 @@
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Akka" Version="1.5.62" />
|
||||
<PackageReference Include="Akka.Remote" Version="1.5.62" />
|
||||
<PackageReference Include="Akka.Cluster" Version="1.5.62" />
|
||||
<PackageReference Include="Akka.Cluster.Tools" Version="1.5.62" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.5" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.29.3" />
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.71.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
1396
src/ScadaLink.Communication/SiteStreamGrpc/Sitestream.cs
Normal file
1396
src/ScadaLink.Communication/SiteStreamGrpc/Sitestream.cs
Normal file
File diff suppressed because it is too large
Load Diff
145
src/ScadaLink.Communication/SiteStreamGrpc/SitestreamGrpc.cs
Normal file
145
src/ScadaLink.Communication/SiteStreamGrpc/SitestreamGrpc.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
// <auto-generated>
|
||||
// Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
// source: sitestream.proto
|
||||
// </auto-generated>
|
||||
#pragma warning disable 0414, 1591, 8981, 0612
|
||||
#region Designer generated code
|
||||
|
||||
using grpc = global::Grpc.Core;
|
||||
|
||||
namespace ScadaLink.Communication.Grpc {
|
||||
public static partial class SiteStreamService
|
||||
{
|
||||
static readonly string __ServiceName = "sitestream.SiteStreamService";
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
static void __Helper_SerializeMessage(global::Google.Protobuf.IMessage message, grpc::SerializationContext context)
|
||||
{
|
||||
#if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION
|
||||
if (message is global::Google.Protobuf.IBufferMessage)
|
||||
{
|
||||
context.SetPayloadLength(message.CalculateSize());
|
||||
global::Google.Protobuf.MessageExtensions.WriteTo(message, context.GetBufferWriter());
|
||||
context.Complete();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
context.Complete(global::Google.Protobuf.MessageExtensions.ToByteArray(message));
|
||||
}
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
static class __Helper_MessageCache<T>
|
||||
{
|
||||
public static readonly bool IsBufferMessage = global::System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(global::Google.Protobuf.IBufferMessage)).IsAssignableFrom(typeof(T));
|
||||
}
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
static T __Helper_DeserializeMessage<T>(grpc::DeserializationContext context, global::Google.Protobuf.MessageParser<T> parser) where T : global::Google.Protobuf.IMessage<T>
|
||||
{
|
||||
#if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION
|
||||
if (__Helper_MessageCache<T>.IsBufferMessage)
|
||||
{
|
||||
return parser.ParseFrom(context.PayloadAsReadOnlySequence());
|
||||
}
|
||||
#endif
|
||||
return parser.ParseFrom(context.PayloadAsNewBuffer());
|
||||
}
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
static readonly grpc::Marshaller<global::ScadaLink.Communication.Grpc.InstanceStreamRequest> __Marshaller_sitestream_InstanceStreamRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.Communication.Grpc.InstanceStreamRequest.Parser));
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
static readonly grpc::Marshaller<global::ScadaLink.Communication.Grpc.SiteStreamEvent> __Marshaller_sitestream_SiteStreamEvent = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ScadaLink.Communication.Grpc.SiteStreamEvent.Parser));
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
static readonly grpc::Method<global::ScadaLink.Communication.Grpc.InstanceStreamRequest, global::ScadaLink.Communication.Grpc.SiteStreamEvent> __Method_SubscribeInstance = new grpc::Method<global::ScadaLink.Communication.Grpc.InstanceStreamRequest, global::ScadaLink.Communication.Grpc.SiteStreamEvent>(
|
||||
grpc::MethodType.ServerStreaming,
|
||||
__ServiceName,
|
||||
"SubscribeInstance",
|
||||
__Marshaller_sitestream_InstanceStreamRequest,
|
||||
__Marshaller_sitestream_SiteStreamEvent);
|
||||
|
||||
/// <summary>Service descriptor</summary>
|
||||
public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor
|
||||
{
|
||||
get { return global::ScadaLink.Communication.Grpc.SitestreamReflection.Descriptor.Services[0]; }
|
||||
}
|
||||
|
||||
/// <summary>Base class for server-side implementations of SiteStreamService</summary>
|
||||
[grpc::BindServiceMethod(typeof(SiteStreamService), "BindService")]
|
||||
public abstract partial class SiteStreamServiceBase
|
||||
{
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
public virtual global::System.Threading.Tasks.Task SubscribeInstance(global::ScadaLink.Communication.Grpc.InstanceStreamRequest request, grpc::IServerStreamWriter<global::ScadaLink.Communication.Grpc.SiteStreamEvent> responseStream, grpc::ServerCallContext context)
|
||||
{
|
||||
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Client for SiteStreamService</summary>
|
||||
public partial class SiteStreamServiceClient : grpc::ClientBase<SiteStreamServiceClient>
|
||||
{
|
||||
/// <summary>Creates a new client for SiteStreamService</summary>
|
||||
/// <param name="channel">The channel to use to make remote calls.</param>
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
public SiteStreamServiceClient(grpc::ChannelBase channel) : base(channel)
|
||||
{
|
||||
}
|
||||
/// <summary>Creates a new client for SiteStreamService that uses a custom <c>CallInvoker</c>.</summary>
|
||||
/// <param name="callInvoker">The callInvoker to use to make remote calls.</param>
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
public SiteStreamServiceClient(grpc::CallInvoker callInvoker) : base(callInvoker)
|
||||
{
|
||||
}
|
||||
/// <summary>Protected parameterless constructor to allow creation of test doubles.</summary>
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
protected SiteStreamServiceClient() : base()
|
||||
{
|
||||
}
|
||||
/// <summary>Protected constructor to allow creation of configured clients.</summary>
|
||||
/// <param name="configuration">The client configuration.</param>
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
protected SiteStreamServiceClient(ClientBaseConfiguration configuration) : base(configuration)
|
||||
{
|
||||
}
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
public virtual grpc::AsyncServerStreamingCall<global::ScadaLink.Communication.Grpc.SiteStreamEvent> SubscribeInstance(global::ScadaLink.Communication.Grpc.InstanceStreamRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
|
||||
{
|
||||
return SubscribeInstance(request, new grpc::CallOptions(headers, deadline, cancellationToken));
|
||||
}
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
public virtual grpc::AsyncServerStreamingCall<global::ScadaLink.Communication.Grpc.SiteStreamEvent> SubscribeInstance(global::ScadaLink.Communication.Grpc.InstanceStreamRequest request, grpc::CallOptions options)
|
||||
{
|
||||
return CallInvoker.AsyncServerStreamingCall(__Method_SubscribeInstance, null, options, request);
|
||||
}
|
||||
/// <summary>Creates a new instance of client from given <c>ClientBaseConfiguration</c>.</summary>
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
protected override SiteStreamServiceClient NewInstance(ClientBaseConfiguration configuration)
|
||||
{
|
||||
return new SiteStreamServiceClient(configuration);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates service definition that can be registered with a server</summary>
|
||||
/// <param name="serviceImpl">An object implementing the server-side handling logic.</param>
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
public static grpc::ServerServiceDefinition BindService(SiteStreamServiceBase serviceImpl)
|
||||
{
|
||||
return grpc::ServerServiceDefinition.CreateBuilder()
|
||||
.AddMethod(__Method_SubscribeInstance, serviceImpl.SubscribeInstance).Build();
|
||||
}
|
||||
|
||||
/// <summary>Register service method with a service binder with or without implementation. Useful when customizing the service binding logic.
|
||||
/// Note: this method is part of an experimental API that can change or be removed without any prior notice.</summary>
|
||||
/// <param name="serviceBinder">Service methods will be bound by calling <c>AddMethod</c> on this object.</param>
|
||||
/// <param name="serviceImpl">An object implementing the server-side handling logic.</param>
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
public static void BindService(grpc::ServiceBinderBase serviceBinder, SiteStreamServiceBase serviceImpl)
|
||||
{
|
||||
serviceBinder.AddMethod(__Method_SubscribeInstance, serviceImpl == null ? null : new grpc::ServerStreamingServerMethod<global::ScadaLink.Communication.Grpc.InstanceStreamRequest, global::ScadaLink.Communication.Grpc.SiteStreamEvent>(serviceImpl.SubscribeInstance));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
129
tests/ScadaLink.Communication.Tests/Grpc/ProtoRoundtripTests.cs
Normal file
129
tests/ScadaLink.Communication.Tests/Grpc/ProtoRoundtripTests.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using ScadaLink.Communication.Grpc;
|
||||
|
||||
namespace ScadaLink.Communication.Tests.Grpc;
|
||||
|
||||
public class ProtoRoundtripTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(Quality.Good)]
|
||||
[InlineData(Quality.Uncertain)]
|
||||
[InlineData(Quality.Bad)]
|
||||
[InlineData(Quality.Unspecified)]
|
||||
public void AttributeValueUpdate_RoundTrip(Quality quality)
|
||||
{
|
||||
var timestamp = Timestamp.FromDateTimeOffset(
|
||||
new DateTimeOffset(2026, 3, 21, 12, 0, 0, TimeSpan.Zero));
|
||||
|
||||
var original = new AttributeValueUpdate
|
||||
{
|
||||
InstanceUniqueName = "Site1.Pump01",
|
||||
AttributePath = "Modules.PressureModule",
|
||||
AttributeName = "CurrentPressure",
|
||||
Value = "42.5",
|
||||
Quality = quality,
|
||||
Timestamp = timestamp
|
||||
};
|
||||
|
||||
var bytes = original.ToByteArray();
|
||||
var deserialized = AttributeValueUpdate.Parser.ParseFrom(bytes);
|
||||
|
||||
Assert.Equal(original.InstanceUniqueName, deserialized.InstanceUniqueName);
|
||||
Assert.Equal(original.AttributePath, deserialized.AttributePath);
|
||||
Assert.Equal(original.AttributeName, deserialized.AttributeName);
|
||||
Assert.Equal(original.Value, deserialized.Value);
|
||||
Assert.Equal(original.Quality, deserialized.Quality);
|
||||
Assert.Equal(original.Timestamp, deserialized.Timestamp);
|
||||
Assert.Equal(timestamp.Seconds, deserialized.Timestamp.Seconds);
|
||||
Assert.Equal(timestamp.Nanos, deserialized.Timestamp.Nanos);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(AlarmStateEnum.AlarmStateNormal)]
|
||||
[InlineData(AlarmStateEnum.AlarmStateActive)]
|
||||
[InlineData(AlarmStateEnum.AlarmStateUnspecified)]
|
||||
public void AlarmStateUpdate_RoundTrip(AlarmStateEnum state)
|
||||
{
|
||||
var timestamp = Timestamp.FromDateTimeOffset(
|
||||
new DateTimeOffset(2026, 3, 21, 12, 30, 0, TimeSpan.Zero));
|
||||
|
||||
var original = new AlarmStateUpdate
|
||||
{
|
||||
InstanceUniqueName = "Site1.Pump01",
|
||||
AlarmName = "HighPressure",
|
||||
State = state,
|
||||
Priority = 3,
|
||||
Timestamp = timestamp
|
||||
};
|
||||
|
||||
var bytes = original.ToByteArray();
|
||||
var deserialized = AlarmStateUpdate.Parser.ParseFrom(bytes);
|
||||
|
||||
Assert.Equal(original.InstanceUniqueName, deserialized.InstanceUniqueName);
|
||||
Assert.Equal(original.AlarmName, deserialized.AlarmName);
|
||||
Assert.Equal(original.State, deserialized.State);
|
||||
Assert.Equal(original.Priority, deserialized.Priority);
|
||||
Assert.Equal(original.Timestamp, deserialized.Timestamp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SiteStreamEvent_OneOf_AttributeChanged()
|
||||
{
|
||||
var evt = new SiteStreamEvent
|
||||
{
|
||||
CorrelationId = "corr-123",
|
||||
AttributeChanged = new AttributeValueUpdate
|
||||
{
|
||||
InstanceUniqueName = "Site1.Pump01",
|
||||
AttributePath = "Modules.PressureModule",
|
||||
AttributeName = "CurrentPressure",
|
||||
Value = "42.5",
|
||||
Quality = Quality.Good,
|
||||
Timestamp = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow)
|
||||
}
|
||||
};
|
||||
|
||||
Assert.Equal(SiteStreamEvent.EventOneofCase.AttributeChanged, evt.EventCase);
|
||||
Assert.NotNull(evt.AttributeChanged);
|
||||
Assert.Null(evt.AlarmChanged);
|
||||
|
||||
// Round-trip
|
||||
var bytes = evt.ToByteArray();
|
||||
var deserialized = SiteStreamEvent.Parser.ParseFrom(bytes);
|
||||
|
||||
Assert.Equal(SiteStreamEvent.EventOneofCase.AttributeChanged, deserialized.EventCase);
|
||||
Assert.Equal("corr-123", deserialized.CorrelationId);
|
||||
Assert.Equal("42.5", deserialized.AttributeChanged.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SiteStreamEvent_OneOf_AlarmChanged()
|
||||
{
|
||||
var evt = new SiteStreamEvent
|
||||
{
|
||||
CorrelationId = "corr-456",
|
||||
AlarmChanged = new AlarmStateUpdate
|
||||
{
|
||||
InstanceUniqueName = "Site1.Pump01",
|
||||
AlarmName = "HighPressure",
|
||||
State = AlarmStateEnum.AlarmStateActive,
|
||||
Priority = 1,
|
||||
Timestamp = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow)
|
||||
}
|
||||
};
|
||||
|
||||
Assert.Equal(SiteStreamEvent.EventOneofCase.AlarmChanged, evt.EventCase);
|
||||
Assert.NotNull(evt.AlarmChanged);
|
||||
Assert.Null(evt.AttributeChanged);
|
||||
|
||||
// Round-trip
|
||||
var bytes = evt.ToByteArray();
|
||||
var deserialized = SiteStreamEvent.Parser.ParseFrom(bytes);
|
||||
|
||||
Assert.Equal(SiteStreamEvent.EventOneofCase.AlarmChanged, deserialized.EventCase);
|
||||
Assert.Equal("corr-456", deserialized.CorrelationId);
|
||||
Assert.Equal(AlarmStateEnum.AlarmStateActive, deserialized.AlarmChanged.State);
|
||||
Assert.Equal(1, deserialized.AlarmChanged.Priority);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user