Compare commits
4 Commits
b76ce09221
...
9b0a80dcbd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b0a80dcbd | ||
|
|
64ee316609 | ||
|
|
deb58e1f17 | ||
|
|
826cfbee31 |
@@ -53,6 +53,8 @@ public static class SiteCommands
|
||||
var descOption = new Option<string?>("--description") { Description = "Site description" };
|
||||
var nodeAOption = new Option<string?>("--node-a-address") { Description = "Akka address for Node A" };
|
||||
var nodeBOption = new Option<string?>("--node-b-address") { Description = "Akka address for Node B" };
|
||||
var grpcNodeAOption = new Option<string?>("--grpc-node-a-address") { Description = "gRPC address for Node A" };
|
||||
var grpcNodeBOption = new Option<string?>("--grpc-node-b-address") { Description = "gRPC address for Node B" };
|
||||
|
||||
var cmd = new Command("create") { Description = "Create a new site" };
|
||||
cmd.Add(nameOption);
|
||||
@@ -60,6 +62,8 @@ public static class SiteCommands
|
||||
cmd.Add(descOption);
|
||||
cmd.Add(nodeAOption);
|
||||
cmd.Add(nodeBOption);
|
||||
cmd.Add(grpcNodeAOption);
|
||||
cmd.Add(grpcNodeBOption);
|
||||
cmd.SetAction(async (ParseResult result) =>
|
||||
{
|
||||
var name = result.GetValue(nameOption)!;
|
||||
@@ -67,9 +71,11 @@ public static class SiteCommands
|
||||
var desc = result.GetValue(descOption);
|
||||
var nodeA = result.GetValue(nodeAOption);
|
||||
var nodeB = result.GetValue(nodeBOption);
|
||||
var grpcNodeA = result.GetValue(grpcNodeAOption);
|
||||
var grpcNodeB = result.GetValue(grpcNodeBOption);
|
||||
return await CommandHelpers.ExecuteCommandAsync(
|
||||
result, urlOption, formatOption, usernameOption, passwordOption,
|
||||
new CreateSiteCommand(name, identifier, desc, nodeA, nodeB));
|
||||
new CreateSiteCommand(name, identifier, desc, nodeA, nodeB, grpcNodeA, grpcNodeB));
|
||||
});
|
||||
return cmd;
|
||||
}
|
||||
@@ -81,6 +87,8 @@ public static class SiteCommands
|
||||
var descOption = new Option<string?>("--description") { Description = "Site description" };
|
||||
var nodeAOption = new Option<string?>("--node-a-address") { Description = "Akka address for Node A" };
|
||||
var nodeBOption = new Option<string?>("--node-b-address") { Description = "Akka address for Node B" };
|
||||
var grpcNodeAOption = new Option<string?>("--grpc-node-a-address") { Description = "gRPC address for Node A" };
|
||||
var grpcNodeBOption = new Option<string?>("--grpc-node-b-address") { Description = "gRPC address for Node B" };
|
||||
|
||||
var cmd = new Command("update") { Description = "Update an existing site" };
|
||||
cmd.Add(idOption);
|
||||
@@ -88,6 +96,8 @@ public static class SiteCommands
|
||||
cmd.Add(descOption);
|
||||
cmd.Add(nodeAOption);
|
||||
cmd.Add(nodeBOption);
|
||||
cmd.Add(grpcNodeAOption);
|
||||
cmd.Add(grpcNodeBOption);
|
||||
cmd.SetAction(async (ParseResult result) =>
|
||||
{
|
||||
var id = result.GetValue(idOption);
|
||||
@@ -95,9 +105,11 @@ public static class SiteCommands
|
||||
var desc = result.GetValue(descOption);
|
||||
var nodeA = result.GetValue(nodeAOption);
|
||||
var nodeB = result.GetValue(nodeBOption);
|
||||
var grpcNodeA = result.GetValue(grpcNodeAOption);
|
||||
var grpcNodeB = result.GetValue(grpcNodeBOption);
|
||||
return await CommandHelpers.ExecuteCommandAsync(
|
||||
result, urlOption, formatOption, usernameOption, passwordOption,
|
||||
new UpdateSiteCommand(id, name, desc, nodeA, nodeB));
|
||||
new UpdateSiteCommand(id, name, desc, nodeA, nodeB, grpcNodeA, grpcNodeB));
|
||||
});
|
||||
return cmd;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,18 @@
|
||||
placeholder="akka.tcp://scadalink@host:port/user/site-communication" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 align-items-end mt-1">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">gRPC Node A Address</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_formGrpcNodeAAddress"
|
||||
placeholder="http://host:8083" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">gRPC Node B Address (optional)</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_formGrpcNodeBAddress"
|
||||
placeholder="http://host:8083" />
|
||||
</div>
|
||||
</div>
|
||||
@if (_formError != null)
|
||||
{
|
||||
<div class="text-danger small mt-1">@_formError</div>
|
||||
@@ -92,6 +104,8 @@
|
||||
<th>Description</th>
|
||||
<th>Node A</th>
|
||||
<th>Node B</th>
|
||||
<th>gRPC Node A</th>
|
||||
<th>gRPC Node B</th>
|
||||
<th>Data Connections</th>
|
||||
<th style="width: 260px;">Actions</th>
|
||||
</tr>
|
||||
@@ -100,7 +114,7 @@
|
||||
@if (_sites.Count == 0)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="8" class="text-muted text-center">No sites configured.</td>
|
||||
<td colspan="10" class="text-muted text-center">No sites configured.</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var site in _sites)
|
||||
@@ -112,6 +126,8 @@
|
||||
<td class="text-muted small">@(site.Description ?? "—")</td>
|
||||
<td class="small text-truncate" style="max-width: 200px;" title="@site.NodeAAddress">@(site.NodeAAddress ?? "—")</td>
|
||||
<td class="small text-truncate" style="max-width: 200px;" title="@site.NodeBAddress">@(site.NodeBAddress ?? "—")</td>
|
||||
<td class="small text-truncate" style="max-width: 200px;" title="@site.GrpcNodeAAddress">@(site.GrpcNodeAAddress ?? "—")</td>
|
||||
<td class="small text-truncate" style="max-width: 200px;" title="@site.GrpcNodeBAddress">@(site.GrpcNodeBAddress ?? "—")</td>
|
||||
<td>
|
||||
@{
|
||||
var conns = _siteConnections.GetValueOrDefault(site.Id);
|
||||
@@ -163,6 +179,8 @@
|
||||
private string? _formDescription;
|
||||
private string? _formNodeAAddress;
|
||||
private string? _formNodeBAddress;
|
||||
private string? _formGrpcNodeAAddress;
|
||||
private string? _formGrpcNodeBAddress;
|
||||
private string? _formError;
|
||||
|
||||
private bool _deploying;
|
||||
@@ -207,6 +225,8 @@
|
||||
_formDescription = null;
|
||||
_formNodeAAddress = null;
|
||||
_formNodeBAddress = null;
|
||||
_formGrpcNodeAAddress = null;
|
||||
_formGrpcNodeBAddress = null;
|
||||
_formError = null;
|
||||
_showForm = true;
|
||||
}
|
||||
@@ -219,6 +239,8 @@
|
||||
_formDescription = site.Description;
|
||||
_formNodeAAddress = site.NodeAAddress;
|
||||
_formNodeBAddress = site.NodeBAddress;
|
||||
_formGrpcNodeAAddress = site.GrpcNodeAAddress;
|
||||
_formGrpcNodeBAddress = site.GrpcNodeBAddress;
|
||||
_formError = null;
|
||||
_showForm = true;
|
||||
}
|
||||
@@ -248,6 +270,8 @@
|
||||
_editingSite.Description = _formDescription?.Trim();
|
||||
_editingSite.NodeAAddress = _formNodeAAddress?.Trim();
|
||||
_editingSite.NodeBAddress = _formNodeBAddress?.Trim();
|
||||
_editingSite.GrpcNodeAAddress = _formGrpcNodeAAddress?.Trim();
|
||||
_editingSite.GrpcNodeBAddress = _formGrpcNodeBAddress?.Trim();
|
||||
await SiteRepository.UpdateSiteAsync(_editingSite);
|
||||
}
|
||||
else
|
||||
@@ -261,7 +285,9 @@
|
||||
{
|
||||
Description = _formDescription?.Trim(),
|
||||
NodeAAddress = _formNodeAAddress?.Trim(),
|
||||
NodeBAddress = _formNodeBAddress?.Trim()
|
||||
NodeBAddress = _formNodeBAddress?.Trim(),
|
||||
GrpcNodeAAddress = _formGrpcNodeAAddress?.Trim(),
|
||||
GrpcNodeBAddress = _formGrpcNodeBAddress?.Trim()
|
||||
};
|
||||
await SiteRepository.AddSiteAsync(site);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ public class Site
|
||||
public string? Description { get; set; }
|
||||
public string? NodeAAddress { get; set; }
|
||||
public string? NodeBAddress { get; set; }
|
||||
public string? GrpcNodeAAddress { get; set; }
|
||||
public string? GrpcNodeBAddress { get; set; }
|
||||
|
||||
public Site(string name, string siteIdentifier)
|
||||
{
|
||||
|
||||
@@ -2,8 +2,8 @@ namespace ScadaLink.Commons.Messages.Management;
|
||||
|
||||
public record ListSitesCommand;
|
||||
public record GetSiteCommand(int SiteId);
|
||||
public record CreateSiteCommand(string Name, string SiteIdentifier, string? Description, string? NodeAAddress = null, string? NodeBAddress = null);
|
||||
public record UpdateSiteCommand(int SiteId, string Name, string? Description, string? NodeAAddress = null, string? NodeBAddress = null);
|
||||
public record CreateSiteCommand(string Name, string SiteIdentifier, string? Description, string? NodeAAddress = null, string? NodeBAddress = null, string? GrpcNodeAAddress = null, string? GrpcNodeBAddress = null);
|
||||
public record UpdateSiteCommand(int SiteId, string Name, string? Description, string? NodeAAddress = null, string? NodeBAddress = null, string? GrpcNodeAAddress = null, string? GrpcNodeBAddress = null);
|
||||
public record DeleteSiteCommand(int SiteId);
|
||||
public record ListAreasCommand(int SiteId);
|
||||
public record CreateAreaCommand(int SiteId, string Name, int? ParentAreaId);
|
||||
|
||||
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
|
||||
@@ -6,4 +6,5 @@ public class NodeOptions
|
||||
public string NodeHostname { get; set; } = string.Empty;
|
||||
public string? SiteId { get; set; }
|
||||
public int RemotingPort { get; set; } = 8081;
|
||||
public int GrpcPort { get; set; } = 8083;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ public static class StartupValidator
|
||||
|
||||
if (role == "Site")
|
||||
{
|
||||
var grpcPortStr = nodeSection["GrpcPort"];
|
||||
if (grpcPortStr != null && (!int.TryParse(grpcPortStr, out var gp) || gp < 1 || gp > 65535))
|
||||
errors.Add("ScadaLink:Node:GrpcPort must be 1-65535");
|
||||
|
||||
var dbSection = configuration.GetSection("ScadaLink:Database");
|
||||
if (string.IsNullOrEmpty(dbSection["SiteDbPath"]))
|
||||
errors.Add("ScadaLink:Database:SiteDbPath required for Site nodes");
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"Role": "Site",
|
||||
"NodeHostname": "localhost",
|
||||
"SiteId": "site-a",
|
||||
"RemotingPort": 8082
|
||||
"RemotingPort": 8082,
|
||||
"GrpcPort": 8083
|
||||
},
|
||||
"Cluster": {
|
||||
"SeedNodes": [
|
||||
|
||||
@@ -596,7 +596,9 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
Description = cmd.Description,
|
||||
NodeAAddress = cmd.NodeAAddress,
|
||||
NodeBAddress = cmd.NodeBAddress
|
||||
NodeBAddress = cmd.NodeBAddress,
|
||||
GrpcNodeAAddress = cmd.GrpcNodeAAddress,
|
||||
GrpcNodeBAddress = cmd.GrpcNodeBAddress
|
||||
};
|
||||
await repo.AddSiteAsync(site);
|
||||
await repo.SaveChangesAsync();
|
||||
@@ -615,6 +617,8 @@ public class ManagementActor : ReceiveActor
|
||||
site.Description = cmd.Description;
|
||||
site.NodeAAddress = cmd.NodeAAddress;
|
||||
site.NodeBAddress = cmd.NodeBAddress;
|
||||
site.GrpcNodeAAddress = cmd.GrpcNodeAAddress;
|
||||
site.GrpcNodeBAddress = cmd.GrpcNodeBAddress;
|
||||
await repo.UpdateSiteAsync(site);
|
||||
await repo.SaveChangesAsync();
|
||||
var commService = sp.GetService<CommunicationService>();
|
||||
|
||||
157
tests/ScadaLink.Communication.Tests/Grpc/ProtoRoundtripTests.cs
Normal file
157
tests/ScadaLink.Communication.Tests/Grpc/ProtoRoundtripTests.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Timestamp_DateTimeOffset_FullRoundTrip()
|
||||
{
|
||||
var original = new DateTimeOffset(2026, 3, 21, 14, 30, 45, 123, TimeSpan.Zero);
|
||||
var update = new AttributeValueUpdate
|
||||
{
|
||||
InstanceUniqueName = "Motor-1",
|
||||
AttributePath = "Speed",
|
||||
AttributeName = "Speed",
|
||||
Value = "42.5",
|
||||
Quality = Quality.Good,
|
||||
Timestamp = Timestamp.FromDateTimeOffset(original)
|
||||
};
|
||||
|
||||
var bytes = update.ToByteArray();
|
||||
var deserialized = AttributeValueUpdate.Parser.ParseFrom(bytes);
|
||||
|
||||
var roundTripped = deserialized.Timestamp.ToDateTimeOffset();
|
||||
Assert.Equal(original.Year, roundTripped.Year);
|
||||
Assert.Equal(original.Month, roundTripped.Month);
|
||||
Assert.Equal(original.Day, roundTripped.Day);
|
||||
Assert.Equal(original.Hour, roundTripped.Hour);
|
||||
Assert.Equal(original.Minute, roundTripped.Minute);
|
||||
Assert.Equal(original.Second, roundTripped.Second);
|
||||
Assert.Equal(original.Millisecond, roundTripped.Millisecond);
|
||||
Assert.Equal(TimeSpan.Zero, roundTripped.Offset);
|
||||
}
|
||||
}
|
||||
@@ -217,6 +217,43 @@ public class StartupValidatorTests
|
||||
Assert.Contains("SeedNodes must have at least 2 entries", ex.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0")]
|
||||
[InlineData("-1")]
|
||||
[InlineData("65536")]
|
||||
[InlineData("abc")]
|
||||
public void Site_InvalidGrpcPort_FailsValidation(string grpcPort)
|
||||
{
|
||||
var values = ValidSiteConfig();
|
||||
values["ScadaLink:Node:GrpcPort"] = grpcPort;
|
||||
var config = BuildConfig(values);
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => StartupValidator.Validate(config));
|
||||
Assert.Contains("GrpcPort must be 1-65535", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Site_ValidGrpcPort_PassesValidation()
|
||||
{
|
||||
var values = ValidSiteConfig();
|
||||
values["ScadaLink:Node:GrpcPort"] = "8083";
|
||||
var config = BuildConfig(values);
|
||||
|
||||
var ex = Record.Exception(() => StartupValidator.Validate(config));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Central_InvalidGrpcPort_NotValidated()
|
||||
{
|
||||
var values = ValidCentralConfig();
|
||||
values["ScadaLink:Node:GrpcPort"] = "0";
|
||||
var config = BuildConfig(values);
|
||||
|
||||
var ex = Record.Exception(() => StartupValidator.Validate(config));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleErrors_AllReported()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user