dd4f1c2aa3
AlarmConditionState.Severity mixed two conventions: OPC UA rows carried Part 9 Severity (higher = more severe) while MxGateway native alarms and computed alarms carried a raw AVEVA-style priority (1 = most urgent). Every consumer ranks higher as worse, so Worst Severity, the severity filter/sort and the site event-log Error threshold picked the LEAST urgent MxGateway/computed alarms. - AlarmPriorityBands: the OtOpcUa GalaxyAlarmPriority table (lmxopcua#887, b61f3e05), value for value: 1-250 -> 1000, 251-500 -> 750, 501-750 -> 500, 751-999 -> 250; <= 0 clamps to Critical, > 999 to Low. - MxGatewayAlarmMapper bands the gateway severity (= AVEVA priority); AlarmConditionStateFactory.ForComputed bands PriorityLevel. OPC UA unchanged. - AlarmConditionState gains trailing Priority (int?): the raw source number, exactly the pre-#129 Severity. AlarmStateChanged.Priority and the wire AlarmStateUpdate.priority keep that meaning (no proto change); central derives Severity by kind (SeverityFromPriority), so an older site decodes correctly. - ScriptAlarm gains trailing Priority. The native MES 900-999 band and the MES API Severity/MinSeverity/MaxSeverity key off Priority; rig seed scripts moved from a.Severity to a.Priority, so MES output is unchanged. - AlarmActor raise logs Error when the banded severity >= 700 (same threshold as NativeAlarmActor): priority 1-500 Error, 501+ Warning (was raw >= 700). - NativeAlarmActor upgrades pre-#129 native_alarm_state rows on rehydrate. - Debug snapshot decode restores Condition.Priority from the row's priority. - Docs: Commons, DCL, SiteRuntime, Communication, CentralUI, InboundAPI, HighLevelReqs, MES alarm-status plan, CLAUDE.md. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
3147 lines
135 KiB
C#
3147 lines
135 KiB
C#
using Google.Protobuf;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Artifacts;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Lifecycle;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
using BoolValue = Google.Protobuf.WellKnownTypes.BoolValue;
|
|
using Duration = Google.Protobuf.WellKnownTypes.Duration;
|
|
using Int32Value = Google.Protobuf.WellKnownTypes.Int32Value;
|
|
using StringValue = Google.Protobuf.WellKnownTypes.StringValue;
|
|
using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
|
|
|
|
/// <summary>
|
|
/// Canonical bridge between the in-process central→site command/reply records
|
|
/// (Commons <c>Messages.*</c>) and the wire messages of
|
|
/// <c>Protos/site_command.proto</c>. Every one of the 28 migrated commands and
|
|
/// all 22 reply shapes pass through here; nothing else may hand-roll the
|
|
/// translation, so the contract has exactly one definition.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Mirrors the sibling <see cref="AuditEventDtoMapper"/>/<see cref="SiteCallDtoMapper"/>
|
|
/// and lives in <c>Communication</c> for the same reason: this project owns the
|
|
/// generated wire types and already references <c>Commons</c>, so both the site
|
|
/// server (T1B.2) and the central transport (T1B.3) can share one implementation
|
|
/// without a project-reference cycle.
|
|
/// </para>
|
|
/// <para><b>Null conventions.</b> proto3 has no presence for scalars, so:</para>
|
|
/// <list type="bullet">
|
|
/// <item>
|
|
/// Nullable <b>strings</b> ride as plain <c>string</c> and the empty string
|
|
/// represents <c>null</c> — the convention <see cref="AuditEventDtoMapper"/>
|
|
/// already established. It is a normalisation, not a loss: every such member
|
|
/// here is an optional error message, token, filter or id where "" and
|
|
/// <c>null</c> carry the same meaning. The one exception is
|
|
/// <see cref="RouteToWaitForAttributeRequest.TargetValueEncoded"/>, where
|
|
/// "wait for the empty string" is a real target distinct from "no target";
|
|
/// that field uses a <c>StringValue</c> wrapper instead.
|
|
/// </item>
|
|
/// <item>
|
|
/// Nullable <b>ints/bools</b> use the <c>Int32Value</c>/<c>BoolValue</c>
|
|
/// wrappers, and nullable <b>enums</b> use a one-field message of the same
|
|
/// shape (proto3 enums have no presence and no stock wrapper exists).
|
|
/// </item>
|
|
/// <item>
|
|
/// Nullable <b>collections</b> are wrapped in a per-collection message, so
|
|
/// the null/empty distinction becomes a message-presence question. proto3
|
|
/// <c>repeated</c>/<c>map</c> alone collapses the two — precisely the silent
|
|
/// data-loss class the transport round-trip guard caught in the transport round-trip tests.
|
|
/// </item>
|
|
/// <item>
|
|
/// <c>Guid?</c> members ride as a <c>string</c> holding the "D" form, with
|
|
/// the empty string representing <c>null</c>.
|
|
/// <see cref="TrackedOperationId"/> (a non-nullable struct over a
|
|
/// <see cref="Guid"/>) rides as the same "D" string.
|
|
/// </item>
|
|
/// </list>
|
|
/// <para>
|
|
/// <b>Time.</b> DTO members declared <see cref="DateTimeOffset"/> use
|
|
/// <c>google.protobuf.Timestamp</c>. That normalises to UTC, which is lossless
|
|
/// under <see cref="DateTimeOffset"/> equality (it compares instants), and every
|
|
/// timestamp in this contract is documented UTC anyway. Members declared
|
|
/// <see cref="DateTime"/> (the certificate validity bounds) are coerced to
|
|
/// <see cref="DateTimeKind.Utc"/> first, matching their <c>…Utc</c> names.
|
|
/// Loosely-typed <c>object?</c> values are the exception and keep their exact
|
|
/// kind/offset — see <see cref="LooseValueCodec"/>.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Enums</b> translate through explicit switches in both directions, never by
|
|
/// ordinal, so reordering a C# enum cannot silently re-map the wire. Every wire
|
|
/// enum reserves 0 for <c>_UNSPECIFIED</c>; decoding an unspecified or unknown
|
|
/// value yields the documented safe default for that enum (never a throw — a
|
|
/// version-skewed peer must not be able to fault a whole command).
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class SiteCommandDtoMapper
|
|
{
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// Group classification — the authoritative 28-command inventory in code.
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Classifies a central→site command onto the RPC group that carries it.
|
|
/// The central transport (T1B.3) uses this to pick both the RPC and the
|
|
/// deadline; the coverage tests use it as the command inventory.
|
|
/// </summary>
|
|
/// <param name="command">The command record to classify.</param>
|
|
/// <returns>The <see cref="SiteCommandGroup"/> whose RPC carries the command.</returns>
|
|
/// <exception cref="ArgumentException">The type is not part of the migrated command set.</exception>
|
|
public static SiteCommandGroup GroupOf(object command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return command switch
|
|
{
|
|
RefreshDeploymentCommand or EnableInstanceCommand or DisableInstanceCommand
|
|
or DeleteInstanceCommand or DeploymentStateQueryRequest or DeployArtifactsCommand
|
|
=> SiteCommandGroup.Lifecycle,
|
|
|
|
BrowseNodeCommand or SearchAddressSpaceCommand or ReadTagValuesCommand
|
|
or VerifyEndpointCommand or TrustServerCertCommand or ListServerCertsCommand
|
|
or RemoveServerCertCommand or WriteTagRequest
|
|
=> SiteCommandGroup.OpcUa,
|
|
|
|
EventLogQueryRequest or DebugSnapshotRequest or SubscribeDebugViewRequest
|
|
or UnsubscribeDebugViewRequest
|
|
=> SiteCommandGroup.Query,
|
|
|
|
ParkedMessageQueryRequest or ParkedMessageRetryRequest or ParkedMessageDiscardRequest
|
|
or RetryParkedOperation or DiscardParkedOperation
|
|
=> SiteCommandGroup.Parked,
|
|
|
|
RouteToCallRequest or RouteToGetAttributesRequest or RouteToSetAttributesRequest
|
|
or RouteToWaitForAttributeRequest
|
|
=> SiteCommandGroup.Route,
|
|
|
|
TriggerSiteFailover => SiteCommandGroup.Failover,
|
|
|
|
_ => throw new ArgumentException(
|
|
$"'{command.GetType().Name}' is not a migrated site command.", nameof(command))
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Classifies a site→central reply onto the RPC group whose reply envelope
|
|
/// carries it — the mirror of <see cref="GroupOf"/>, used by the site
|
|
/// dispatcher to pack whatever a handler answered into the right envelope.
|
|
/// </summary>
|
|
/// <param name="reply">The reply record to classify.</param>
|
|
/// <returns>The <see cref="SiteCommandGroup"/> whose reply envelope carries it.</returns>
|
|
/// <exception cref="ArgumentException">The type is not a migrated reply shape.</exception>
|
|
public static SiteCommandGroup GroupOfReply(object reply)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(reply);
|
|
|
|
return reply switch
|
|
{
|
|
DeploymentStatusResponse or InstanceLifecycleResponse or DeploymentStateQueryResponse
|
|
or ArtifactDeploymentResponse
|
|
=> SiteCommandGroup.Lifecycle,
|
|
|
|
BrowseNodeResult or SearchAddressSpaceResult or ReadTagValuesResult or VerifyEndpointResult
|
|
or CertTrustResult or WriteTagResponse
|
|
=> SiteCommandGroup.OpcUa,
|
|
|
|
EventLogQueryResponse or DebugViewSnapshot or UnsubscribeDebugViewAck
|
|
=> SiteCommandGroup.Query,
|
|
|
|
ParkedMessageQueryResponse or ParkedMessageRetryResponse or ParkedMessageDiscardResponse
|
|
or ParkedOperationActionAck
|
|
=> SiteCommandGroup.Parked,
|
|
|
|
RouteToCallResponse or RouteToGetAttributesResponse or RouteToSetAttributesResponse
|
|
or RouteToWaitForAttributeResponse
|
|
=> SiteCommandGroup.Route,
|
|
|
|
SiteFailoverAck => SiteCommandGroup.Failover,
|
|
|
|
_ => throw new ArgumentException(
|
|
$"'{reply.GetType().Name}' is not a migrated site reply.", nameof(reply))
|
|
};
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// RPC 1 — ExecuteLifecycle: envelopes
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Packs a lifecycle-group command into its request envelope.</summary>
|
|
/// <param name="command">One of the six lifecycle-group commands.</param>
|
|
/// <returns>The populated request envelope.</returns>
|
|
/// <exception cref="ArgumentException">The command does not belong to this group.</exception>
|
|
public static LifecycleRequest ToLifecycleRequest(object command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return command switch
|
|
{
|
|
RefreshDeploymentCommand c => new LifecycleRequest { RefreshDeployment = ToProto(c) },
|
|
EnableInstanceCommand c => new LifecycleRequest { EnableInstance = ToProto(c) },
|
|
DisableInstanceCommand c => new LifecycleRequest { DisableInstance = ToProto(c) },
|
|
DeleteInstanceCommand c => new LifecycleRequest { DeleteInstance = ToProto(c) },
|
|
DeploymentStateQueryRequest c => new LifecycleRequest { DeploymentStateQuery = ToProto(c) },
|
|
DeployArtifactsCommand c => new LifecycleRequest { DeployArtifacts = ToProto(c) },
|
|
_ => throw NotInGroup(command, SiteCommandGroup.Lifecycle)
|
|
};
|
|
}
|
|
|
|
/// <summary>Unpacks a lifecycle-group request envelope back to its command record.</summary>
|
|
/// <param name="request">The request envelope received on the wire.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
|
|
public static object FromLifecycleRequest(LifecycleRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
return request.CommandCase switch
|
|
{
|
|
LifecycleRequest.CommandOneofCase.RefreshDeployment => FromProto(request.RefreshDeployment),
|
|
LifecycleRequest.CommandOneofCase.EnableInstance => FromProto(request.EnableInstance),
|
|
LifecycleRequest.CommandOneofCase.DisableInstance => FromProto(request.DisableInstance),
|
|
LifecycleRequest.CommandOneofCase.DeleteInstance => FromProto(request.DeleteInstance),
|
|
LifecycleRequest.CommandOneofCase.DeploymentStateQuery => FromProto(request.DeploymentStateQuery),
|
|
LifecycleRequest.CommandOneofCase.DeployArtifacts => FromProto(request.DeployArtifacts),
|
|
_ => throw UnknownCase(request.CommandCase, nameof(LifecycleRequest))
|
|
};
|
|
}
|
|
|
|
/// <summary>Packs a lifecycle-group reply into its reply envelope.</summary>
|
|
/// <param name="reply">One of the four lifecycle-group reply records.</param>
|
|
/// <returns>The populated reply envelope.</returns>
|
|
/// <exception cref="ArgumentException">The reply does not belong to this group.</exception>
|
|
public static LifecycleReply ToLifecycleReply(object reply)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(reply);
|
|
|
|
return reply switch
|
|
{
|
|
DeploymentStatusResponse r => new LifecycleReply { DeploymentStatus = ToProto(r) },
|
|
InstanceLifecycleResponse r => new LifecycleReply { InstanceLifecycle = ToProto(r) },
|
|
DeploymentStateQueryResponse r => new LifecycleReply { DeploymentStateQuery = ToProto(r) },
|
|
ArtifactDeploymentResponse r => new LifecycleReply { ArtifactDeployment = ToProto(r) },
|
|
_ => throw NotInGroup(reply, SiteCommandGroup.Lifecycle)
|
|
};
|
|
}
|
|
|
|
/// <summary>Unpacks a lifecycle-group reply envelope back to its reply record.</summary>
|
|
/// <param name="reply">The reply envelope received on the wire.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
|
|
public static object FromLifecycleReply(LifecycleReply reply)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(reply);
|
|
|
|
return reply.ReplyCase switch
|
|
{
|
|
LifecycleReply.ReplyOneofCase.DeploymentStatus => FromProto(reply.DeploymentStatus),
|
|
LifecycleReply.ReplyOneofCase.InstanceLifecycle => FromProto(reply.InstanceLifecycle),
|
|
LifecycleReply.ReplyOneofCase.DeploymentStateQuery => FromProto(reply.DeploymentStateQuery),
|
|
LifecycleReply.ReplyOneofCase.ArtifactDeployment => FromProto(reply.ArtifactDeployment),
|
|
_ => throw UnknownCase(reply.ReplyCase, nameof(LifecycleReply))
|
|
};
|
|
}
|
|
|
|
// ── Lifecycle commands ──
|
|
|
|
/// <summary>Projects a <see cref="RefreshDeploymentCommand"/> onto the wire.</summary>
|
|
/// <param name="command">The command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static RefreshDeploymentCommandDto ToProto(RefreshDeploymentCommand command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return new RefreshDeploymentCommandDto
|
|
{
|
|
DeploymentId = command.DeploymentId,
|
|
InstanceUniqueName = command.InstanceUniqueName,
|
|
RevisionHash = command.RevisionHash,
|
|
DeployedBy = command.DeployedBy,
|
|
Timestamp = Ts(command.Timestamp),
|
|
CentralFetchBaseUrl = command.CentralFetchBaseUrl,
|
|
FetchToken = command.FetchToken
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="RefreshDeploymentCommand"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
public static RefreshDeploymentCommand FromProto(RefreshDeploymentCommandDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new RefreshDeploymentCommand(
|
|
dto.DeploymentId,
|
|
dto.InstanceUniqueName,
|
|
dto.RevisionHash,
|
|
dto.DeployedBy,
|
|
Dto(dto.Timestamp),
|
|
dto.CentralFetchBaseUrl,
|
|
dto.FetchToken);
|
|
}
|
|
|
|
/// <summary>Projects an <see cref="EnableInstanceCommand"/> onto the wire.</summary>
|
|
/// <param name="command">The command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static EnableInstanceCommandDto ToProto(EnableInstanceCommand command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return new EnableInstanceCommandDto
|
|
{
|
|
CommandId = command.CommandId,
|
|
InstanceUniqueName = command.InstanceUniqueName,
|
|
Timestamp = Ts(command.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates an <see cref="EnableInstanceCommand"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
public static EnableInstanceCommand FromProto(EnableInstanceCommandDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new EnableInstanceCommand(dto.CommandId, dto.InstanceUniqueName, Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="DisableInstanceCommand"/> onto the wire.</summary>
|
|
/// <param name="command">The command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static DisableInstanceCommandDto ToProto(DisableInstanceCommand command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return new DisableInstanceCommandDto
|
|
{
|
|
CommandId = command.CommandId,
|
|
InstanceUniqueName = command.InstanceUniqueName,
|
|
Timestamp = Ts(command.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="DisableInstanceCommand"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
public static DisableInstanceCommand FromProto(DisableInstanceCommandDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new DisableInstanceCommand(dto.CommandId, dto.InstanceUniqueName, Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="DeleteInstanceCommand"/> onto the wire.</summary>
|
|
/// <param name="command">The command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static DeleteInstanceCommandDto ToProto(DeleteInstanceCommand command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return new DeleteInstanceCommandDto
|
|
{
|
|
CommandId = command.CommandId,
|
|
InstanceUniqueName = command.InstanceUniqueName,
|
|
Timestamp = Ts(command.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="DeleteInstanceCommand"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
public static DeleteInstanceCommand FromProto(DeleteInstanceCommandDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new DeleteInstanceCommand(dto.CommandId, dto.InstanceUniqueName, Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="DeploymentStateQueryRequest"/> onto the wire.</summary>
|
|
/// <param name="request">The request to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static DeploymentStateQueryRequestDto ToProto(DeploymentStateQueryRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
return new DeploymentStateQueryRequestDto
|
|
{
|
|
CorrelationId = request.CorrelationId,
|
|
InstanceUniqueName = request.InstanceUniqueName,
|
|
Timestamp = Ts(request.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="DeploymentStateQueryRequest"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded request record.</returns>
|
|
public static DeploymentStateQueryRequest FromProto(DeploymentStateQueryRequestDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new DeploymentStateQueryRequest(dto.CorrelationId, dto.InstanceUniqueName, Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="DeployArtifactsCommand"/> onto the wire.</summary>
|
|
/// <remarks>Each artifact collection is nullable; a null collection leaves its wrapper message unset.</remarks>
|
|
/// <param name="command">The command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static DeployArtifactsCommandDto ToProto(DeployArtifactsCommand command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
var dto = new DeployArtifactsCommandDto
|
|
{
|
|
DeploymentId = command.DeploymentId,
|
|
Timestamp = Ts(command.Timestamp)
|
|
};
|
|
|
|
if (command.SharedScripts is not null)
|
|
{
|
|
var list = new SharedScriptArtifactListDto();
|
|
list.Items.AddRange(command.SharedScripts.Select(ToProto));
|
|
dto.SharedScripts = list;
|
|
}
|
|
|
|
if (command.ExternalSystems is not null)
|
|
{
|
|
var list = new ExternalSystemArtifactListDto();
|
|
list.Items.AddRange(command.ExternalSystems.Select(ToProto));
|
|
dto.ExternalSystems = list;
|
|
}
|
|
|
|
if (command.DatabaseConnections is not null)
|
|
{
|
|
var list = new DatabaseConnectionArtifactListDto();
|
|
list.Items.AddRange(command.DatabaseConnections.Select(ToProto));
|
|
dto.DatabaseConnections = list;
|
|
}
|
|
|
|
if (command.NotificationLists is not null)
|
|
{
|
|
var list = new NotificationListArtifactListDto();
|
|
list.Items.AddRange(command.NotificationLists.Select(ToProto));
|
|
dto.NotificationLists = list;
|
|
}
|
|
|
|
if (command.DataConnections is not null)
|
|
{
|
|
var list = new DataConnectionArtifactListDto();
|
|
list.Items.AddRange(command.DataConnections.Select(ToProto));
|
|
dto.DataConnections = list;
|
|
}
|
|
|
|
if (command.SmtpConfigurations is not null)
|
|
{
|
|
var list = new SmtpConfigurationArtifactListDto();
|
|
list.Items.AddRange(command.SmtpConfigurations.Select(ToProto));
|
|
dto.SmtpConfigurations = list;
|
|
}
|
|
|
|
if (command.Secrets is not null)
|
|
{
|
|
var list = new SecretArtifactListDto();
|
|
list.Items.AddRange(command.Secrets.Select(ToProto));
|
|
dto.Secrets = list;
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="DeployArtifactsCommand"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded command record; unset collection wrappers rehydrate as null.</returns>
|
|
public static DeployArtifactsCommand FromProto(DeployArtifactsCommandDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new DeployArtifactsCommand(
|
|
dto.DeploymentId,
|
|
dto.SharedScripts is null ? null : dto.SharedScripts.Items.Select(FromProto).ToList(),
|
|
dto.ExternalSystems is null ? null : dto.ExternalSystems.Items.Select(FromProto).ToList(),
|
|
dto.DatabaseConnections is null ? null : dto.DatabaseConnections.Items.Select(FromProto).ToList(),
|
|
dto.NotificationLists is null ? null : dto.NotificationLists.Items.Select(FromProto).ToList(),
|
|
dto.DataConnections is null ? null : dto.DataConnections.Items.Select(FromProto).ToList(),
|
|
dto.SmtpConfigurations is null ? null : dto.SmtpConfigurations.Items.Select(FromProto).ToList(),
|
|
Dto(dto.Timestamp),
|
|
dto.Secrets is null ? null : dto.Secrets.Items.Select(FromProto).ToList());
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="SecretArtifact"/> onto the wire.</summary>
|
|
/// <param name="artifact">The artifact to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static SecretArtifactDto ToProto(SecretArtifact artifact)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(artifact);
|
|
|
|
return new SecretArtifactDto
|
|
{
|
|
Name = artifact.Name,
|
|
ContentType = artifact.ContentType,
|
|
Ciphertext = ByteString.CopyFrom(artifact.Ciphertext),
|
|
Nonce = ByteString.CopyFrom(artifact.Nonce),
|
|
Tag = ByteString.CopyFrom(artifact.Tag),
|
|
WrappedDek = ByteString.CopyFrom(artifact.WrappedDek),
|
|
WrapNonce = ByteString.CopyFrom(artifact.WrapNonce),
|
|
WrapTag = ByteString.CopyFrom(artifact.WrapTag),
|
|
KekId = artifact.KekId,
|
|
Revision = artifact.Revision,
|
|
CreatedUtc = Ts(artifact.CreatedUtc),
|
|
UpdatedUtc = Ts(artifact.UpdatedUtc),
|
|
Description = artifact.Description
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="SecretArtifact"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded artifact.</returns>
|
|
public static SecretArtifact FromProto(SecretArtifactDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new SecretArtifact(
|
|
dto.Name,
|
|
dto.ContentType,
|
|
dto.Ciphertext.ToByteArray(),
|
|
dto.Nonce.ToByteArray(),
|
|
dto.Tag.ToByteArray(),
|
|
dto.WrappedDek.ToByteArray(),
|
|
dto.WrapNonce.ToByteArray(),
|
|
dto.WrapTag.ToByteArray(),
|
|
dto.KekId,
|
|
dto.Revision,
|
|
Dto(dto.CreatedUtc),
|
|
Dto(dto.UpdatedUtc),
|
|
dto.Description);
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="SharedScriptArtifact"/> onto the wire.</summary>
|
|
/// <param name="artifact">The artifact to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static SharedScriptArtifactDto ToProto(SharedScriptArtifact artifact)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(artifact);
|
|
|
|
return new SharedScriptArtifactDto
|
|
{
|
|
Name = artifact.Name,
|
|
Code = artifact.Code,
|
|
ParameterDefinitions = OrEmpty(artifact.ParameterDefinitions),
|
|
ReturnDefinition = OrEmpty(artifact.ReturnDefinition)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="SharedScriptArtifact"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded artifact.</returns>
|
|
public static SharedScriptArtifact FromProto(SharedScriptArtifactDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new SharedScriptArtifact(
|
|
dto.Name, dto.Code, NullIfEmpty(dto.ParameterDefinitions), NullIfEmpty(dto.ReturnDefinition));
|
|
}
|
|
|
|
/// <summary>Projects an <see cref="ExternalSystemArtifact"/> onto the wire.</summary>
|
|
/// <param name="artifact">The artifact to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ExternalSystemArtifactDto ToProto(ExternalSystemArtifact artifact)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(artifact);
|
|
|
|
return new ExternalSystemArtifactDto
|
|
{
|
|
Name = artifact.Name,
|
|
EndpointUrl = artifact.EndpointUrl,
|
|
AuthType = artifact.AuthType,
|
|
AuthConfiguration = OrEmpty(artifact.AuthConfiguration),
|
|
MethodDefinitionsJson = OrEmpty(artifact.MethodDefinitionsJson),
|
|
TimeoutSeconds = artifact.TimeoutSeconds,
|
|
MaxRetries = artifact.MaxRetries,
|
|
RetryDelay = Duration.FromTimeSpan(artifact.RetryDelay)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates an <see cref="ExternalSystemArtifact"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded artifact.</returns>
|
|
public static ExternalSystemArtifact FromProto(ExternalSystemArtifactDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ExternalSystemArtifact(
|
|
dto.Name,
|
|
dto.EndpointUrl,
|
|
dto.AuthType,
|
|
NullIfEmpty(dto.AuthConfiguration),
|
|
NullIfEmpty(dto.MethodDefinitionsJson),
|
|
dto.TimeoutSeconds,
|
|
dto.MaxRetries,
|
|
dto.RetryDelay?.ToTimeSpan() ?? TimeSpan.Zero);
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="DatabaseConnectionArtifact"/> onto the wire.</summary>
|
|
/// <param name="artifact">The artifact to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static DatabaseConnectionArtifactDto ToProto(DatabaseConnectionArtifact artifact)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(artifact);
|
|
|
|
return new DatabaseConnectionArtifactDto
|
|
{
|
|
Name = artifact.Name,
|
|
ConnectionString = artifact.ConnectionString,
|
|
MaxRetries = artifact.MaxRetries,
|
|
RetryDelay = Duration.FromTimeSpan(artifact.RetryDelay)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="DatabaseConnectionArtifact"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded artifact.</returns>
|
|
public static DatabaseConnectionArtifact FromProto(DatabaseConnectionArtifactDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new DatabaseConnectionArtifact(
|
|
dto.Name, dto.ConnectionString, dto.MaxRetries, dto.RetryDelay?.ToTimeSpan() ?? TimeSpan.Zero);
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="NotificationListArtifact"/> onto the wire.</summary>
|
|
/// <param name="artifact">The artifact to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static NotificationListArtifactDto ToProto(NotificationListArtifact artifact)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(artifact);
|
|
|
|
var dto = new NotificationListArtifactDto { Name = artifact.Name };
|
|
dto.RecipientEmails.AddRange(artifact.RecipientEmails);
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="NotificationListArtifact"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded artifact.</returns>
|
|
public static NotificationListArtifact FromProto(NotificationListArtifactDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new NotificationListArtifact(dto.Name, dto.RecipientEmails.ToList());
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="DataConnectionArtifact"/> onto the wire.</summary>
|
|
/// <param name="artifact">The artifact to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static DataConnectionArtifactDto ToProto(DataConnectionArtifact artifact)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(artifact);
|
|
|
|
return new DataConnectionArtifactDto
|
|
{
|
|
Name = artifact.Name,
|
|
Protocol = artifact.Protocol,
|
|
PrimaryConfigurationJson = OrEmpty(artifact.PrimaryConfigurationJson),
|
|
BackupConfigurationJson = OrEmpty(artifact.BackupConfigurationJson),
|
|
FailoverRetryCount = artifact.FailoverRetryCount
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="DataConnectionArtifact"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded artifact.</returns>
|
|
public static DataConnectionArtifact FromProto(DataConnectionArtifactDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new DataConnectionArtifact(
|
|
dto.Name,
|
|
dto.Protocol,
|
|
NullIfEmpty(dto.PrimaryConfigurationJson),
|
|
NullIfEmpty(dto.BackupConfigurationJson),
|
|
dto.FailoverRetryCount);
|
|
}
|
|
|
|
/// <summary>Projects an <see cref="SmtpConfigurationArtifact"/> onto the wire.</summary>
|
|
/// <param name="artifact">The artifact to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static SmtpConfigurationArtifactDto ToProto(SmtpConfigurationArtifact artifact)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(artifact);
|
|
|
|
return new SmtpConfigurationArtifactDto
|
|
{
|
|
Name = artifact.Name,
|
|
Server = artifact.Server,
|
|
Port = artifact.Port,
|
|
AuthMode = artifact.AuthMode,
|
|
FromAddress = artifact.FromAddress,
|
|
Username = OrEmpty(artifact.Username),
|
|
Password = OrEmpty(artifact.Password),
|
|
OauthConfig = OrEmpty(artifact.OAuthConfig)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates an <see cref="SmtpConfigurationArtifact"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded artifact.</returns>
|
|
public static SmtpConfigurationArtifact FromProto(SmtpConfigurationArtifactDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new SmtpConfigurationArtifact(
|
|
dto.Name,
|
|
dto.Server,
|
|
dto.Port,
|
|
dto.AuthMode,
|
|
dto.FromAddress,
|
|
NullIfEmpty(dto.Username),
|
|
NullIfEmpty(dto.Password),
|
|
NullIfEmpty(dto.OauthConfig));
|
|
}
|
|
|
|
// ── Lifecycle replies ──
|
|
|
|
/// <summary>Projects a <see cref="DeploymentStatusResponse"/> onto the wire.</summary>
|
|
/// <param name="response">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static DeploymentStatusResponseDto ToProto(DeploymentStatusResponse response)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(response);
|
|
|
|
return new DeploymentStatusResponseDto
|
|
{
|
|
DeploymentId = response.DeploymentId,
|
|
InstanceUniqueName = response.InstanceUniqueName,
|
|
Status = ToProto(response.Status),
|
|
ErrorMessage = OrEmpty(response.ErrorMessage),
|
|
Timestamp = Ts(response.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="DeploymentStatusResponse"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static DeploymentStatusResponse FromProto(DeploymentStatusResponseDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new DeploymentStatusResponse(
|
|
dto.DeploymentId,
|
|
dto.InstanceUniqueName,
|
|
FromProto(dto.Status),
|
|
NullIfEmpty(dto.ErrorMessage),
|
|
Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects an <see cref="InstanceLifecycleResponse"/> onto the wire.</summary>
|
|
/// <param name="response">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static InstanceLifecycleResponseDto ToProto(InstanceLifecycleResponse response)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(response);
|
|
|
|
return new InstanceLifecycleResponseDto
|
|
{
|
|
CommandId = response.CommandId,
|
|
InstanceUniqueName = response.InstanceUniqueName,
|
|
Success = response.Success,
|
|
ErrorMessage = OrEmpty(response.ErrorMessage),
|
|
Timestamp = Ts(response.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates an <see cref="InstanceLifecycleResponse"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static InstanceLifecycleResponse FromProto(InstanceLifecycleResponseDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new InstanceLifecycleResponse(
|
|
dto.CommandId,
|
|
dto.InstanceUniqueName,
|
|
dto.Success,
|
|
NullIfEmpty(dto.ErrorMessage),
|
|
Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="DeploymentStateQueryResponse"/> onto the wire.</summary>
|
|
/// <param name="response">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static DeploymentStateQueryResponseDto ToProto(DeploymentStateQueryResponse response)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(response);
|
|
|
|
return new DeploymentStateQueryResponseDto
|
|
{
|
|
CorrelationId = response.CorrelationId,
|
|
InstanceUniqueName = response.InstanceUniqueName,
|
|
IsDeployed = response.IsDeployed,
|
|
AppliedDeploymentId = OrEmpty(response.AppliedDeploymentId),
|
|
AppliedRevisionHash = OrEmpty(response.AppliedRevisionHash),
|
|
Timestamp = Ts(response.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="DeploymentStateQueryResponse"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static DeploymentStateQueryResponse FromProto(DeploymentStateQueryResponseDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new DeploymentStateQueryResponse(
|
|
dto.CorrelationId,
|
|
dto.InstanceUniqueName,
|
|
dto.IsDeployed,
|
|
NullIfEmpty(dto.AppliedDeploymentId),
|
|
NullIfEmpty(dto.AppliedRevisionHash),
|
|
Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects an <see cref="ArtifactDeploymentResponse"/> onto the wire.</summary>
|
|
/// <param name="response">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ArtifactDeploymentResponseDto ToProto(ArtifactDeploymentResponse response)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(response);
|
|
|
|
var dto = new ArtifactDeploymentResponseDto
|
|
{
|
|
DeploymentId = response.DeploymentId,
|
|
SiteId = response.SiteId,
|
|
Success = response.Success,
|
|
ErrorMessage = OrEmpty(response.ErrorMessage),
|
|
Timestamp = Ts(response.Timestamp)
|
|
};
|
|
|
|
if (response.SecretsApplied.HasValue)
|
|
{
|
|
dto.SecretsApplied = response.SecretsApplied.Value;
|
|
}
|
|
|
|
if (response.SecretsRemoved is { Count: > 0 })
|
|
{
|
|
dto.SecretsRemoved.AddRange(response.SecretsRemoved);
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates an <see cref="ArtifactDeploymentResponse"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static ArtifactDeploymentResponse FromProto(ArtifactDeploymentResponseDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ArtifactDeploymentResponse(
|
|
dto.DeploymentId, dto.SiteId, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp),
|
|
dto.SecretsApplied,
|
|
dto.SecretsRemoved.Count > 0 ? dto.SecretsRemoved.ToList() : null);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// RPC 2 — ExecuteOpcUa: envelopes
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Packs an OPC UA-group command into its request envelope.</summary>
|
|
/// <param name="command">One of the eight OPC UA-group commands.</param>
|
|
/// <returns>The populated request envelope.</returns>
|
|
/// <exception cref="ArgumentException">The command does not belong to this group.</exception>
|
|
public static OpcUaRequest ToOpcUaRequest(object command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return command switch
|
|
{
|
|
BrowseNodeCommand c => new OpcUaRequest { BrowseNode = ToProto(c) },
|
|
SearchAddressSpaceCommand c => new OpcUaRequest { SearchAddressSpace = ToProto(c) },
|
|
ReadTagValuesCommand c => new OpcUaRequest { ReadTagValues = ToProto(c) },
|
|
VerifyEndpointCommand c => new OpcUaRequest { VerifyEndpoint = ToProto(c) },
|
|
TrustServerCertCommand c => new OpcUaRequest { TrustServerCert = ToProto(c) },
|
|
ListServerCertsCommand c => new OpcUaRequest { ListServerCerts = ToProto(c) },
|
|
RemoveServerCertCommand c => new OpcUaRequest { RemoveServerCert = ToProto(c) },
|
|
WriteTagRequest c => new OpcUaRequest { WriteTag = ToProto(c) },
|
|
_ => throw NotInGroup(command, SiteCommandGroup.OpcUa)
|
|
};
|
|
}
|
|
|
|
/// <summary>Unpacks an OPC UA-group request envelope back to its command record.</summary>
|
|
/// <param name="request">The request envelope received on the wire.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
|
|
public static object FromOpcUaRequest(OpcUaRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
return request.CommandCase switch
|
|
{
|
|
OpcUaRequest.CommandOneofCase.BrowseNode => FromProto(request.BrowseNode),
|
|
OpcUaRequest.CommandOneofCase.SearchAddressSpace => FromProto(request.SearchAddressSpace),
|
|
OpcUaRequest.CommandOneofCase.ReadTagValues => FromProto(request.ReadTagValues),
|
|
OpcUaRequest.CommandOneofCase.VerifyEndpoint => FromProto(request.VerifyEndpoint),
|
|
OpcUaRequest.CommandOneofCase.TrustServerCert => FromProto(request.TrustServerCert),
|
|
OpcUaRequest.CommandOneofCase.ListServerCerts => FromProto(request.ListServerCerts),
|
|
OpcUaRequest.CommandOneofCase.RemoveServerCert => FromProto(request.RemoveServerCert),
|
|
OpcUaRequest.CommandOneofCase.WriteTag => FromProto(request.WriteTag),
|
|
_ => throw UnknownCase(request.CommandCase, nameof(OpcUaRequest))
|
|
};
|
|
}
|
|
|
|
/// <summary>Packs an OPC UA-group reply into its reply envelope.</summary>
|
|
/// <param name="reply">One of the six OPC UA-group reply records.</param>
|
|
/// <returns>The populated reply envelope.</returns>
|
|
/// <exception cref="ArgumentException">The reply does not belong to this group.</exception>
|
|
public static OpcUaReply ToOpcUaReply(object reply)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(reply);
|
|
|
|
return reply switch
|
|
{
|
|
BrowseNodeResult r => new OpcUaReply { BrowseNode = ToProto(r) },
|
|
SearchAddressSpaceResult r => new OpcUaReply { SearchAddressSpace = ToProto(r) },
|
|
ReadTagValuesResult r => new OpcUaReply { ReadTagValues = ToProto(r) },
|
|
VerifyEndpointResult r => new OpcUaReply { VerifyEndpoint = ToProto(r) },
|
|
CertTrustResult r => new OpcUaReply { CertTrust = ToProto(r) },
|
|
WriteTagResponse r => new OpcUaReply { WriteTag = ToProto(r) },
|
|
_ => throw NotInGroup(reply, SiteCommandGroup.OpcUa)
|
|
};
|
|
}
|
|
|
|
/// <summary>Unpacks an OPC UA-group reply envelope back to its reply record.</summary>
|
|
/// <param name="reply">The reply envelope received on the wire.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
|
|
public static object FromOpcUaReply(OpcUaReply reply)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(reply);
|
|
|
|
return reply.ReplyCase switch
|
|
{
|
|
OpcUaReply.ReplyOneofCase.BrowseNode => FromProto(reply.BrowseNode),
|
|
OpcUaReply.ReplyOneofCase.SearchAddressSpace => FromProto(reply.SearchAddressSpace),
|
|
OpcUaReply.ReplyOneofCase.ReadTagValues => FromProto(reply.ReadTagValues),
|
|
OpcUaReply.ReplyOneofCase.VerifyEndpoint => FromProto(reply.VerifyEndpoint),
|
|
OpcUaReply.ReplyOneofCase.CertTrust => FromProto(reply.CertTrust),
|
|
OpcUaReply.ReplyOneofCase.WriteTag => FromProto(reply.WriteTag),
|
|
_ => throw UnknownCase(reply.ReplyCase, nameof(OpcUaReply))
|
|
};
|
|
}
|
|
|
|
// ── OPC UA commands ──
|
|
|
|
/// <summary>Projects a <see cref="BrowseNodeCommand"/> onto the wire.</summary>
|
|
/// <param name="command">The command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static BrowseNodeCommandDto ToProto(BrowseNodeCommand command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return new BrowseNodeCommandDto
|
|
{
|
|
ConnectionName = command.ConnectionName,
|
|
ParentNodeId = OrEmpty(command.ParentNodeId),
|
|
ContinuationToken = OrEmpty(command.ContinuationToken),
|
|
SiteIdentifier = OrEmpty(command.SiteIdentifier)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="BrowseNodeCommand"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
public static BrowseNodeCommand FromProto(BrowseNodeCommandDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new BrowseNodeCommand(
|
|
dto.ConnectionName,
|
|
NullIfEmpty(dto.ParentNodeId),
|
|
NullIfEmpty(dto.ContinuationToken),
|
|
NullIfEmpty(dto.SiteIdentifier));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="SearchAddressSpaceCommand"/> onto the wire.</summary>
|
|
/// <param name="command">The command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static SearchAddressSpaceCommandDto ToProto(SearchAddressSpaceCommand command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return new SearchAddressSpaceCommandDto
|
|
{
|
|
ConnectionName = command.ConnectionName,
|
|
Query = command.Query,
|
|
MaxDepth = command.MaxDepth,
|
|
MaxResults = command.MaxResults,
|
|
SiteIdentifier = OrEmpty(command.SiteIdentifier)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="SearchAddressSpaceCommand"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
public static SearchAddressSpaceCommand FromProto(SearchAddressSpaceCommandDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new SearchAddressSpaceCommand(
|
|
dto.ConnectionName, dto.Query, dto.MaxDepth, dto.MaxResults, NullIfEmpty(dto.SiteIdentifier));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="ReadTagValuesCommand"/> onto the wire.</summary>
|
|
/// <param name="command">The command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ReadTagValuesCommandDto ToProto(ReadTagValuesCommand command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
var dto = new ReadTagValuesCommandDto { ConnectionName = command.ConnectionName };
|
|
dto.TagPaths.AddRange(command.TagPaths);
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="ReadTagValuesCommand"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
public static ReadTagValuesCommand FromProto(ReadTagValuesCommandDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ReadTagValuesCommand(dto.ConnectionName, dto.TagPaths.ToList());
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="VerifyEndpointCommand"/> onto the wire.</summary>
|
|
/// <param name="command">The command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static VerifyEndpointCommandDto ToProto(VerifyEndpointCommand command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return new VerifyEndpointCommandDto
|
|
{
|
|
ConnectionName = command.ConnectionName,
|
|
Protocol = command.Protocol,
|
|
ConfigJson = command.ConfigJson,
|
|
SiteIdentifier = OrEmpty(command.SiteIdentifier)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="VerifyEndpointCommand"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
public static VerifyEndpointCommand FromProto(VerifyEndpointCommandDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new VerifyEndpointCommand(
|
|
dto.ConnectionName, dto.Protocol, dto.ConfigJson, NullIfEmpty(dto.SiteIdentifier));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="TrustServerCertCommand"/> onto the wire.</summary>
|
|
/// <param name="command">The command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static TrustServerCertCommandDto ToProto(TrustServerCertCommand command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return new TrustServerCertCommandDto
|
|
{
|
|
ConnectionName = command.ConnectionName,
|
|
DerBase64 = command.DerBase64,
|
|
Thumbprint = command.Thumbprint,
|
|
SiteIdentifier = OrEmpty(command.SiteIdentifier)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="TrustServerCertCommand"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
public static TrustServerCertCommand FromProto(TrustServerCertCommandDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new TrustServerCertCommand(
|
|
dto.ConnectionName, dto.DerBase64, dto.Thumbprint, NullIfEmpty(dto.SiteIdentifier));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="ListServerCertsCommand"/> onto the wire.</summary>
|
|
/// <param name="command">The command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ListServerCertsCommandDto ToProto(ListServerCertsCommand command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return new ListServerCertsCommandDto { SiteIdentifier = OrEmpty(command.SiteIdentifier) };
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="ListServerCertsCommand"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
public static ListServerCertsCommand FromProto(ListServerCertsCommandDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ListServerCertsCommand(NullIfEmpty(dto.SiteIdentifier));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="RemoveServerCertCommand"/> onto the wire.</summary>
|
|
/// <param name="command">The command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static RemoveServerCertCommandDto ToProto(RemoveServerCertCommand command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return new RemoveServerCertCommandDto
|
|
{
|
|
Thumbprint = command.Thumbprint,
|
|
SiteIdentifier = OrEmpty(command.SiteIdentifier)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="RemoveServerCertCommand"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
public static RemoveServerCertCommand FromProto(RemoveServerCertCommandDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new RemoveServerCertCommand(dto.Thumbprint, NullIfEmpty(dto.SiteIdentifier));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="WriteTagRequest"/> onto the wire.</summary>
|
|
/// <param name="request">The request to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static WriteTagRequestDto ToProto(WriteTagRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
return new WriteTagRequestDto
|
|
{
|
|
CorrelationId = request.CorrelationId,
|
|
ConnectionName = request.ConnectionName,
|
|
TagPath = request.TagPath,
|
|
Value = LooseValueCodec.ToProtoOrNull(request.Value),
|
|
Timestamp = Ts(request.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="WriteTagRequest"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded request record.</returns>
|
|
public static WriteTagRequest FromProto(WriteTagRequestDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new WriteTagRequest(
|
|
dto.CorrelationId,
|
|
dto.ConnectionName,
|
|
dto.TagPath,
|
|
LooseValueCodec.FromProto(dto.Value),
|
|
Dto(dto.Timestamp));
|
|
}
|
|
|
|
// ── OPC UA replies ──
|
|
|
|
/// <summary>Projects a <see cref="BrowseNodeResult"/> onto the wire.</summary>
|
|
/// <param name="result">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static BrowseNodeResultDto ToProto(BrowseNodeResult result)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(result);
|
|
|
|
var dto = new BrowseNodeResultDto
|
|
{
|
|
Truncated = result.Truncated,
|
|
ContinuationToken = OrEmpty(result.ContinuationToken)
|
|
};
|
|
dto.Children.AddRange(result.Children.Select(ToProto));
|
|
if (result.Failure is not null)
|
|
{
|
|
dto.Failure = ToProto(result.Failure);
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="BrowseNodeResult"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static BrowseNodeResult FromProto(BrowseNodeResultDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new BrowseNodeResult(
|
|
dto.Children.Select(FromProto).ToList(),
|
|
dto.Truncated,
|
|
dto.Failure is null ? null : FromProto(dto.Failure),
|
|
NullIfEmpty(dto.ContinuationToken));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="BrowseNode"/> onto the wire.</summary>
|
|
/// <param name="node">The address-space node to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static BrowseNodeDto ToProto(BrowseNode node)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(node);
|
|
|
|
var dto = new BrowseNodeDto
|
|
{
|
|
NodeId = node.NodeId,
|
|
DisplayName = node.DisplayName,
|
|
NodeClass = ToProto(node.NodeClass),
|
|
HasChildren = node.HasChildren,
|
|
DataType = OrEmpty(node.DataType)
|
|
};
|
|
|
|
if (node.ValueRank.HasValue)
|
|
{
|
|
dto.ValueRank = node.ValueRank.Value;
|
|
}
|
|
|
|
if (node.Writable.HasValue)
|
|
{
|
|
dto.Writable = node.Writable.Value;
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="BrowseNode"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded address-space node.</returns>
|
|
public static BrowseNode FromProto(BrowseNodeDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new BrowseNode(
|
|
dto.NodeId,
|
|
dto.DisplayName,
|
|
FromProto(dto.NodeClass),
|
|
dto.HasChildren,
|
|
NullIfEmpty(dto.DataType),
|
|
dto.ValueRank,
|
|
dto.Writable);
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="BrowseFailure"/> onto the wire.</summary>
|
|
/// <param name="failure">The structured browse failure to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static BrowseFailureDto ToProto(BrowseFailure failure)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(failure);
|
|
|
|
return new BrowseFailureDto { Kind = ToProto(failure.Kind), Message = failure.Message };
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="BrowseFailure"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded structured browse failure.</returns>
|
|
public static BrowseFailure FromProto(BrowseFailureDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new BrowseFailure(FromProto(dto.Kind), dto.Message);
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="SearchAddressSpaceResult"/> onto the wire.</summary>
|
|
/// <param name="result">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static SearchAddressSpaceResultDto ToProto(SearchAddressSpaceResult result)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(result);
|
|
|
|
var dto = new SearchAddressSpaceResultDto { CapReached = result.CapReached };
|
|
dto.Matches.AddRange(result.Matches.Select(ToProto));
|
|
if (result.Failure is not null)
|
|
{
|
|
dto.Failure = ToProto(result.Failure);
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="SearchAddressSpaceResult"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static SearchAddressSpaceResult FromProto(SearchAddressSpaceResultDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new SearchAddressSpaceResult(
|
|
dto.Matches.Select(FromProto).ToList(),
|
|
dto.CapReached,
|
|
dto.Failure is null ? null : FromProto(dto.Failure));
|
|
}
|
|
|
|
/// <summary>Projects an <see cref="AddressSpaceMatch"/> onto the wire.</summary>
|
|
/// <param name="match">The search match to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static AddressSpaceMatchDto ToProto(AddressSpaceMatch match)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(match);
|
|
|
|
return new AddressSpaceMatchDto { Node = ToProto(match.Node), Path = match.Path };
|
|
}
|
|
|
|
/// <summary>Rehydrates an <see cref="AddressSpaceMatch"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded search match.</returns>
|
|
public static AddressSpaceMatch FromProto(AddressSpaceMatchDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new AddressSpaceMatch(FromProto(dto.Node), dto.Path);
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="ReadTagValuesResult"/> onto the wire.</summary>
|
|
/// <param name="result">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ReadTagValuesResultDto ToProto(ReadTagValuesResult result)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(result);
|
|
|
|
var dto = new ReadTagValuesResultDto();
|
|
dto.Outcomes.AddRange(result.Outcomes.Select(ToProto));
|
|
if (result.Failure is not null)
|
|
{
|
|
dto.Failure = ToProto(result.Failure);
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="ReadTagValuesResult"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static ReadTagValuesResult FromProto(ReadTagValuesResultDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ReadTagValuesResult(
|
|
dto.Outcomes.Select(FromProto).ToList(),
|
|
dto.Failure is null ? null : FromProto(dto.Failure));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="TagReadOutcome"/> onto the wire.</summary>
|
|
/// <param name="outcome">The per-tag read outcome to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static TagReadOutcomeDto ToProto(TagReadOutcome outcome)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(outcome);
|
|
|
|
return new TagReadOutcomeDto
|
|
{
|
|
TagPath = outcome.TagPath,
|
|
Success = outcome.Success,
|
|
Value = LooseValueCodec.ToProtoOrNull(outcome.Value),
|
|
Quality = outcome.Quality,
|
|
Timestamp = Ts(outcome.Timestamp),
|
|
ErrorMessage = OrEmpty(outcome.ErrorMessage)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="TagReadOutcome"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded per-tag read outcome.</returns>
|
|
public static TagReadOutcome FromProto(TagReadOutcomeDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new TagReadOutcome(
|
|
dto.TagPath,
|
|
dto.Success,
|
|
LooseValueCodec.FromProto(dto.Value),
|
|
dto.Quality,
|
|
Dto(dto.Timestamp),
|
|
NullIfEmpty(dto.ErrorMessage));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="ReadTagValuesFailure"/> onto the wire.</summary>
|
|
/// <param name="failure">The connection-level read failure to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ReadTagValuesFailureDto ToProto(ReadTagValuesFailure failure)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(failure);
|
|
|
|
return new ReadTagValuesFailureDto { Kind = ToProto(failure.Kind), Message = failure.Message };
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="ReadTagValuesFailure"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded connection-level read failure.</returns>
|
|
public static ReadTagValuesFailure FromProto(ReadTagValuesFailureDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ReadTagValuesFailure(FromProto(dto.Kind), dto.Message);
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="VerifyEndpointResult"/> onto the wire.</summary>
|
|
/// <param name="result">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static VerifyEndpointResultDto ToProto(VerifyEndpointResult result)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(result);
|
|
|
|
var dto = new VerifyEndpointResultDto
|
|
{
|
|
Success = result.Success,
|
|
Error = OrEmpty(result.Error)
|
|
};
|
|
|
|
if (result.FailureKind.HasValue)
|
|
{
|
|
dto.FailureKind = new VerifyFailureKindValue { Value = ToProto(result.FailureKind.Value) };
|
|
}
|
|
|
|
if (result.Cert is not null)
|
|
{
|
|
dto.Cert = ToProto(result.Cert);
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="VerifyEndpointResult"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static VerifyEndpointResult FromProto(VerifyEndpointResultDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new VerifyEndpointResult(
|
|
dto.Success,
|
|
dto.FailureKind is null ? null : FromProto(dto.FailureKind.Value),
|
|
NullIfEmpty(dto.Error),
|
|
dto.Cert is null ? null : FromProto(dto.Cert));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="ServerCertInfo"/> onto the wire.</summary>
|
|
/// <param name="cert">The captured server certificate to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ServerCertInfoDto ToProto(ServerCertInfo cert)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(cert);
|
|
|
|
return new ServerCertInfoDto
|
|
{
|
|
Thumbprint = cert.Thumbprint,
|
|
Subject = cert.Subject,
|
|
Issuer = cert.Issuer,
|
|
NotBeforeUtc = TsUtc(cert.NotBeforeUtc),
|
|
NotAfterUtc = TsUtc(cert.NotAfterUtc),
|
|
DerBase64 = cert.DerBase64
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="ServerCertInfo"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded captured server certificate.</returns>
|
|
public static ServerCertInfo FromProto(ServerCertInfoDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ServerCertInfo(
|
|
dto.Thumbprint,
|
|
dto.Subject,
|
|
dto.Issuer,
|
|
DtUtc(dto.NotBeforeUtc),
|
|
DtUtc(dto.NotAfterUtc),
|
|
dto.DerBase64);
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="CertTrustResult"/> onto the wire.</summary>
|
|
/// <remarks>A null <see cref="CertTrustResult.Certs"/> (trust/remove) leaves the list wrapper unset.</remarks>
|
|
/// <param name="result">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static CertTrustResultDto ToProto(CertTrustResult result)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(result);
|
|
|
|
var dto = new CertTrustResultDto { Success = result.Success, Error = OrEmpty(result.Error) };
|
|
if (result.Certs is not null)
|
|
{
|
|
var list = new TrustedCertInfoListDto();
|
|
list.Items.AddRange(result.Certs.Select(ToProto));
|
|
dto.Certs = list;
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="CertTrustResult"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static CertTrustResult FromProto(CertTrustResultDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new CertTrustResult(
|
|
dto.Success,
|
|
NullIfEmpty(dto.Error),
|
|
dto.Certs is null ? null : dto.Certs.Items.Select(FromProto).ToList());
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="TrustedCertInfo"/> onto the wire.</summary>
|
|
/// <param name="cert">The PKI store entry to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static TrustedCertInfoDto ToProto(TrustedCertInfo cert)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(cert);
|
|
|
|
return new TrustedCertInfoDto
|
|
{
|
|
Thumbprint = cert.Thumbprint,
|
|
Subject = cert.Subject,
|
|
Issuer = cert.Issuer,
|
|
NotBeforeUtc = TsUtc(cert.NotBeforeUtc),
|
|
NotAfterUtc = TsUtc(cert.NotAfterUtc),
|
|
Rejected = cert.Rejected
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="TrustedCertInfo"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded PKI store entry.</returns>
|
|
public static TrustedCertInfo FromProto(TrustedCertInfoDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new TrustedCertInfo(
|
|
dto.Thumbprint,
|
|
dto.Subject,
|
|
dto.Issuer,
|
|
DtUtc(dto.NotBeforeUtc),
|
|
DtUtc(dto.NotAfterUtc),
|
|
dto.Rejected);
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="WriteTagResponse"/> onto the wire.</summary>
|
|
/// <param name="response">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static WriteTagResponseDto ToProto(WriteTagResponse response)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(response);
|
|
|
|
return new WriteTagResponseDto
|
|
{
|
|
CorrelationId = response.CorrelationId,
|
|
Success = response.Success,
|
|
ErrorMessage = OrEmpty(response.ErrorMessage),
|
|
Timestamp = Ts(response.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="WriteTagResponse"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static WriteTagResponse FromProto(WriteTagResponseDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new WriteTagResponse(
|
|
dto.CorrelationId, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp));
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// RPC 3 — ExecuteQuery: envelopes
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Packs a query-group command into its request envelope.</summary>
|
|
/// <param name="command">One of the four query-group commands.</param>
|
|
/// <returns>The populated request envelope.</returns>
|
|
/// <exception cref="ArgumentException">The command does not belong to this group.</exception>
|
|
public static QueryRequest ToQueryRequest(object command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return command switch
|
|
{
|
|
EventLogQueryRequest c => new QueryRequest { EventLogQuery = ToProto(c) },
|
|
DebugSnapshotRequest c => new QueryRequest { DebugSnapshot = ToProto(c) },
|
|
SubscribeDebugViewRequest c => new QueryRequest { SubscribeDebugView = ToProto(c) },
|
|
UnsubscribeDebugViewRequest c => new QueryRequest { UnsubscribeDebugView = ToProto(c) },
|
|
_ => throw NotInGroup(command, SiteCommandGroup.Query)
|
|
};
|
|
}
|
|
|
|
/// <summary>Unpacks a query-group request envelope back to its command record.</summary>
|
|
/// <param name="request">The request envelope received on the wire.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
|
|
public static object FromQueryRequest(QueryRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
return request.CommandCase switch
|
|
{
|
|
QueryRequest.CommandOneofCase.EventLogQuery => FromProto(request.EventLogQuery),
|
|
QueryRequest.CommandOneofCase.DebugSnapshot => FromProto(request.DebugSnapshot),
|
|
QueryRequest.CommandOneofCase.SubscribeDebugView => FromProto(request.SubscribeDebugView),
|
|
QueryRequest.CommandOneofCase.UnsubscribeDebugView => FromProto(request.UnsubscribeDebugView),
|
|
_ => throw UnknownCase(request.CommandCase, nameof(QueryRequest))
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Packs a query-group reply into its reply envelope. Pass
|
|
/// <see cref="UnsubscribeDebugViewAck"/> for the unsubscribe path, which is a
|
|
/// Tell today and carries no payload.
|
|
/// </summary>
|
|
/// <param name="reply">One of the query-group reply records.</param>
|
|
/// <returns>The populated reply envelope.</returns>
|
|
/// <exception cref="ArgumentException">The reply does not belong to this group.</exception>
|
|
public static QueryReply ToQueryReply(object reply)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(reply);
|
|
|
|
return reply switch
|
|
{
|
|
EventLogQueryResponse r => new QueryReply { EventLogQuery = ToProto(r) },
|
|
DebugViewSnapshot r => new QueryReply { DebugViewSnapshot = ToProto(r) },
|
|
UnsubscribeDebugViewAck => new QueryReply { UnsubscribeDebugView = new UnsubscribeDebugViewAckDto() },
|
|
_ => throw NotInGroup(reply, SiteCommandGroup.Query)
|
|
};
|
|
}
|
|
|
|
/// <summary>Unpacks a query-group reply envelope back to its reply record.</summary>
|
|
/// <param name="reply">The reply envelope received on the wire.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
|
|
public static object FromQueryReply(QueryReply reply)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(reply);
|
|
|
|
return reply.ReplyCase switch
|
|
{
|
|
QueryReply.ReplyOneofCase.EventLogQuery => FromProto(reply.EventLogQuery),
|
|
QueryReply.ReplyOneofCase.DebugViewSnapshot => FromProto(reply.DebugViewSnapshot),
|
|
QueryReply.ReplyOneofCase.UnsubscribeDebugView => UnsubscribeDebugViewAck.Instance,
|
|
_ => throw UnknownCase(reply.ReplyCase, nameof(QueryReply))
|
|
};
|
|
}
|
|
|
|
// ── Query commands ──
|
|
|
|
/// <summary>Projects an <see cref="EventLogQueryRequest"/> onto the wire.</summary>
|
|
/// <param name="request">The request to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static EventLogQueryRequestDto ToProto(EventLogQueryRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
var dto = new EventLogQueryRequestDto
|
|
{
|
|
CorrelationId = request.CorrelationId,
|
|
SiteId = request.SiteId,
|
|
EventType = OrEmpty(request.EventType),
|
|
Severity = OrEmpty(request.Severity),
|
|
InstanceId = OrEmpty(request.InstanceId),
|
|
KeywordFilter = OrEmpty(request.KeywordFilter),
|
|
ContinuationToken = OrEmpty(request.ContinuationToken),
|
|
PageSize = request.PageSize,
|
|
Timestamp = Ts(request.Timestamp)
|
|
};
|
|
|
|
if (request.From.HasValue)
|
|
{
|
|
dto.From = Ts(request.From.Value);
|
|
}
|
|
|
|
if (request.To.HasValue)
|
|
{
|
|
dto.To = Ts(request.To.Value);
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates an <see cref="EventLogQueryRequest"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded request record.</returns>
|
|
public static EventLogQueryRequest FromProto(EventLogQueryRequestDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new EventLogQueryRequest(
|
|
dto.CorrelationId,
|
|
dto.SiteId,
|
|
DtoOrNull(dto.From),
|
|
DtoOrNull(dto.To),
|
|
NullIfEmpty(dto.EventType),
|
|
NullIfEmpty(dto.Severity),
|
|
NullIfEmpty(dto.InstanceId),
|
|
NullIfEmpty(dto.KeywordFilter),
|
|
NullIfEmpty(dto.ContinuationToken),
|
|
dto.PageSize,
|
|
Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="DebugSnapshotRequest"/> onto the wire.</summary>
|
|
/// <param name="request">The request to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static DebugSnapshotRequestDto ToProto(DebugSnapshotRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
return new DebugSnapshotRequestDto
|
|
{
|
|
InstanceUniqueName = request.InstanceUniqueName,
|
|
CorrelationId = request.CorrelationId,
|
|
// Additive field 3 — proto3 false default means an older site that does
|
|
// not know the field simply returns the full snapshot (correct, just fatter).
|
|
AlarmsOnly = request.AlarmsOnly
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="DebugSnapshotRequest"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded request record.</returns>
|
|
public static DebugSnapshotRequest FromProto(DebugSnapshotRequestDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new DebugSnapshotRequest(dto.InstanceUniqueName, dto.CorrelationId, dto.AlarmsOnly);
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="SubscribeDebugViewRequest"/> onto the wire.</summary>
|
|
/// <param name="request">The request to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static SubscribeDebugViewRequestDto ToProto(SubscribeDebugViewRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
return new SubscribeDebugViewRequestDto
|
|
{
|
|
InstanceUniqueName = request.InstanceUniqueName,
|
|
CorrelationId = request.CorrelationId
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="SubscribeDebugViewRequest"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded request record.</returns>
|
|
public static SubscribeDebugViewRequest FromProto(SubscribeDebugViewRequestDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new SubscribeDebugViewRequest(dto.InstanceUniqueName, dto.CorrelationId);
|
|
}
|
|
|
|
/// <summary>Projects an <see cref="UnsubscribeDebugViewRequest"/> onto the wire.</summary>
|
|
/// <param name="request">The request to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static UnsubscribeDebugViewRequestDto ToProto(UnsubscribeDebugViewRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
return new UnsubscribeDebugViewRequestDto
|
|
{
|
|
InstanceUniqueName = request.InstanceUniqueName,
|
|
CorrelationId = request.CorrelationId
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates an <see cref="UnsubscribeDebugViewRequest"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded request record.</returns>
|
|
public static UnsubscribeDebugViewRequest FromProto(UnsubscribeDebugViewRequestDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new UnsubscribeDebugViewRequest(dto.InstanceUniqueName, dto.CorrelationId);
|
|
}
|
|
|
|
// ── Query replies ──
|
|
|
|
/// <summary>Projects an <see cref="EventLogQueryResponse"/> onto the wire.</summary>
|
|
/// <param name="response">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static EventLogQueryResponseDto ToProto(EventLogQueryResponse response)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(response);
|
|
|
|
var dto = new EventLogQueryResponseDto
|
|
{
|
|
CorrelationId = response.CorrelationId,
|
|
SiteId = response.SiteId,
|
|
ContinuationToken = OrEmpty(response.ContinuationToken),
|
|
HasMore = response.HasMore,
|
|
Success = response.Success,
|
|
ErrorMessage = OrEmpty(response.ErrorMessage),
|
|
Timestamp = Ts(response.Timestamp)
|
|
};
|
|
dto.Entries.AddRange(response.Entries.Select(ToProto));
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates an <see cref="EventLogQueryResponse"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static EventLogQueryResponse FromProto(EventLogQueryResponseDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new EventLogQueryResponse(
|
|
dto.CorrelationId,
|
|
dto.SiteId,
|
|
dto.Entries.Select(FromProto).ToList(),
|
|
NullIfEmpty(dto.ContinuationToken),
|
|
dto.HasMore,
|
|
dto.Success,
|
|
NullIfEmpty(dto.ErrorMessage),
|
|
Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects an <see cref="EventLogEntry"/> onto the wire.</summary>
|
|
/// <param name="entry">The event-log row to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static EventLogEntryDto ToProto(EventLogEntry entry)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entry);
|
|
|
|
return new EventLogEntryDto
|
|
{
|
|
Id = entry.Id,
|
|
Timestamp = Ts(entry.Timestamp),
|
|
EventType = entry.EventType,
|
|
Severity = entry.Severity,
|
|
InstanceId = OrEmpty(entry.InstanceId),
|
|
Source = entry.Source,
|
|
Message = entry.Message,
|
|
Details = OrEmpty(entry.Details)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates an <see cref="EventLogEntry"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded event-log row.</returns>
|
|
public static EventLogEntry FromProto(EventLogEntryDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new EventLogEntry(
|
|
dto.Id,
|
|
Dto(dto.Timestamp),
|
|
dto.EventType,
|
|
dto.Severity,
|
|
NullIfEmpty(dto.InstanceId),
|
|
dto.Source,
|
|
dto.Message,
|
|
NullIfEmpty(dto.Details));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="DebugViewSnapshot"/> onto the wire.</summary>
|
|
/// <param name="snapshot">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static DebugViewSnapshotDto ToProto(DebugViewSnapshot snapshot)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(snapshot);
|
|
|
|
var dto = new DebugViewSnapshotDto
|
|
{
|
|
InstanceUniqueName = snapshot.InstanceUniqueName,
|
|
SnapshotTimestamp = Ts(snapshot.SnapshotTimestamp),
|
|
InstanceNotFound = snapshot.InstanceNotFound
|
|
};
|
|
dto.AttributeValues.AddRange(snapshot.AttributeValues.Select(ToProto));
|
|
dto.AlarmStates.AddRange(snapshot.AlarmStates.Select(ToProto));
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="DebugViewSnapshot"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static DebugViewSnapshot FromProto(DebugViewSnapshotDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new DebugViewSnapshot(
|
|
dto.InstanceUniqueName,
|
|
dto.AttributeValues.Select(FromProto).ToList(),
|
|
dto.AlarmStates.Select(FromProto).ToList(),
|
|
Dto(dto.SnapshotTimestamp),
|
|
dto.InstanceNotFound);
|
|
}
|
|
|
|
/// <summary>Projects an <see cref="AttributeValueChanged"/> snapshot row onto the wire.</summary>
|
|
/// <param name="value">The attribute snapshot row to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static DebugAttributeValueDto ToProto(AttributeValueChanged value)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(value);
|
|
|
|
return new DebugAttributeValueDto
|
|
{
|
|
InstanceUniqueName = value.InstanceUniqueName,
|
|
AttributePath = value.AttributePath,
|
|
AttributeName = value.AttributeName,
|
|
Value = LooseValueCodec.ToProtoOrNull(value.Value),
|
|
Quality = value.Quality,
|
|
Timestamp = Ts(value.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates an <see cref="AttributeValueChanged"/> snapshot row from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded attribute snapshot row.</returns>
|
|
public static AttributeValueChanged FromProto(DebugAttributeValueDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new AttributeValueChanged(
|
|
dto.InstanceUniqueName,
|
|
dto.AttributePath,
|
|
dto.AttributeName,
|
|
LooseValueCodec.FromProto(dto.Value),
|
|
dto.Quality,
|
|
Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects an <see cref="AlarmStateChanged"/> snapshot row onto the wire.</summary>
|
|
/// <remarks>
|
|
/// <see cref="AlarmStateChanged.Condition"/> is only written when it differs
|
|
/// from the value the record itself derives from <c>State</c> + <c>Priority</c>.
|
|
/// The record stores the condition in a nullable backing field and synthesises
|
|
/// the computed default on read, so writing it unconditionally would turn an
|
|
/// implicit condition into an explicit one — indistinguishable through the
|
|
/// property, but a difference under record equality. Omitting the redundant
|
|
/// value keeps the round trip exact for computed alarms, which are the common
|
|
/// case, and is information-preserving for the rest.
|
|
/// </remarks>
|
|
/// <param name="alarm">The alarm snapshot row to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static DebugAlarmStateDto ToProto(AlarmStateChanged alarm)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(alarm);
|
|
|
|
var dto = new DebugAlarmStateDto
|
|
{
|
|
InstanceUniqueName = alarm.InstanceUniqueName,
|
|
AlarmName = alarm.AlarmName,
|
|
State = ToProto(alarm.State),
|
|
Priority = alarm.Priority,
|
|
Timestamp = Ts(alarm.Timestamp),
|
|
Level = ToProto(alarm.Level),
|
|
Message = alarm.Message,
|
|
Kind = ToProto(alarm.Kind),
|
|
SourceReference = alarm.SourceReference,
|
|
AlarmTypeName = alarm.AlarmTypeName,
|
|
Category = alarm.Category,
|
|
OperatorUser = alarm.OperatorUser,
|
|
OperatorComment = alarm.OperatorComment,
|
|
CurrentValue = alarm.CurrentValue,
|
|
LimitValue = alarm.LimitValue,
|
|
NativeSourceCanonicalName = alarm.NativeSourceCanonicalName,
|
|
IsConfiguredPlaceholder = alarm.IsConfiguredPlaceholder,
|
|
IsMesReportable = alarm.IsMesReportable
|
|
};
|
|
|
|
if (alarm.Condition != AlarmConditionStateFactory.ForComputed(alarm.State, alarm.Priority))
|
|
{
|
|
dto.Condition = ToProto(alarm.Condition);
|
|
}
|
|
|
|
if (alarm.OriginalRaiseTime.HasValue)
|
|
{
|
|
dto.OriginalRaiseTime = Ts(alarm.OriginalRaiseTime.Value);
|
|
}
|
|
|
|
if (alarm.AckTime.HasValue)
|
|
{
|
|
dto.AckTime = Ts(alarm.AckTime.Value);
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates an <see cref="AlarmStateChanged"/> snapshot row from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded alarm snapshot row.</returns>
|
|
public static AlarmStateChanged FromProto(DebugAlarmStateDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
var alarm = new AlarmStateChanged(
|
|
dto.InstanceUniqueName,
|
|
dto.AlarmName,
|
|
FromProto(dto.State),
|
|
dto.Priority,
|
|
Dto(dto.Timestamp))
|
|
{
|
|
Level = FromProto(dto.Level),
|
|
Message = dto.Message,
|
|
Kind = FromProto(dto.Kind),
|
|
SourceReference = dto.SourceReference,
|
|
AlarmTypeName = dto.AlarmTypeName,
|
|
Category = dto.Category,
|
|
OperatorUser = dto.OperatorUser,
|
|
OperatorComment = dto.OperatorComment,
|
|
OriginalRaiseTime = DtoOrNull(dto.OriginalRaiseTime),
|
|
CurrentValue = dto.CurrentValue,
|
|
LimitValue = dto.LimitValue,
|
|
NativeSourceCanonicalName = dto.NativeSourceCanonicalName,
|
|
IsConfiguredPlaceholder = dto.IsConfiguredPlaceholder,
|
|
IsMesReportable = dto.IsMesReportable,
|
|
AckTime = DtoOrNull(dto.AckTime)
|
|
};
|
|
|
|
// AlarmConditionStateDto carries Severity only; the raw priority (Gitea #129) travels as the
|
|
// row's own Priority, which the site always sets equal to Condition.Priority, so restore it here.
|
|
return dto.Condition is null
|
|
? alarm
|
|
: alarm with { Condition = FromProto(dto.Condition) with { Priority = dto.Priority } };
|
|
}
|
|
|
|
/// <summary>Projects an <see cref="AlarmConditionState"/> onto the wire.</summary>
|
|
/// <param name="condition">The unified alarm condition to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static AlarmConditionStateDto ToProto(AlarmConditionState condition)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(condition);
|
|
|
|
var dto = new AlarmConditionStateDto
|
|
{
|
|
Active = condition.Active,
|
|
Acknowledged = condition.Acknowledged,
|
|
Shelve = ToProto(condition.Shelve),
|
|
Suppressed = condition.Suppressed,
|
|
Severity = condition.Severity
|
|
};
|
|
|
|
if (condition.Confirmed.HasValue)
|
|
{
|
|
dto.Confirmed = condition.Confirmed.Value;
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates an <see cref="AlarmConditionState"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded unified alarm condition.</returns>
|
|
public static AlarmConditionState FromProto(AlarmConditionStateDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new AlarmConditionState(
|
|
dto.Active,
|
|
dto.Acknowledged,
|
|
dto.Confirmed,
|
|
FromProto(dto.Shelve),
|
|
dto.Suppressed,
|
|
dto.Severity);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// RPC 4 — ExecuteParked: envelopes
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Packs a parked-group command into its request envelope.</summary>
|
|
/// <param name="command">One of the five parked-group commands.</param>
|
|
/// <returns>The populated request envelope.</returns>
|
|
/// <exception cref="ArgumentException">The command does not belong to this group.</exception>
|
|
public static ParkedRequest ToParkedRequest(object command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return command switch
|
|
{
|
|
ParkedMessageQueryRequest c => new ParkedRequest { ParkedMessageQuery = ToProto(c) },
|
|
ParkedMessageRetryRequest c => new ParkedRequest { ParkedMessageRetry = ToProto(c) },
|
|
ParkedMessageDiscardRequest c => new ParkedRequest { ParkedMessageDiscard = ToProto(c) },
|
|
RetryParkedOperation c => new ParkedRequest { RetryParkedOperation = ToProto(c) },
|
|
DiscardParkedOperation c => new ParkedRequest { DiscardParkedOperation = ToProto(c) },
|
|
_ => throw NotInGroup(command, SiteCommandGroup.Parked)
|
|
};
|
|
}
|
|
|
|
/// <summary>Unpacks a parked-group request envelope back to its command record.</summary>
|
|
/// <param name="request">The request envelope received on the wire.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
|
|
public static object FromParkedRequest(ParkedRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
return request.CommandCase switch
|
|
{
|
|
ParkedRequest.CommandOneofCase.ParkedMessageQuery => FromProto(request.ParkedMessageQuery),
|
|
ParkedRequest.CommandOneofCase.ParkedMessageRetry => FromProto(request.ParkedMessageRetry),
|
|
ParkedRequest.CommandOneofCase.ParkedMessageDiscard => FromProto(request.ParkedMessageDiscard),
|
|
ParkedRequest.CommandOneofCase.RetryParkedOperation => FromProto(request.RetryParkedOperation),
|
|
ParkedRequest.CommandOneofCase.DiscardParkedOperation => FromProto(request.DiscardParkedOperation),
|
|
_ => throw UnknownCase(request.CommandCase, nameof(ParkedRequest))
|
|
};
|
|
}
|
|
|
|
/// <summary>Packs a parked-group reply into its reply envelope.</summary>
|
|
/// <param name="reply">One of the four parked-group reply records.</param>
|
|
/// <returns>The populated reply envelope.</returns>
|
|
/// <exception cref="ArgumentException">The reply does not belong to this group.</exception>
|
|
public static ParkedReply ToParkedReply(object reply)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(reply);
|
|
|
|
return reply switch
|
|
{
|
|
ParkedMessageQueryResponse r => new ParkedReply { ParkedMessageQuery = ToProto(r) },
|
|
ParkedMessageRetryResponse r => new ParkedReply { ParkedMessageRetry = ToProto(r) },
|
|
ParkedMessageDiscardResponse r => new ParkedReply { ParkedMessageDiscard = ToProto(r) },
|
|
ParkedOperationActionAck r => new ParkedReply { ParkedOperationAction = ToProto(r) },
|
|
_ => throw NotInGroup(reply, SiteCommandGroup.Parked)
|
|
};
|
|
}
|
|
|
|
/// <summary>Unpacks a parked-group reply envelope back to its reply record.</summary>
|
|
/// <param name="reply">The reply envelope received on the wire.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
|
|
public static object FromParkedReply(ParkedReply reply)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(reply);
|
|
|
|
return reply.ReplyCase switch
|
|
{
|
|
ParkedReply.ReplyOneofCase.ParkedMessageQuery => FromProto(reply.ParkedMessageQuery),
|
|
ParkedReply.ReplyOneofCase.ParkedMessageRetry => FromProto(reply.ParkedMessageRetry),
|
|
ParkedReply.ReplyOneofCase.ParkedMessageDiscard => FromProto(reply.ParkedMessageDiscard),
|
|
ParkedReply.ReplyOneofCase.ParkedOperationAction => FromProto(reply.ParkedOperationAction),
|
|
_ => throw UnknownCase(reply.ReplyCase, nameof(ParkedReply))
|
|
};
|
|
}
|
|
|
|
// ── Parked commands ──
|
|
|
|
/// <summary>Projects a <see cref="ParkedMessageQueryRequest"/> onto the wire.</summary>
|
|
/// <param name="request">The request to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ParkedMessageQueryRequestDto ToProto(ParkedMessageQueryRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
return new ParkedMessageQueryRequestDto
|
|
{
|
|
CorrelationId = request.CorrelationId,
|
|
SiteId = request.SiteId,
|
|
PageNumber = request.PageNumber,
|
|
PageSize = request.PageSize,
|
|
Timestamp = Ts(request.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="ParkedMessageQueryRequest"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded request record.</returns>
|
|
public static ParkedMessageQueryRequest FromProto(ParkedMessageQueryRequestDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ParkedMessageQueryRequest(
|
|
dto.CorrelationId, dto.SiteId, dto.PageNumber, dto.PageSize, Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="ParkedMessageRetryRequest"/> onto the wire.</summary>
|
|
/// <param name="request">The request to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ParkedMessageRetryRequestDto ToProto(ParkedMessageRetryRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
return new ParkedMessageRetryRequestDto
|
|
{
|
|
CorrelationId = request.CorrelationId,
|
|
SiteId = request.SiteId,
|
|
MessageId = request.MessageId,
|
|
Timestamp = Ts(request.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="ParkedMessageRetryRequest"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded request record.</returns>
|
|
public static ParkedMessageRetryRequest FromProto(ParkedMessageRetryRequestDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ParkedMessageRetryRequest(
|
|
dto.CorrelationId, dto.SiteId, dto.MessageId, Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="ParkedMessageDiscardRequest"/> onto the wire.</summary>
|
|
/// <param name="request">The request to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ParkedMessageDiscardRequestDto ToProto(ParkedMessageDiscardRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
return new ParkedMessageDiscardRequestDto
|
|
{
|
|
CorrelationId = request.CorrelationId,
|
|
SiteId = request.SiteId,
|
|
MessageId = request.MessageId,
|
|
Timestamp = Ts(request.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="ParkedMessageDiscardRequest"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded request record.</returns>
|
|
public static ParkedMessageDiscardRequest FromProto(ParkedMessageDiscardRequestDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ParkedMessageDiscardRequest(
|
|
dto.CorrelationId, dto.SiteId, dto.MessageId, Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="RetryParkedOperation"/> relay onto the wire.</summary>
|
|
/// <param name="command">The relay command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static RetryParkedOperationDto ToProto(RetryParkedOperation command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return new RetryParkedOperationDto
|
|
{
|
|
CorrelationId = command.CorrelationId,
|
|
TrackedOperationId = command.TrackedOperationId.ToString()
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="RetryParkedOperation"/> relay from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded relay command.</returns>
|
|
public static RetryParkedOperation FromProto(RetryParkedOperationDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new RetryParkedOperation(dto.CorrelationId, TrackedId(dto.TrackedOperationId));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="DiscardParkedOperation"/> relay onto the wire.</summary>
|
|
/// <param name="command">The relay command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static DiscardParkedOperationDto ToProto(DiscardParkedOperation command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return new DiscardParkedOperationDto
|
|
{
|
|
CorrelationId = command.CorrelationId,
|
|
TrackedOperationId = command.TrackedOperationId.ToString()
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="DiscardParkedOperation"/> relay from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded relay command.</returns>
|
|
public static DiscardParkedOperation FromProto(DiscardParkedOperationDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new DiscardParkedOperation(dto.CorrelationId, TrackedId(dto.TrackedOperationId));
|
|
}
|
|
|
|
// ── Parked replies ──
|
|
|
|
/// <summary>Projects a <see cref="ParkedMessageQueryResponse"/> onto the wire.</summary>
|
|
/// <param name="response">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ParkedMessageQueryResponseDto ToProto(ParkedMessageQueryResponse response)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(response);
|
|
|
|
var dto = new ParkedMessageQueryResponseDto
|
|
{
|
|
CorrelationId = response.CorrelationId,
|
|
SiteId = response.SiteId,
|
|
TotalCount = response.TotalCount,
|
|
PageNumber = response.PageNumber,
|
|
PageSize = response.PageSize,
|
|
Success = response.Success,
|
|
ErrorMessage = OrEmpty(response.ErrorMessage),
|
|
Timestamp = Ts(response.Timestamp)
|
|
};
|
|
dto.Messages.AddRange(response.Messages.Select(ToProto));
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="ParkedMessageQueryResponse"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static ParkedMessageQueryResponse FromProto(ParkedMessageQueryResponseDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ParkedMessageQueryResponse(
|
|
dto.CorrelationId,
|
|
dto.SiteId,
|
|
dto.Messages.Select(FromProto).ToList(),
|
|
dto.TotalCount,
|
|
dto.PageNumber,
|
|
dto.PageSize,
|
|
dto.Success,
|
|
NullIfEmpty(dto.ErrorMessage),
|
|
Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="ParkedMessageEntry"/> onto the wire.</summary>
|
|
/// <param name="entry">The parked store-and-forward row to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ParkedMessageEntryDto ToProto(ParkedMessageEntry entry)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entry);
|
|
|
|
return new ParkedMessageEntryDto
|
|
{
|
|
MessageId = entry.MessageId,
|
|
TargetSystem = entry.TargetSystem,
|
|
MethodName = entry.MethodName,
|
|
ErrorMessage = entry.ErrorMessage,
|
|
AttemptCount = entry.AttemptCount,
|
|
OriginalTimestamp = Ts(entry.OriginalTimestamp),
|
|
LastAttemptTimestamp = Ts(entry.LastAttemptTimestamp),
|
|
MaxAttempts = entry.MaxAttempts,
|
|
Category = ToProto(entry.Category),
|
|
OriginInstance = OrEmpty(entry.OriginInstance)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="ParkedMessageEntry"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded parked store-and-forward row.</returns>
|
|
public static ParkedMessageEntry FromProto(ParkedMessageEntryDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ParkedMessageEntry(
|
|
dto.MessageId,
|
|
dto.TargetSystem,
|
|
dto.MethodName,
|
|
dto.ErrorMessage,
|
|
dto.AttemptCount,
|
|
Dto(dto.OriginalTimestamp),
|
|
Dto(dto.LastAttemptTimestamp),
|
|
dto.MaxAttempts,
|
|
FromProto(dto.Category),
|
|
NullIfEmpty(dto.OriginInstance));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="ParkedMessageRetryResponse"/> onto the wire.</summary>
|
|
/// <param name="response">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ParkedMessageRetryResponseDto ToProto(ParkedMessageRetryResponse response)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(response);
|
|
|
|
return new ParkedMessageRetryResponseDto
|
|
{
|
|
CorrelationId = response.CorrelationId,
|
|
Success = response.Success,
|
|
ErrorMessage = OrEmpty(response.ErrorMessage)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="ParkedMessageRetryResponse"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static ParkedMessageRetryResponse FromProto(ParkedMessageRetryResponseDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ParkedMessageRetryResponse(dto.CorrelationId, dto.Success, NullIfEmpty(dto.ErrorMessage));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="ParkedMessageDiscardResponse"/> onto the wire.</summary>
|
|
/// <param name="response">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ParkedMessageDiscardResponseDto ToProto(ParkedMessageDiscardResponse response)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(response);
|
|
|
|
return new ParkedMessageDiscardResponseDto
|
|
{
|
|
CorrelationId = response.CorrelationId,
|
|
Success = response.Success,
|
|
ErrorMessage = OrEmpty(response.ErrorMessage)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="ParkedMessageDiscardResponse"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static ParkedMessageDiscardResponse FromProto(ParkedMessageDiscardResponseDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ParkedMessageDiscardResponse(dto.CorrelationId, dto.Success, NullIfEmpty(dto.ErrorMessage));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="ParkedOperationActionAck"/> onto the wire.</summary>
|
|
/// <param name="ack">The relay ack to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static ParkedOperationActionAckDto ToProto(ParkedOperationActionAck ack)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(ack);
|
|
|
|
return new ParkedOperationActionAckDto
|
|
{
|
|
CorrelationId = ack.CorrelationId,
|
|
Applied = ack.Applied,
|
|
ErrorMessage = OrEmpty(ack.ErrorMessage)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="ParkedOperationActionAck"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded relay ack.</returns>
|
|
public static ParkedOperationActionAck FromProto(ParkedOperationActionAckDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new ParkedOperationActionAck(dto.CorrelationId, dto.Applied, NullIfEmpty(dto.ErrorMessage));
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// RPC 5 — ExecuteRoute: envelopes
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Packs a route-group command into its request envelope.</summary>
|
|
/// <param name="command">One of the four route-group commands.</param>
|
|
/// <returns>The populated request envelope.</returns>
|
|
/// <exception cref="ArgumentException">The command does not belong to this group.</exception>
|
|
public static RouteRequest ToRouteRequest(object command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return command switch
|
|
{
|
|
RouteToCallRequest c => new RouteRequest { RouteToCall = ToProto(c) },
|
|
RouteToGetAttributesRequest c => new RouteRequest { RouteToGetAttributes = ToProto(c) },
|
|
RouteToSetAttributesRequest c => new RouteRequest { RouteToSetAttributes = ToProto(c) },
|
|
RouteToWaitForAttributeRequest c => new RouteRequest { RouteToWaitForAttribute = ToProto(c) },
|
|
_ => throw NotInGroup(command, SiteCommandGroup.Route)
|
|
};
|
|
}
|
|
|
|
/// <summary>Unpacks a route-group request envelope back to its command record.</summary>
|
|
/// <param name="request">The request envelope received on the wire.</param>
|
|
/// <returns>The decoded command record.</returns>
|
|
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
|
|
public static object FromRouteRequest(RouteRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
return request.CommandCase switch
|
|
{
|
|
RouteRequest.CommandOneofCase.RouteToCall => FromProto(request.RouteToCall),
|
|
RouteRequest.CommandOneofCase.RouteToGetAttributes => FromProto(request.RouteToGetAttributes),
|
|
RouteRequest.CommandOneofCase.RouteToSetAttributes => FromProto(request.RouteToSetAttributes),
|
|
RouteRequest.CommandOneofCase.RouteToWaitForAttribute => FromProto(request.RouteToWaitForAttribute),
|
|
_ => throw UnknownCase(request.CommandCase, nameof(RouteRequest))
|
|
};
|
|
}
|
|
|
|
/// <summary>Packs a route-group reply into its reply envelope.</summary>
|
|
/// <param name="reply">One of the four route-group reply records.</param>
|
|
/// <returns>The populated reply envelope.</returns>
|
|
/// <exception cref="ArgumentException">The reply does not belong to this group.</exception>
|
|
public static RouteReply ToRouteReply(object reply)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(reply);
|
|
|
|
return reply switch
|
|
{
|
|
RouteToCallResponse r => new RouteReply { RouteToCall = ToProto(r) },
|
|
RouteToGetAttributesResponse r => new RouteReply { RouteToGetAttributes = ToProto(r) },
|
|
RouteToSetAttributesResponse r => new RouteReply { RouteToSetAttributes = ToProto(r) },
|
|
RouteToWaitForAttributeResponse r => new RouteReply { RouteToWaitForAttribute = ToProto(r) },
|
|
_ => throw NotInGroup(reply, SiteCommandGroup.Route)
|
|
};
|
|
}
|
|
|
|
/// <summary>Unpacks a route-group reply envelope back to its reply record.</summary>
|
|
/// <param name="reply">The reply envelope received on the wire.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
/// <exception cref="NotSupportedException">The envelope carries no case this build knows.</exception>
|
|
public static object FromRouteReply(RouteReply reply)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(reply);
|
|
|
|
return reply.ReplyCase switch
|
|
{
|
|
RouteReply.ReplyOneofCase.RouteToCall => FromProto(reply.RouteToCall),
|
|
RouteReply.ReplyOneofCase.RouteToGetAttributes => FromProto(reply.RouteToGetAttributes),
|
|
RouteReply.ReplyOneofCase.RouteToSetAttributes => FromProto(reply.RouteToSetAttributes),
|
|
RouteReply.ReplyOneofCase.RouteToWaitForAttribute => FromProto(reply.RouteToWaitForAttribute),
|
|
_ => throw UnknownCase(reply.ReplyCase, nameof(RouteReply))
|
|
};
|
|
}
|
|
|
|
// ── Route commands ──
|
|
|
|
/// <summary>Projects a <see cref="RouteToCallRequest"/> onto the wire.</summary>
|
|
/// <param name="request">The request to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static RouteToCallRequestDto ToProto(RouteToCallRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
return new RouteToCallRequestDto
|
|
{
|
|
CorrelationId = request.CorrelationId,
|
|
InstanceUniqueName = request.InstanceUniqueName,
|
|
ScriptName = request.ScriptName,
|
|
Parameters = LooseValueCodec.ToProtoMapOrNull(request.Parameters),
|
|
Timestamp = Ts(request.Timestamp),
|
|
ParentExecutionId = GuidToWire(request.ParentExecutionId),
|
|
Trace = request.Trace
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="RouteToCallRequest"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded request record.</returns>
|
|
public static RouteToCallRequest FromProto(RouteToCallRequestDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new RouteToCallRequest(
|
|
dto.CorrelationId,
|
|
dto.InstanceUniqueName,
|
|
dto.ScriptName,
|
|
LooseValueCodec.FromProtoMapOrNull(dto.Parameters),
|
|
Dto(dto.Timestamp),
|
|
GuidFromWire(dto.ParentExecutionId),
|
|
dto.Trace);
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="RouteToGetAttributesRequest"/> onto the wire.</summary>
|
|
/// <param name="request">The request to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static RouteToGetAttributesRequestDto ToProto(RouteToGetAttributesRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
var dto = new RouteToGetAttributesRequestDto
|
|
{
|
|
CorrelationId = request.CorrelationId,
|
|
InstanceUniqueName = request.InstanceUniqueName,
|
|
Timestamp = Ts(request.Timestamp),
|
|
ParentExecutionId = GuidToWire(request.ParentExecutionId)
|
|
};
|
|
dto.AttributeNames.AddRange(request.AttributeNames);
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="RouteToGetAttributesRequest"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded request record.</returns>
|
|
public static RouteToGetAttributesRequest FromProto(RouteToGetAttributesRequestDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new RouteToGetAttributesRequest(
|
|
dto.CorrelationId,
|
|
dto.InstanceUniqueName,
|
|
dto.AttributeNames.ToList(),
|
|
Dto(dto.Timestamp),
|
|
GuidFromWire(dto.ParentExecutionId));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="RouteToSetAttributesRequest"/> onto the wire.</summary>
|
|
/// <param name="request">The request to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static RouteToSetAttributesRequestDto ToProto(RouteToSetAttributesRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
var dto = new RouteToSetAttributesRequestDto
|
|
{
|
|
CorrelationId = request.CorrelationId,
|
|
InstanceUniqueName = request.InstanceUniqueName,
|
|
Timestamp = Ts(request.Timestamp),
|
|
ParentExecutionId = GuidToWire(request.ParentExecutionId),
|
|
Trace = request.Trace
|
|
};
|
|
|
|
foreach (var (key, value) in request.AttributeValues)
|
|
{
|
|
dto.AttributeValues[key] = value;
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="RouteToSetAttributesRequest"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded request record.</returns>
|
|
public static RouteToSetAttributesRequest FromProto(RouteToSetAttributesRequestDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new RouteToSetAttributesRequest(
|
|
dto.CorrelationId,
|
|
dto.InstanceUniqueName,
|
|
dto.AttributeValues.ToDictionary(kv => kv.Key, kv => kv.Value),
|
|
Dto(dto.Timestamp),
|
|
GuidFromWire(dto.ParentExecutionId),
|
|
dto.Trace);
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="RouteToWaitForAttributeRequest"/> onto the wire.</summary>
|
|
/// <param name="request">The request to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static RouteToWaitForAttributeRequestDto ToProto(RouteToWaitForAttributeRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
var dto = new RouteToWaitForAttributeRequestDto
|
|
{
|
|
CorrelationId = request.CorrelationId,
|
|
InstanceUniqueName = request.InstanceUniqueName,
|
|
AttributeName = request.AttributeName,
|
|
Timeout = Duration.FromTimeSpan(request.Timeout),
|
|
Timestamp = Ts(request.Timestamp),
|
|
ParentExecutionId = GuidToWire(request.ParentExecutionId),
|
|
RequireGoodQuality = request.RequireGoodQuality
|
|
};
|
|
|
|
if (request.TargetValueEncoded is not null)
|
|
{
|
|
dto.TargetValueEncoded = request.TargetValueEncoded;
|
|
}
|
|
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="RouteToWaitForAttributeRequest"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded request record.</returns>
|
|
public static RouteToWaitForAttributeRequest FromProto(RouteToWaitForAttributeRequestDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new RouteToWaitForAttributeRequest(
|
|
dto.CorrelationId,
|
|
dto.InstanceUniqueName,
|
|
dto.AttributeName,
|
|
dto.TargetValueEncoded,
|
|
dto.Timeout?.ToTimeSpan() ?? TimeSpan.Zero,
|
|
Dto(dto.Timestamp),
|
|
GuidFromWire(dto.ParentExecutionId),
|
|
dto.RequireGoodQuality);
|
|
}
|
|
|
|
// ── Route replies ──
|
|
|
|
/// <summary>Projects a <see cref="RouteToCallResponse"/> onto the wire.</summary>
|
|
/// <param name="response">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static RouteToCallResponseDto ToProto(RouteToCallResponse response)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(response);
|
|
|
|
var dto = new RouteToCallResponseDto
|
|
{
|
|
CorrelationId = response.CorrelationId,
|
|
Success = response.Success,
|
|
ReturnValue = LooseValueCodec.ToProtoOrNull(response.ReturnValue),
|
|
ErrorMessage = OrEmpty(response.ErrorMessage),
|
|
Timestamp = Ts(response.Timestamp),
|
|
// Flow Debugger (design 2026-09-05 §5.4): additive fields 6/7.
|
|
ChildExecutionId = GuidToWire(response.ChildExecutionId),
|
|
// Gitea #54: additive fields 8/9 — the site collector's truncation flags.
|
|
Truncated = response.Truncated,
|
|
LogsTruncated = response.LogsTruncated
|
|
};
|
|
if (response.TraceEvents is { Count: > 0 } traceEvents)
|
|
{
|
|
foreach (var evt in traceEvents)
|
|
{
|
|
dto.TraceEvents.Add(AuditEventDtoMapper.ToDto(evt));
|
|
}
|
|
}
|
|
return dto;
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="RouteToCallResponse"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static RouteToCallResponse FromProto(RouteToCallResponseDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new RouteToCallResponse(
|
|
dto.CorrelationId,
|
|
dto.Success,
|
|
LooseValueCodec.FromProto(dto.ReturnValue),
|
|
NullIfEmpty(dto.ErrorMessage),
|
|
Dto(dto.Timestamp),
|
|
GuidFromWire(dto.ChildExecutionId),
|
|
// An untraced run — or an older site that never sets the field — sends no
|
|
// events, which maps to null so the UI can label the lane "site not traced".
|
|
dto.TraceEvents.Count == 0 ? null : dto.TraceEvents.Select(AuditEventDtoMapper.FromDto).ToList(),
|
|
dto.Truncated,
|
|
dto.LogsTruncated);
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="RouteToGetAttributesResponse"/> onto the wire.</summary>
|
|
/// <param name="response">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static RouteToGetAttributesResponseDto ToProto(RouteToGetAttributesResponse response)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(response);
|
|
|
|
return new RouteToGetAttributesResponseDto
|
|
{
|
|
CorrelationId = response.CorrelationId,
|
|
Values = LooseValueCodec.ToProtoMap(response.Values),
|
|
Success = response.Success,
|
|
ErrorMessage = OrEmpty(response.ErrorMessage),
|
|
Timestamp = Ts(response.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="RouteToGetAttributesResponse"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static RouteToGetAttributesResponse FromProto(RouteToGetAttributesResponseDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new RouteToGetAttributesResponse(
|
|
dto.CorrelationId,
|
|
LooseValueCodec.FromProtoMap(dto.Values),
|
|
dto.Success,
|
|
NullIfEmpty(dto.ErrorMessage),
|
|
Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="RouteToSetAttributesResponse"/> onto the wire.</summary>
|
|
/// <param name="response">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static RouteToSetAttributesResponseDto ToProto(RouteToSetAttributesResponse response)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(response);
|
|
|
|
return new RouteToSetAttributesResponseDto
|
|
{
|
|
CorrelationId = response.CorrelationId,
|
|
Success = response.Success,
|
|
ErrorMessage = OrEmpty(response.ErrorMessage),
|
|
Timestamp = Ts(response.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="RouteToSetAttributesResponse"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static RouteToSetAttributesResponse FromProto(RouteToSetAttributesResponseDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new RouteToSetAttributesResponse(
|
|
dto.CorrelationId, dto.Success, NullIfEmpty(dto.ErrorMessage), Dto(dto.Timestamp));
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="RouteToWaitForAttributeResponse"/> onto the wire.</summary>
|
|
/// <param name="response">The reply to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static RouteToWaitForAttributeResponseDto ToProto(RouteToWaitForAttributeResponse response)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(response);
|
|
|
|
return new RouteToWaitForAttributeResponseDto
|
|
{
|
|
CorrelationId = response.CorrelationId,
|
|
Matched = response.Matched,
|
|
Value = LooseValueCodec.ToProtoOrNull(response.Value),
|
|
Quality = OrEmpty(response.Quality),
|
|
TimedOut = response.TimedOut,
|
|
Success = response.Success,
|
|
ErrorMessage = OrEmpty(response.ErrorMessage),
|
|
Timestamp = Ts(response.Timestamp)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="RouteToWaitForAttributeResponse"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded reply record.</returns>
|
|
public static RouteToWaitForAttributeResponse FromProto(RouteToWaitForAttributeResponseDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new RouteToWaitForAttributeResponse(
|
|
dto.CorrelationId,
|
|
dto.Matched,
|
|
LooseValueCodec.FromProto(dto.Value),
|
|
NullIfEmpty(dto.Quality),
|
|
dto.TimedOut,
|
|
dto.Success,
|
|
NullIfEmpty(dto.ErrorMessage),
|
|
Dto(dto.Timestamp));
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// RPC 6 — TriggerFailover
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Projects a <see cref="TriggerSiteFailover"/> onto the wire.</summary>
|
|
/// <param name="command">The relay command to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static TriggerSiteFailoverDto ToProto(TriggerSiteFailover command)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(command);
|
|
|
|
return new TriggerSiteFailoverDto
|
|
{
|
|
CorrelationId = command.CorrelationId,
|
|
SiteId = command.SiteId
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="TriggerSiteFailover"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded relay command.</returns>
|
|
public static TriggerSiteFailover FromProto(TriggerSiteFailoverDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new TriggerSiteFailover(dto.CorrelationId, dto.SiteId);
|
|
}
|
|
|
|
/// <summary>Projects a <see cref="SiteFailoverAck"/> onto the wire.</summary>
|
|
/// <param name="ack">The relay ack to project.</param>
|
|
/// <returns>The wire message.</returns>
|
|
public static SiteFailoverAckDto ToProto(SiteFailoverAck ack)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(ack);
|
|
|
|
return new SiteFailoverAckDto
|
|
{
|
|
CorrelationId = ack.CorrelationId,
|
|
Accepted = ack.Accepted,
|
|
TargetAddress = OrEmpty(ack.TargetAddress),
|
|
ErrorMessage = OrEmpty(ack.ErrorMessage)
|
|
};
|
|
}
|
|
|
|
/// <summary>Rehydrates a <see cref="SiteFailoverAck"/> from the wire.</summary>
|
|
/// <param name="dto">The wire message to decode.</param>
|
|
/// <returns>The decoded relay ack.</returns>
|
|
public static SiteFailoverAck FromProto(SiteFailoverAckDto dto)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dto);
|
|
|
|
return new SiteFailoverAck(
|
|
dto.CorrelationId,
|
|
dto.Accepted,
|
|
NullIfEmpty(dto.TargetAddress),
|
|
NullIfEmpty(dto.ErrorMessage));
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// Enum translation — explicit both ways, never by ordinal.
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Projects a <see cref="DeploymentStatus"/> onto its wire enum.</summary>
|
|
/// <param name="status">The status to project.</param>
|
|
/// <returns>The wire enum value.</returns>
|
|
public static DeploymentStatusDto ToProto(DeploymentStatus status) => status switch
|
|
{
|
|
DeploymentStatus.Pending => DeploymentStatusDto.Pending,
|
|
DeploymentStatus.InProgress => DeploymentStatusDto.InProgress,
|
|
DeploymentStatus.Success => DeploymentStatusDto.Success,
|
|
DeploymentStatus.Failed => DeploymentStatusDto.Failed,
|
|
_ => DeploymentStatusDto.Unspecified
|
|
};
|
|
|
|
/// <summary>Rehydrates a <see cref="DeploymentStatus"/>; unknown decodes to <c>Failed</c> (never claim success).</summary>
|
|
/// <param name="status">The wire enum value.</param>
|
|
/// <returns>The decoded status.</returns>
|
|
public static DeploymentStatus FromProto(DeploymentStatusDto status) => status switch
|
|
{
|
|
DeploymentStatusDto.Pending => DeploymentStatus.Pending,
|
|
DeploymentStatusDto.InProgress => DeploymentStatus.InProgress,
|
|
DeploymentStatusDto.Success => DeploymentStatus.Success,
|
|
_ => DeploymentStatus.Failed
|
|
};
|
|
|
|
/// <summary>Projects a <see cref="BrowseNodeClass"/> onto its wire enum.</summary>
|
|
/// <param name="nodeClass">The node class to project.</param>
|
|
/// <returns>The wire enum value.</returns>
|
|
public static BrowseNodeClassDto ToProto(BrowseNodeClass nodeClass) => nodeClass switch
|
|
{
|
|
BrowseNodeClass.Object => BrowseNodeClassDto.Object,
|
|
BrowseNodeClass.Variable => BrowseNodeClassDto.Variable,
|
|
BrowseNodeClass.Method => BrowseNodeClassDto.Method,
|
|
BrowseNodeClass.Other => BrowseNodeClassDto.Other,
|
|
_ => BrowseNodeClassDto.Unspecified
|
|
};
|
|
|
|
/// <summary>Rehydrates a <see cref="BrowseNodeClass"/>; unknown decodes to <c>Other</c>.</summary>
|
|
/// <param name="nodeClass">The wire enum value.</param>
|
|
/// <returns>The decoded node class.</returns>
|
|
public static BrowseNodeClass FromProto(BrowseNodeClassDto nodeClass) => nodeClass switch
|
|
{
|
|
BrowseNodeClassDto.Object => BrowseNodeClass.Object,
|
|
BrowseNodeClassDto.Variable => BrowseNodeClass.Variable,
|
|
BrowseNodeClassDto.Method => BrowseNodeClass.Method,
|
|
_ => BrowseNodeClass.Other
|
|
};
|
|
|
|
/// <summary>Projects a <see cref="BrowseFailureKind"/> onto its wire enum.</summary>
|
|
/// <param name="kind">The failure kind to project.</param>
|
|
/// <returns>The wire enum value.</returns>
|
|
public static BrowseFailureKindDto ToProto(BrowseFailureKind kind) => kind switch
|
|
{
|
|
BrowseFailureKind.ConnectionNotFound => BrowseFailureKindDto.ConnectionNotFound,
|
|
BrowseFailureKind.ConnectionNotConnected => BrowseFailureKindDto.ConnectionNotConnected,
|
|
BrowseFailureKind.NotBrowsable => BrowseFailureKindDto.NotBrowsable,
|
|
BrowseFailureKind.Timeout => BrowseFailureKindDto.Timeout,
|
|
BrowseFailureKind.ServerError => BrowseFailureKindDto.ServerError,
|
|
_ => BrowseFailureKindDto.Unspecified
|
|
};
|
|
|
|
/// <summary>Rehydrates a <see cref="BrowseFailureKind"/>; unknown decodes to <c>ServerError</c>.</summary>
|
|
/// <param name="kind">The wire enum value.</param>
|
|
/// <returns>The decoded failure kind.</returns>
|
|
public static BrowseFailureKind FromProto(BrowseFailureKindDto kind) => kind switch
|
|
{
|
|
BrowseFailureKindDto.ConnectionNotFound => BrowseFailureKind.ConnectionNotFound,
|
|
BrowseFailureKindDto.ConnectionNotConnected => BrowseFailureKind.ConnectionNotConnected,
|
|
BrowseFailureKindDto.NotBrowsable => BrowseFailureKind.NotBrowsable,
|
|
BrowseFailureKindDto.Timeout => BrowseFailureKind.Timeout,
|
|
_ => BrowseFailureKind.ServerError
|
|
};
|
|
|
|
/// <summary>Projects a <see cref="ReadTagValuesFailureKind"/> onto its wire enum.</summary>
|
|
/// <param name="kind">The failure kind to project.</param>
|
|
/// <returns>The wire enum value.</returns>
|
|
public static ReadTagValuesFailureKindDto ToProto(ReadTagValuesFailureKind kind) => kind switch
|
|
{
|
|
ReadTagValuesFailureKind.ConnectionNotFound => ReadTagValuesFailureKindDto.ConnectionNotFound,
|
|
ReadTagValuesFailureKind.ConnectionNotConnected => ReadTagValuesFailureKindDto.ConnectionNotConnected,
|
|
ReadTagValuesFailureKind.Timeout => ReadTagValuesFailureKindDto.Timeout,
|
|
ReadTagValuesFailureKind.ServerError => ReadTagValuesFailureKindDto.ServerError,
|
|
_ => ReadTagValuesFailureKindDto.Unspecified
|
|
};
|
|
|
|
/// <summary>Rehydrates a <see cref="ReadTagValuesFailureKind"/>; unknown decodes to <c>ServerError</c>.</summary>
|
|
/// <param name="kind">The wire enum value.</param>
|
|
/// <returns>The decoded failure kind.</returns>
|
|
public static ReadTagValuesFailureKind FromProto(ReadTagValuesFailureKindDto kind) => kind switch
|
|
{
|
|
ReadTagValuesFailureKindDto.ConnectionNotFound => ReadTagValuesFailureKind.ConnectionNotFound,
|
|
ReadTagValuesFailureKindDto.ConnectionNotConnected => ReadTagValuesFailureKind.ConnectionNotConnected,
|
|
ReadTagValuesFailureKindDto.Timeout => ReadTagValuesFailureKind.Timeout,
|
|
_ => ReadTagValuesFailureKind.ServerError
|
|
};
|
|
|
|
/// <summary>Projects a <see cref="VerifyFailureKind"/> onto its wire enum.</summary>
|
|
/// <param name="kind">The failure kind to project.</param>
|
|
/// <returns>The wire enum value.</returns>
|
|
public static VerifyFailureKindDto ToProto(VerifyFailureKind kind) => kind switch
|
|
{
|
|
VerifyFailureKind.Unreachable => VerifyFailureKindDto.Unreachable,
|
|
VerifyFailureKind.AuthFailed => VerifyFailureKindDto.AuthFailed,
|
|
VerifyFailureKind.UntrustedCertificate => VerifyFailureKindDto.UntrustedCertificate,
|
|
VerifyFailureKind.Timeout => VerifyFailureKindDto.Timeout,
|
|
VerifyFailureKind.ServerError => VerifyFailureKindDto.ServerError,
|
|
_ => VerifyFailureKindDto.Unspecified
|
|
};
|
|
|
|
/// <summary>Rehydrates a <see cref="VerifyFailureKind"/>; unknown decodes to <c>ServerError</c>.</summary>
|
|
/// <param name="kind">The wire enum value.</param>
|
|
/// <returns>The decoded failure kind.</returns>
|
|
public static VerifyFailureKind FromProto(VerifyFailureKindDto kind) => kind switch
|
|
{
|
|
VerifyFailureKindDto.Unreachable => VerifyFailureKind.Unreachable,
|
|
VerifyFailureKindDto.AuthFailed => VerifyFailureKind.AuthFailed,
|
|
VerifyFailureKindDto.UntrustedCertificate => VerifyFailureKind.UntrustedCertificate,
|
|
VerifyFailureKindDto.Timeout => VerifyFailureKind.Timeout,
|
|
_ => VerifyFailureKind.ServerError
|
|
};
|
|
|
|
/// <summary>Projects a <see cref="StoreAndForwardCategory"/> onto its wire enum.</summary>
|
|
/// <param name="category">The category to project.</param>
|
|
/// <returns>The wire enum value.</returns>
|
|
public static StoreAndForwardCategoryDto ToProto(StoreAndForwardCategory category) => category switch
|
|
{
|
|
StoreAndForwardCategory.ExternalSystem => StoreAndForwardCategoryDto.ExternalSystem,
|
|
StoreAndForwardCategory.Notification => StoreAndForwardCategoryDto.Notification,
|
|
StoreAndForwardCategory.CachedDbWrite => StoreAndForwardCategoryDto.CachedDbWrite,
|
|
_ => StoreAndForwardCategoryDto.Unspecified
|
|
};
|
|
|
|
/// <summary>Rehydrates a <see cref="StoreAndForwardCategory"/>; unknown decodes to <c>ExternalSystem</c> (the DTO's own default).</summary>
|
|
/// <param name="category">The wire enum value.</param>
|
|
/// <returns>The decoded category.</returns>
|
|
public static StoreAndForwardCategory FromProto(StoreAndForwardCategoryDto category) => category switch
|
|
{
|
|
StoreAndForwardCategoryDto.Notification => StoreAndForwardCategory.Notification,
|
|
StoreAndForwardCategoryDto.CachedDbWrite => StoreAndForwardCategory.CachedDbWrite,
|
|
_ => StoreAndForwardCategory.ExternalSystem
|
|
};
|
|
|
|
/// <summary>Projects an <see cref="AlarmState"/> onto its wire enum.</summary>
|
|
/// <param name="state">The alarm state to project.</param>
|
|
/// <returns>The wire enum value.</returns>
|
|
public static AlarmStateDto ToProto(AlarmState state) => state switch
|
|
{
|
|
AlarmState.Active => AlarmStateDto.Active,
|
|
AlarmState.Normal => AlarmStateDto.Normal,
|
|
_ => AlarmStateDto.Unspecified
|
|
};
|
|
|
|
/// <summary>Rehydrates an <see cref="AlarmState"/>; unknown decodes to <c>Normal</c> (never invent an alarm).</summary>
|
|
/// <param name="state">The wire enum value.</param>
|
|
/// <returns>The decoded alarm state.</returns>
|
|
public static AlarmState FromProto(AlarmStateDto state) =>
|
|
state == AlarmStateDto.Active ? AlarmState.Active : AlarmState.Normal;
|
|
|
|
/// <summary>Projects an <see cref="AlarmLevel"/> onto its wire enum.</summary>
|
|
/// <param name="level">The alarm level to project.</param>
|
|
/// <returns>The wire enum value.</returns>
|
|
public static AlarmLevelDto ToProto(AlarmLevel level) => level switch
|
|
{
|
|
AlarmLevel.None => AlarmLevelDto.None,
|
|
AlarmLevel.Low => AlarmLevelDto.Low,
|
|
AlarmLevel.LowLow => AlarmLevelDto.LowLow,
|
|
AlarmLevel.High => AlarmLevelDto.High,
|
|
AlarmLevel.HighHigh => AlarmLevelDto.HighHigh,
|
|
_ => AlarmLevelDto.Unspecified
|
|
};
|
|
|
|
/// <summary>Rehydrates an <see cref="AlarmLevel"/>; unknown decodes to <c>None</c>.</summary>
|
|
/// <param name="level">The wire enum value.</param>
|
|
/// <returns>The decoded alarm level.</returns>
|
|
public static AlarmLevel FromProto(AlarmLevelDto level) => level switch
|
|
{
|
|
AlarmLevelDto.Low => AlarmLevel.Low,
|
|
AlarmLevelDto.LowLow => AlarmLevel.LowLow,
|
|
AlarmLevelDto.High => AlarmLevel.High,
|
|
AlarmLevelDto.HighHigh => AlarmLevel.HighHigh,
|
|
_ => AlarmLevel.None
|
|
};
|
|
|
|
/// <summary>Projects an <see cref="AlarmKind"/> onto its wire enum.</summary>
|
|
/// <param name="kind">The alarm kind to project.</param>
|
|
/// <returns>The wire enum value.</returns>
|
|
public static AlarmKindDto ToProto(AlarmKind kind) => kind switch
|
|
{
|
|
AlarmKind.Computed => AlarmKindDto.Computed,
|
|
AlarmKind.NativeOpcUa => AlarmKindDto.NativeOpcUa,
|
|
AlarmKind.NativeMxAccess => AlarmKindDto.NativeMxAccess,
|
|
_ => AlarmKindDto.Unspecified
|
|
};
|
|
|
|
/// <summary>Rehydrates an <see cref="AlarmKind"/>; unknown decodes to <c>Computed</c> (the record's own default).</summary>
|
|
/// <param name="kind">The wire enum value.</param>
|
|
/// <returns>The decoded alarm kind.</returns>
|
|
public static AlarmKind FromProto(AlarmKindDto kind) => kind switch
|
|
{
|
|
AlarmKindDto.NativeOpcUa => AlarmKind.NativeOpcUa,
|
|
AlarmKindDto.NativeMxAccess => AlarmKind.NativeMxAccess,
|
|
_ => AlarmKind.Computed
|
|
};
|
|
|
|
/// <summary>Projects an <see cref="AlarmShelveState"/> onto its wire enum.</summary>
|
|
/// <param name="shelve">The shelve state to project.</param>
|
|
/// <returns>The wire enum value.</returns>
|
|
public static AlarmShelveStateDto ToProto(AlarmShelveState shelve) => shelve switch
|
|
{
|
|
AlarmShelveState.Unshelved => AlarmShelveStateDto.Unshelved,
|
|
AlarmShelveState.OneShotShelved => AlarmShelveStateDto.OneShotShelved,
|
|
AlarmShelveState.TimedShelved => AlarmShelveStateDto.TimedShelved,
|
|
AlarmShelveState.PermanentShelved => AlarmShelveStateDto.PermanentShelved,
|
|
_ => AlarmShelveStateDto.Unspecified
|
|
};
|
|
|
|
/// <summary>
|
|
/// Rehydrates an <see cref="AlarmShelveState"/>; unknown decodes to
|
|
/// <c>Unshelved</c> — the same safe default <see cref="AlarmShelveStateCodec"/>
|
|
/// applies on the streaming path.
|
|
/// </summary>
|
|
/// <param name="shelve">The wire enum value.</param>
|
|
/// <returns>The decoded shelve state.</returns>
|
|
public static AlarmShelveState FromProto(AlarmShelveStateDto shelve) => shelve switch
|
|
{
|
|
AlarmShelveStateDto.OneShotShelved => AlarmShelveState.OneShotShelved,
|
|
AlarmShelveStateDto.TimedShelved => AlarmShelveState.TimedShelved,
|
|
AlarmShelveStateDto.PermanentShelved => AlarmShelveState.PermanentShelved,
|
|
_ => AlarmShelveState.Unshelved
|
|
};
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// Primitive helpers
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
private static Timestamp Ts(DateTimeOffset value) => Timestamp.FromDateTimeOffset(value);
|
|
|
|
private static DateTimeOffset Dto(Timestamp? value) =>
|
|
value?.ToDateTimeOffset() ?? default;
|
|
|
|
private static DateTimeOffset? DtoOrNull(Timestamp? value) =>
|
|
value?.ToDateTimeOffset();
|
|
|
|
private static Timestamp TsUtc(DateTime value) => Timestamp.FromDateTime(EnsureUtc(value));
|
|
|
|
private static DateTime DtUtc(Timestamp? value) => value?.ToDateTime() ?? default;
|
|
|
|
private static DateTime EnsureUtc(DateTime value) =>
|
|
value.Kind == DateTimeKind.Utc
|
|
? value
|
|
: DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc);
|
|
|
|
private static string OrEmpty(string? value) => value ?? string.Empty;
|
|
|
|
private static string? NullIfEmpty(string value) => string.IsNullOrEmpty(value) ? null : value;
|
|
|
|
private static string GuidToWire(Guid? value) => value?.ToString("D") ?? string.Empty;
|
|
|
|
private static Guid? GuidFromWire(string value) =>
|
|
Guid.TryParse(value, out var parsed) ? parsed : null;
|
|
|
|
private static TrackedOperationId TrackedId(string value) =>
|
|
TrackedOperationId.TryParse(value, out var parsed) ? parsed : default;
|
|
|
|
private static ArgumentException NotInGroup(object message, SiteCommandGroup group) =>
|
|
new($"'{message.GetType().Name}' is not part of the {group} RPC group.", nameof(message));
|
|
|
|
private static NotSupportedException UnknownCase<TCase>(TCase oneofCase, string envelope)
|
|
where TCase : struct, Enum =>
|
|
new($"{envelope} carries oneof case '{oneofCase}', which this build does not understand. "
|
|
+ "The peer is running a newer contract (message evolution is additive-only).");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Marker reply for the fire-and-forget <c>UnsubscribeDebugView</c> command.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The Akka path Tells the unsubscribe and expects nothing back, but a unary RPC
|
|
/// must answer something. This singleton is what the dispatcher returns and what
|
|
/// <see cref="SiteCommandDtoMapper.FromQueryReply"/> produces, so the two
|
|
/// transports present the same shape; the central transport discards it and keeps
|
|
/// the caller-visible fire-and-forget semantics.
|
|
/// </remarks>
|
|
public sealed record UnsubscribeDebugViewAck
|
|
{
|
|
private UnsubscribeDebugViewAck()
|
|
{
|
|
}
|
|
|
|
/// <summary>The single instance.</summary>
|
|
public static UnsubscribeDebugViewAck Instance { get; } = new();
|
|
}
|