@@ -798,6 +798,7 @@ private static readonly (string Channel, int ElementIndex)[] OmicronInstMagQuali
798798 private double _totalDeltaMicroseconds ;
799799 private long _deltaCount ;
800800 private ushort ? _previousSmpCnt ;
801+ private int _counterVerifiedSampleRateHz ; // never advertised as an on-wire smpRate
801802 private long ? _previousCaptureTicks ;
802803 private string ? _pendingJitterEvidence ;
803804 private string ? _activeMappingSignature ;
@@ -952,7 +953,10 @@ public StreamDetailsModel ToDetails()
952953 SourceMac = SourceMac ,
953954 DestinationMac = DestinationMac ,
954955 VlanText = VlanText ,
955- SmpRateText = SmpRate . HasValue ? $ "{ SmpRate } ({ ResolveSmpModText ( SmpMod ) } )" : "N/A" ,
956+ SmpRateText = SmpRate . HasValue ? $ "{ SmpRate } ({ ResolveSmpModText ( SmpMod ) } )"
957+ : _counterVerifiedSampleRateHz > 0
958+ ? $ "{ _counterVerifiedSampleRateHz } (estimated: smpCnt wrap + host arrival rate, not on-wire)"
959+ : "N/A (optional smpRate absent; awaiting verified wrap)" ,
956960 ConfRevText = ConfRev ? . ToString ( ) ?? "N/A" ,
957961 SampleValueMappingText = MappingProfileName ,
958962 SampleValueCountText = DecodedElementCount > 0 ? $ "{ DecodedElementCount } element(s)" : "0" ,
@@ -1568,8 +1572,17 @@ private bool ObserveTiming(long captureTicks, DateTime captureTimeUtc, ushort sm
15681572 acceptForDisplay = ! sequence . IsError ;
15691573 if ( sequence . IsError )
15701574 {
1575+ // A gap invalidates the evidence used for a prior rate estimate.
1576+ _counterVerifiedSampleRateHz = 0 ;
15711577 ClearScopeAcquisition ( clearPublishedSnapshot : false ) ;
15721578 }
1579+ else if ( ! SmpRate . HasValue && SmpCntWrapIsSupportedRate ( _previousSmpCnt . Value , smpCnt , RecentPacketRatePps ) )
1580+ {
1581+ // Only after a full counter rollover AND independent host-rate screening.
1582+ // No SCL is needed and neither counter nor arrival timestamp is
1583+ // represented as certified synchronization/timing accuracy.
1584+ _counterVerifiedSampleRateHz = _previousSmpCnt . Value + 1 ;
1585+ }
15731586
15741587 var actualUs = ( captureTicks - _previousCaptureTicks . Value ) * 1_000_000.0 / Stopwatch . Frequency ;
15751588 CurrentDeltaMicroseconds = actualUs ;
@@ -1632,10 +1645,20 @@ private bool ObserveTiming(long captureTicks, DateTime captureTimeUtc, ushort sm
16321645
16331646 private double ? ResolveExpectedDeltaUs ( int sampleStep )
16341647 {
1635- if ( SmpRate is > 0 && SmpMod == 1 )
1636- return sampleStep * 1_000_000.0 / SmpRate . Value ;
1648+ var rate = ResolveWaveformSampleRate ( ) ;
1649+ return rate > 0 ? sampleStep * 1_000_000.0 / rate : null ;
1650+ }
16371651
1638- return null ;
1652+ // smpRate/smpMod are optional fields in real IEC 61850-9-2LE ASDU streams.
1653+ // The observed rollover alone is not a clock measurement. Gate narrowly on
1654+ // two supported nominal modes plus a rolling host-arrival packet-rate check.
1655+ private static bool SmpCntWrapIsSupportedRate ( ushort previous , ushort current , double ? observedFps )
1656+ {
1657+ if ( current != 0 || ( previous != 3999 && previous != 4799 ) ||
1658+ ! observedFps . HasValue || ! double . IsFinite ( observedFps . Value ) )
1659+ return false ;
1660+ var expected = previous + 1 ;
1661+ return Math . Abs ( observedFps . Value - expected ) <= expected * 0.10 ;
16391662 }
16401663
16411664 private void AddJitter ( double absJitterUs )
@@ -1814,13 +1837,15 @@ private double ResolveWaveformSampleRate()
18141837 if ( SmpRate is > 0 && SmpMod == 1 )
18151838 return SmpRate . Value ;
18161839
1817- return 0 ;
1840+ return _counterVerifiedSampleRateHz ;
18181841 }
18191842
18201843 private double ResolveNominalFrequency ( )
18211844 {
18221845 var sampleRate = ResolveWaveformSampleRate ( ) ;
1823- return sampleRate >= 4000 ? 50.0 : 0 ;
1846+ if ( sampleRate == 4000 ) return 50.0 ;
1847+ if ( sampleRate == 4800 ) return 60.0 ;
1848+ return 0 ;
18241849 }
18251850
18261851 private int ResolveSamplesPerCycle ( )
0 commit comments