From 02e30509d92743c88d3616417d22156ab5130c94 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 15:30:55 +0700 Subject: [PATCH 1/6] fix(ptp-m3): bound software phase tracking by observation uncertainty --- docs/PTP_SV_CONTINUOUS_EPOCH_BINDING_M3C.md | 20 ++++++++++ .../time_sync/sv_ptp_phase_authority.hpp | 9 ++++- tests/test_sv_ptp_phase_authority.cpp | 37 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/docs/PTP_SV_CONTINUOUS_EPOCH_BINDING_M3C.md b/docs/PTP_SV_CONTINUOUS_EPOCH_BINDING_M3C.md index 2be984391..e3ac4d8f8 100644 --- a/docs/PTP_SV_CONTINUOUS_EPOCH_BINDING_M3C.md +++ b/docs/PTP_SV_CONTINUOUS_EPOCH_BINDING_M3C.md @@ -15,3 +15,23 @@ The existing ESP32-P4 SOURCE control-plane observer now publishes ISR phase-stee ## Scope and next gating This is M3C **foundation**, NOT completion of the M3A canonical dual-mode runtime authority. It currently guards the opt-in SOURCE servo; RECEIVER phase correlation, clock-authority selection/switching, absolute PPS/smpCnt physical accuracy, MAC egress, non-default SV profile epoch semantics, full M3 recovery and legitimate AUTO smpSynch remain separate engineering gates. The firmware experiment stays default OFF and native ARStack Studio is unchanged. A successful build/test establishes software verification only; hardware/IED acceptance remains unverified. + +## M3C.3 — Bounded confidence on software phase tracking (2026-10-17) + +The previous software-tracking decision checked only the nominal GPTimer/PTP +phase residual against the configured threshold. It could report +`software_tracking=true` after consecutive 400-µs clock-read brackets even +though the corresponding measurement uncertainty was at least 201 µs, +larger than the default 60-µs tracking threshold. + +The qualification counter now advances only when +`abs(measured_phase_error_us) + observation_uncertainty_us` is within the +configured software-tracking threshold. One low-confidence observation revokes +tracking. Integer-second continuity, the separate phase-observation acceptance +limit, bounded PI steering, ISR work and firmware feature gates are unchanged. +This is a software confidence bound, not a hardware egress measurement or +authorization to assert `smpSynch=1/2`. + +Portable regression includes six accurate-phase/uncertain-clock observations, +four fresh narrowly bracketed reacquisition observations, and confidence +demotion after one degraded bracket. Hardware bench acceptance is still absent. diff --git a/include/ariec61850/time_sync/sv_ptp_phase_authority.hpp b/include/ariec61850/time_sync/sv_ptp_phase_authority.hpp index b67e46841..c0104615f 100644 --- a/include/ariec61850/time_sync/sv_ptp_phase_authority.hpp +++ b/include/ariec61850/time_sync/sv_ptp_phase_authority.hpp @@ -182,7 +182,14 @@ class SvPtpPhaseAuthority final { const bool first_observation = !have_previous_; have_previous_ = true; - if (sv_phase_detail::magnitude(phase) <= + // A small nominal phase residual is NOT sufficiently qualified + // when the GPTimer/PTP read bracket is wider than the tracking + // tolerance. Include the full bracket uncertainty before advancing + // consecutive software-tracking samples (never wire smpSynch). + const std::int64_t worst_case_phase_us = + sv_phase_detail::magnitude(phase) + + static_cast(uncertainty); + if (worst_case_phase_us <= policy_.software_tracking_threshold_us) { if (tracking_samples_ < policy_.consecutive_tracking_samples) { ++tracking_samples_; diff --git a/tests/test_sv_ptp_phase_authority.cpp b/tests/test_sv_ptp_phase_authority.cpp index 2c77a912a..99805e75d 100644 --- a/tests/test_sv_ptp_phase_authority.cpp +++ b/tests/test_sv_ptp_phase_authority.cpp @@ -141,6 +141,42 @@ void check_software_servo_tracks_80ppm_drift_without_counter_reset() { } } + +void check_uncertain_brackets_never_claim_software_tracking() { + SvPtpPhaseAuthority tracker; + // Valid control-plane reads may be too uncertain to support a 60 us + // tracking claim. A 400 us PTP read bracket has >=201 us uncertainty; + // the nominal phase is deliberately zero to catch false positives. + for (std::uint32_t epoch = 1U; epoch <= 6U; ++epoch) { + const auto decision = tracker.observe(snapshot(epoch, 20, 400U)); + CHECK(decision.accepted); + CHECK(decision.epoch_second_continuous == (epoch != 1U)); + CHECK(decision.phase_error_us == 0); + CHECK(decision.observation_uncertainty_us >= 201U); + CHECK(decision.state == SvPtpPhaseState::acquiring); + CHECK(!decision.software_tracking); + } + + // Qualification requires four fresh, narrow observations; old + // uncertain observations cannot contribute toward this threshold. + for (std::uint32_t epoch = 7U; epoch <= 9U; ++epoch) { + const auto decision = tracker.observe(snapshot(epoch, 20, 8U)); + CHECK(decision.accepted); + CHECK(!decision.software_tracking); + } + const auto qualified = tracker.observe(snapshot(10U, 20, 8U)); + CHECK(qualified.accepted); + CHECK(qualified.software_tracking); + CHECK(qualified.state == SvPtpPhaseState::tracking); + + // One uncertain sample revokes tracking, even with perfect measured + // phase. The PI/slew remains bounded separately from qualification. + const auto degraded = tracker.observe(snapshot(11U, 20, 400U)); + CHECK(degraded.accepted); + CHECK(!degraded.software_tracking); + CHECK(degraded.state == SvPtpPhaseState::acquiring); +} + void check_publisher_continuity_gate_reacquisition() { SvPtpPublisherContinuityGate gate; SvPtpPhaseAuthority phase; @@ -319,6 +355,7 @@ void check_continuous_ptp_second_binding() { int main() { try { check_phase_sign_and_bounded_corrections(); + check_uncertain_brackets_never_claim_software_tracking(); check_per_second_slew_distribution(); check_software_servo_tracks_80ppm_drift_without_counter_reset(); check_clock_step_stale_brackets_and_timer_wrap(); From 13817466d718d5c6ffae860baaacabef6e16dc66 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 15:33:01 +0700 Subject: [PATCH 2/6] test(ptp): avoid MSVC optional VLAN narrowing under warnings-as-errors --- tests/test_ptp_power_profile.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_ptp_power_profile.cpp b/tests/test_ptp_power_profile.cpp index 8c3cc825f..a951251f0 100644 --- a/tests/test_ptp_power_profile.cpp +++ b/tests/test_ptp_power_profile.cpp @@ -281,8 +281,8 @@ void check_tai_seed_and_truthful_time_properties() { void check_vlan_encoding_remains_configurable() { auto options = make_source(); - options.vlan_id = 100U; - options.vlan_priority = 4U; + options.vlan_id = std::uint16_t{100U}; + options.vlan_priority = std::uint8_t{4U}; PtpPublisherRuntime source{options}; REQUIRE(source.start()); const auto announce = source.prepare_announce(PtpTimestamp{1000U, 0U}); From 2732029048dc2686b6d88e5b68d9dffb42b59c9f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 15:34:39 +0700 Subject: [PATCH 3/6] fix(esp32p4): enforce zero logging after PTP-aware sample clock START --- .github/workflows/esp32p4-smv-injector.yml | 7 ++++--- embedded/esp32p4_smv_injector/main/app_main.cpp | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/esp32p4-smv-injector.yml b/.github/workflows/esp32p4-smv-injector.yml index b8a5e3744..c1ccd29db 100644 --- a/.github/workflows/esp32p4-smv-injector.yml +++ b/.github/workflows/esp32p4-smv-injector.yml @@ -101,9 +101,10 @@ jobs: start_begin = source.index('if (take_start_request())') start_end = source.index('if (!live_tx_running())', start_begin) start_block = source[start_begin:start_end] - clock_start = start_block.rindex( - 'sample_clock = start_sample_clock(task_handle, active_profile.publisher_rate_hz);' - ) + # Match the call site, not an obsolete fixed argument list. The + # SOURCE/RECEIVER experimental arm passes ETH/PTP epoch metadata. + # Never relax the no-console-after-arming invariant. + clock_start = start_block.rindex('sample_clock = start_sample_clock(') assert start_block.rindex('ESP_LOGI') < clock_start assert 'ESP_LOG' not in start_block[clock_start:] assert 'static_assert(std::atomic::is_always_lock_free);' in source diff --git a/embedded/esp32p4_smv_injector/main/app_main.cpp b/embedded/esp32p4_smv_injector/main/app_main.cpp index 947089bad..1ec72b4fa 100644 --- a/embedded/esp32p4_smv_injector/main/app_main.cpp +++ b/embedded/esp32p4_smv_injector/main/app_main.cpp @@ -785,9 +785,11 @@ void publisher_task(void* argument) { // Legacy first alarm already consumes the first rational tick; // a PTP-armed first alarm is an independent epoch delay. static_cast(expected_schedule.next_ticks()); - } else { - ESP_LOGI(kTag, "PTP epoch START armed; SV sync remains unqualified (smpSynch AUTO=0)"); } + // Do not log after the timer is enabled: even a single slow + // console write can accumulate ISR notifications before the + // publisher consumes its first slot (false START misses). + // This does not change fail-closed smpSynch AUTO=0. continue; } From c776e7b0f324e0bb9dae4e578495448ff00fdbdb Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 15:35:54 +0700 Subject: [PATCH 4/6] ci(ptp): build all M3 portable test binaries before Windows CTest --- .github/workflows/ptp-core-ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ptp-core-ci.yml b/.github/workflows/ptp-core-ci.yml index 3820d14e6..b9e513c21 100644 --- a/.github/workflows/ptp-core-ci.yml +++ b/.github/workflows/ptp-core-ci.yml @@ -128,6 +128,13 @@ jobs: Build-Target 'smp_synch_policy_tests' Build-Target 'smp_synch_wire_offset_tests' Build-Target 'ptp_discipline_tests' + # Keep explicit MSVC targets in sync with the canonical CTest + # registrations; CTest otherwise fails with missing M3 binaries. + Build-Target 'ptp_control_cancellation_tests' + Build-Target 'ptp_emac_role_ownership_tests' + Build-Target 'ptp_canonical_authority_tests' + Build-Target 'ptp_receiver_clock_authority_tests' + Build-Target 'sv_ptp_clock_lease_tests' Build-Target 'sv_ptp_epoch_tests' Build-Target 'sv_ptp_phase_authority_tests' Build-Target 'sv_tx_egress_evidence_tests' From 60892ce2ec7742ae73e533d687f95ae6775bb6e3 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 15:38:33 +0700 Subject: [PATCH 5/6] ci(ptp): assert freshness-qualified SOURCE readiness instead of stale atomic read --- .github/workflows/esp32p4-ptp-lab.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/esp32p4-ptp-lab.yml b/.github/workflows/esp32p4-ptp-lab.yml index 61b817436..8292f2947 100644 --- a/.github/workflows/esp32p4-ptp-lab.yml +++ b/.github/workflows/esp32p4-ptp-lab.yml @@ -182,7 +182,9 @@ jobs: 'g_announce_sent.load', 'g_sync_sent.load', 'g_follow_up_sent.load', - 'return g_ptp_ready.load', + '[[nodiscard]] bool source_has_fresh_evidence() noexcept', + 'PtpSourceEvidenceFreshness::fresh(', + 'return source_has_fresh_evidence();', ] combined = eth + '\n' + ptp missing = [token for token in required if token not in combined] From 0dc0599dec9342acbd14f0a1119e4a9c86cebc5f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 17 Oct 2026 15:40:38 +0700 Subject: [PATCH 6/6] ci(ptp): repair PTP role policy workflow YAML script indentation --- .github/workflows/esp32p4-ptp-lab.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/esp32p4-ptp-lab.yml b/.github/workflows/esp32p4-ptp-lab.yml index 8292f2947..1f04ab1db 100644 --- a/.github/workflows/esp32p4-ptp-lab.yml +++ b/.github/workflows/esp32p4-ptp-lab.yml @@ -183,8 +183,8 @@ jobs: 'g_sync_sent.load', 'g_follow_up_sent.load', '[[nodiscard]] bool source_has_fresh_evidence() noexcept', - 'PtpSourceEvidenceFreshness::fresh(', - 'return source_has_fresh_evidence();', + 'PtpSourceEvidenceFreshness::fresh(', + 'return source_has_fresh_evidence();', ] combined = eth + '\n' + ptp missing = [token for token in required if token not in combined]