Skip to content
4 changes: 2 additions & 2 deletions Services/SntpClockService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ private async Task StartCoreAsync(SntpNetworkBinding binding, CancellationToken
SetState(
SntpClockServiceState.Serving,
binding.DirectedBroadcast == null
? $"SNTP UDP server active on {binding.LocalAddress}:123 with SIPROTEC compatibility stratum {_profile.Stratum}. No usable directed broadcast is available."
: $"SNTP UDP server active on {binding.LocalAddress}:123 with SIPROTEC compatibility stratum {_profile.Stratum}; Mode 5 broadcast targets {binding.DirectedBroadcast}:123.");
? $"SNTP UDP server active on {binding.LocalAddress}:123 with commissioning compatibility stratum {_profile.Stratum}. No usable directed broadcast is available."
: $"SNTP UDP server active on {binding.LocalAddress}:123 with commissioning compatibility stratum {_profile.Stratum}; Mode 5 broadcast targets {binding.DirectedBroadcast}:123.");

_receiveTask = ReceiveLoopAsync(udp, serviceCancellation.Token);
_broadcastTask = BroadcastLoopAsync(binding, serviceCancellation.Token);
Expand Down
8 changes: 4 additions & 4 deletions Services/SntpPacket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,15 @@ public readonly record struct SntpClientRequest(
public sealed record SntpServerProfile
{
/// <summary>
/// SIPROTEC compatibility advertisement used by ARSAS commissioning Clock Sync.
/// Field experience with SIPROTEC requires a trusted-looking low stratum; stratum 2
/// Commissioning compatibility advertisement used by ARSAS Clock Sync.
/// Field experience with IED requires a trusted-looking low stratum; stratum 2
/// is deliberately used instead of stratum 1 so ARSAS does not claim to be a primary
/// GPS/PTP/atomic reference. ReferenceId remains LOCL and diagnostics state that the
/// laptop clock is a local commissioning source, not a traceable grandmaster.
/// </summary>
public const byte SiprotecCompatibilityStratum = 2;
public const byte CommissioningCompatibilityStratum = 2;

public byte Stratum { get; init; } = SiprotecCompatibilityStratum;
public byte Stratum { get; init; } = CommissioningCompatibilityStratum;
public byte LeapIndicator { get; init; }
public sbyte PollExponent { get; init; } = 6;
public sbyte PrecisionExponent { get; init; } = -10;
Expand Down
10 changes: 5 additions & 5 deletions docs/SNTP_CLOCK_SYNC.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ ARSAS includes a small clean-room SNTPv4 commissioning service for station-bus w
- Sends an immediate SNTPv4 Mode 5 directed broadcast, then repeats every 64 seconds by default when a usable directed-broadcast address exists.
- Sends another immediate broadcast when a newly connected IED is observed.
- Separately records `broadcast sent`, `client request seen`, and `Mode 4 reply sent` evidence.
- Advertises synchronized commissioning packets with SIPROTEC compatibility `stratum 2` and reference ID `LOCL`.
- Advertises synchronized commissioning packets with commissioning compatibility `stratum 2` and reference ID `LOCL`.
- Performs a wall-clock sanity/step check. A large time step suppresses broadcast and makes that instant's unicast reply RFC-style unsynchronized (`LI=3`, `stratum=0`, `INIT`, server timestamps zero).
- Never fails an IEC 61850 association when SNTP cannot start.

Expand All @@ -29,11 +29,11 @@ FAT Clock Sync telemetry intentionally distinguishes packet activity from actual

A broadcast without a client request may still be valid when the relay is explicitly configured for broadcast NTP, but ARSAS does not treat it as an acknowledgement. For unicast SNTP, the strongest wire-level evidence is a Mode 3 request followed by a Mode 4 reply. Device-side time-quality or clock evidence is still required before declaring the relay synchronized.

## SIPROTEC compatibility stratum
## Commissioning compatibility stratum

Field commissioning has shown that a conservative high-stratum local source can be rejected or remain marked unsynchronized on some SIPROTEC installations. ARSAS therefore uses `stratum 2` for both Mode 4 replies and Mode 5 broadcasts.
Authorized field observations on the historical reference configuration showed that a conservative high-stratum local source could be rejected or remain marked unsynchronized. ARSAS therefore uses the same `stratum 2` advertisement for Mode 4 replies and Mode 5 broadcasts. This is a bounded commissioning default, not a claim about all IEDs. The original device-specific naming and observation context remain traceable in the [pre-migration source revision](https://github.com/masarray/arsas/commit/e03ff1caa7d83902ef106f0c4aafdc3fb24143e5).

The value is named in code as `SntpServerProfile.SiprotecCompatibilityStratum` and is protected by regression tests. It does not claim that the Windows laptop is physically traceable to a stratum-1 GNSS/PTP/atomic source. `LOCL` remains the reference ID and ARSAS diagnostics describe the laptop as a local commissioning source.
The value is named in code as `SntpServerProfile.CommissioningCompatibilityStratum` and is protected by regression tests. It does not claim that the Windows laptop is physically traceable to a stratum-1 GNSS/PTP/atomic source. `LOCL` remains the reference ID and ARSAS diagnostics describe the laptop as a local commissioning source.

If the Windows clock fails the ARSAS clock-health guard, synchronized stratum is not advertised: the affected unicast response becomes unsynchronized (`LI=3`, `stratum=0`, `INIT`) and broadcast is suppressed.

Expand Down Expand Up @@ -75,7 +75,7 @@ If another connected IED routes through a different local IPv4 interface, ARSAS
- version/poll field copy behavior;
- Mode 4 reply semantics;
- originate timestamp echo;
- SIPROTEC compatibility stratum 2 on unicast and broadcast packets;
- commissioning compatibility stratum 2 on unicast and broadcast packets;
- Mode 5 broadcast semantics;
- RFC-style unsynchronized response fields;
- directed-broadcast calculation;
Expand Down
2 changes: 1 addition & 1 deletion tests/ARSAS.Tests/SntpEthernetFrameCodecTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void Mode3RawFrame_IsParsed_AndMode4ReplySwapsEndpointsWithValidChecksums

var payload = reply.AsSpan(udpOffset + 8, SntpPacket.MinimumLength);
Assert.Equal(4, payload[0] & 0x07);
Assert.Equal(SntpServerProfile.SiprotecCompatibilityStratum, payload[1]);
Assert.Equal(SntpServerProfile.CommissioningCompatibilityStratum, payload[1]);
Assert.Equal(requestPayload.AsSpan(40, 8).ToArray(), payload.Slice(24, 8).ToArray());
}

Expand Down
8 changes: 4 additions & 4 deletions tests/ARSAS.Tests/SntpPacketTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,24 @@ public void ClientRequest_IsRecognized_AndReplyCopiesVersionPollAndOriginateTime

Assert.Equal(4, reply[0] & 0x07);
Assert.Equal(4, (reply[0] >> 3) & 0x07);
Assert.Equal(SntpServerProfile.SiprotecCompatibilityStratum, reply[1]);
Assert.Equal(SntpServerProfile.CommissioningCompatibilityStratum, reply[1]);
Assert.Equal(9, unchecked((sbyte)reply[2]));
Assert.Equal(request.AsSpan(40, 8).ToArray(), reply.AsSpan(24, 8).ToArray());
Assert.InRange((SntpPacket.ReadTimestamp(reply.AsSpan(32, 8), receive) - receive).Duration(), TimeSpan.Zero, TimeSpan.FromTicks(2));
Assert.InRange((SntpPacket.ReadTimestamp(reply.AsSpan(40, 8), transmit) - transmit).Duration(), TimeSpan.Zero, TimeSpan.FromTicks(2));
}

[Fact]
public void Broadcast_IsMode5_AndUsesSiprotecCompatibilityStratum()
public void Broadcast_IsMode5_AndUsesCommissioningCompatibilityStratum()
{
var now = new DateTimeOffset(2026, 8, 13, 2, 3, 4, TimeSpan.Zero);
var packet = SntpPacket.BuildBroadcast(now, new SntpServerProfile());

Assert.Equal(SntpPacket.MinimumLength, packet.Length);
Assert.Equal(5, packet[0] & 0x07);
Assert.Equal(4, (packet[0] >> 3) & 0x07);
Assert.Equal((byte)2, SntpServerProfile.SiprotecCompatibilityStratum);
Assert.Equal(SntpServerProfile.SiprotecCompatibilityStratum, packet[1]);
Assert.Equal((byte)2, SntpServerProfile.CommissioningCompatibilityStratum);
Assert.Equal(SntpServerProfile.CommissioningCompatibilityStratum, packet[1]);
Assert.Equal(6, unchecked((sbyte)packet[2]));
Assert.Equal("LOCL", System.Text.Encoding.ASCII.GetString(packet, 12, 4));
Assert.InRange((SntpPacket.ReadTimestamp(packet.AsSpan(40, 8), now) - now).Duration(), TimeSpan.Zero, TimeSpan.FromTicks(2));
Expand Down
Loading