Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3ae97de
fix(m3d): preserve SOURCE bench PTP epoch across same-role Advanced P…
masarray Oct 23, 2026
843dea2
fix(m3d): expose board-certified bench GM readiness to prevent false …
masarray Oct 23, 2026
e306420
fix(studio): track requested bench GM and measured PTP readiness per …
masarray Oct 23, 2026
2e67376
fix(studio): one-time verified PTP SOURCE bench commissioning, no sil…
masarray Oct 23, 2026
b3137ab
fix(studio): enable explicit bench GM commissioning even when boot LO…
masarray Oct 23, 2026
baf238d
fix(sv): bounded warm SOURCE sync continuation policy for unchanged P…
masarray Oct 23, 2026
a5bc32d
fix(sv): seven-epoch fail-closed SOURCE warm resume after STOP with s…
masarray Oct 23, 2026
3ae6863
test(sv): verify bounded warm relay-bench source qualification on STO…
masarray Oct 23, 2026
328341f
test(studio): reject silent boot-LOCAL adoption, verify bench commiss…
masarray Oct 23, 2026
664c663
ci(p0): enable exact-head Draft SOURCE bench + STOP/START regression …
masarray Oct 23, 2026
1e7906e
ci(p0): enable exact-head Draft SOURCE bench + STOP/START regression …
masarray Oct 23, 2026
47faac9
ci(policy): restrict P0 bench GM hardware preview exception to exact …
masarray Oct 23, 2026
9d24588
feat(bench): opt-in guard prevents SV START against unseeded LOCAL PT…
masarray Oct 23, 2026
c490031
config(bench): require verified internal PTP bench GM before SV outpu…
masarray Oct 23, 2026
e79db70
fix(bench): reject SOURCE SV injection until real seeded PTP GM ready…
masarray Oct 23, 2026
54ed0e7
ci(p0): ensure matched FAT BIN actually compiles global-bench SOURCE …
masarray Oct 23, 2026
1850d33
test(studio): enforce FAT SOURCE bench global prerequisite and explan…
masarray Oct 23, 2026
09427cd
fix(bench): fail closed on missing PTP profile as well as unseeded lo…
masarray Oct 23, 2026
e427850
fix(bench): report measured SOURCE runtime separately from configured…
masarray Oct 23, 2026
0742c73
fix(studio): use active SOURCE generation in PTPGM preflight, not sav…
masarray Oct 23, 2026
c0b025f
test(bench): require runtime SOURCE proof before commissioning branch
masarray Oct 23, 2026
ef18db1
fix(compile): bound warm PTP SOURCE context to phase-enabled ESP32-P4…
masarray Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/esp32p4-smv-injector.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
# On draft consolidation, do not run ordinary expensive standalone builds.
# The Studio Native Preview explicitly requests a paired firmware via
# workflow_call phase_sync_preview=true; allow only that purposeful build.
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft || (inputs.phase_sync_preview == true && (github.event.pull_request.head.ref == 'feat/ptp-source-policy-fsm-20261022' || github.event.pull_request.head.ref == 'fix/autolive-from-pcap-proven-7c95-20261023' || github.event.pull_request.head.ref == 'feat/m3a-persistent-sv-grid-20261023' || github.event.pull_request.head.ref == 'feat/m3b-boot-timebase-supervisor-20261023' || github.event.pull_request.head.ref == 'feat/m3c-safe-external-gm-handover-20261023' || github.event.pull_request.head.ref == 'feat/m3d-ptp-advanced-policy-diagnostics-20261024')) || github.event.pull_request.head.ref == 'feat/m3a-persistent-sv-grid-20261023' || github.event.pull_request.head.ref == 'feat/m3b-boot-timebase-supervisor-20261023' || github.event.pull_request.head.ref == 'feat/m3c-safe-external-gm-handover-20261023' || github.event.pull_request.head.ref == 'feat/m3d-ptp-advanced-policy-diagnostics-20261024' }}
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft || (inputs.phase_sync_preview == true && (github.event.pull_request.head.ref == 'feat/ptp-source-policy-fsm-20261022' || github.event.pull_request.head.ref == 'fix/autolive-from-pcap-proven-7c95-20261023' || github.event.pull_request.head.ref == 'feat/m3a-persistent-sv-grid-20261023' || github.event.pull_request.head.ref == 'feat/m3b-boot-timebase-supervisor-20261023' || github.event.pull_request.head.ref == 'feat/m3c-safe-external-gm-handover-20261023' || github.event.pull_request.head.ref == 'feat/m3d-ptp-advanced-policy-diagnostics-20261024' || github.event.pull_request.head.ref == 'fix/m3d-relay-bench-global-ptp-persistent-20261024')) || github.event.pull_request.head.ref == 'feat/m3a-persistent-sv-grid-20261023' || github.event.pull_request.head.ref == 'feat/m3b-boot-timebase-supervisor-20261023' || github.event.pull_request.head.ref == 'feat/m3c-safe-external-gm-handover-20261023' || github.event.pull_request.head.ref == 'feat/m3d-ptp-advanced-policy-diagnostics-20261024' || github.event.pull_request.head.ref == 'fix/m3d-relay-bench-global-ptp-persistent-20261024' }}
runs-on: ubuntu-latest
timeout-minutes: 25
env:
Expand Down Expand Up @@ -158,6 +158,7 @@ jobs:
# M3.B must really be linked into the exact shipped ESP32-P4 binary.
# A disabled dependency would otherwise silently negate sdkconfig.defaults.
grep -qx 'CONFIG_AR_PTP_BOOT_SOURCE_AUTOSTART=y' sdkconfig
grep -qx 'CONFIG_AR_SMV_RELAY_BENCH_GLOBAL_GUARD=y' sdkconfig
grep -qx 'CONFIG_AR_PTP_AUTO_PREFER_EXTERNAL=y' sdkconfig
grep -qx 'CONFIG_AR_PTP_AUTO_LOCAL_FALLBACK=y' sdkconfig
grep -qx 'CONFIG_AR_SMV_HW_TX_EVIDENCE_EXPERIMENTAL=y' sdkconfig
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ptp-core-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ concurrency:
jobs:
portable-ptp:
# Exactly this isolated Draft M3.A runs GCC/Clang phase regression.
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft || (github.event.pull_request.head.ref == 'feat/m3a-persistent-sv-grid-20261023' || github.event.pull_request.head.ref == 'feat/m3b-boot-timebase-supervisor-20261023' || github.event.pull_request.head.ref == 'feat/m3c-safe-external-gm-handover-20261023' || github.event.pull_request.head.ref == 'feat/m3d-ptp-advanced-policy-diagnostics-20261024') }}
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft || (github.event.pull_request.head.ref == 'feat/m3a-persistent-sv-grid-20261023' || github.event.pull_request.head.ref == 'feat/m3b-boot-timebase-supervisor-20261023' || github.event.pull_request.head.ref == 'feat/m3c-safe-external-gm-handover-20261023' || github.event.pull_request.head.ref == 'feat/m3d-ptp-advanced-policy-diagnostics-20261024' || github.event.pull_request.head.ref == 'fix/m3d-relay-bench-global-ptp-persistent-20261024') }}
strategy:
fail-fast: false
matrix:
Expand Down
13 changes: 11 additions & 2 deletions apps/arstack_studio/qml/PtpExpertPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,11 @@ SurfacePanel {
}

function startRole(role) {
const commissioningBench = role === "SOURCE" && benchGmCheck.checked &&
!panel.device.running && !panel.startPending
if (!panel.session || !panel.device.ptpAvailable || !panel.device.deviceVerified
|| panel.device.ptpRunning || panel.startPending || panel.session.updatingFirmware)
|| (panel.device.ptpRunning && !commissioningBench) ||
panel.startPending || panel.session.updatingFirmware)
return false
const accepted = panel.session.requestStartPtpConfigured(role, panel.ptpProfile(role))
if (!accepted && panel.controller)
Expand Down Expand Up @@ -431,9 +434,15 @@ SurfacePanel {
tone: "accent"
text: panel.stopPending ? "Stop pending…"
: panel.startPending ? "Starting…"
: modeCombo.currentIndex === 0 && panel.device.ptpRunning &&
benchGmCheck.checked ? "Verify / Commission Bench GM"
: modeCombo.currentIndex === 0 ? "Start PTP Source"
: modeCombo.currentIndex === 1 ? "Start PTP Receiver" : "Start Monitor"
enabled: panel.session && panel.device.ptpAvailable && !panel.device.ptpRunning && !panel.startPending && !panel.session.updatingFirmware
enabled: panel.session && panel.device.ptpAvailable &&
(!panel.device.ptpRunning ||
(modeCombo.currentIndex === 0 && benchGmCheck.checked &&
!panel.device.running)) &&
!panel.startPending && !panel.session.updatingFirmware
onClicked: panel.startSelectedMode()
}
CalmButton {
Expand Down
81 changes: 75 additions & 6 deletions apps/arstack_studio/src/DeviceController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ const QRegularExpression kIdentityExpression{
"(?:build=([A-Fa-f0-9]{16}) )?"
"(?:boot_id=([A-Fa-f0-9]{16}) )?capabilities=([A-Z0-9_,.\\-]+)"),
QRegularExpression::CaseInsensitiveOption};
const QRegularExpression kPtpBenchReadyExpression{
QStringLiteral("PTPGM bench_configured=([01]) bench_ready=([01]) source_active=([01])"),
QRegularExpression::CaseInsensitiveOption};
const QRegularExpression kPtpStatusExpression{
QStringLiteral("PTP status=(RUNNING|STOPPED) Announce=(\\d+) Sync=(\\d+) FollowUp=(\\d+) PdelayFrames=(\\d+) TXfail=(\\d+)"),
QRegularExpression::CaseInsensitiveOption};
Expand Down Expand Up @@ -1441,6 +1444,8 @@ void DeviceController::resetPtpStartup() noexcept {
ptpStartStage_ = PtpStartStage::idle;
pendingPtpRole_.clear();
pendingPtpConfig_.clear();
pendingPtpBenchRequested_ = false;
ptpPreflightBenchReady_ = false;
ptpRuntimeRoleSeen_ = false;
ptpRuntimeRunningSeen_ = false;
}
Expand All @@ -1451,6 +1456,8 @@ bool DeviceController::beginPtpStopVerification() {
ptpStartStage_ = PtpStartStage::awaiting_stop_ack;
pendingPtpRole_.clear();
pendingPtpConfig_.clear();
pendingPtpBenchRequested_ = false;
ptpPreflightBenchReady_ = false;
ptpRuntimeRoleSeen_ = false;
ptpRuntimeRunningSeen_ = false;
ptpStatus_ = QStringLiteral("Stopping PTP; waiting for board confirmation...");
Expand Down Expand Up @@ -1495,8 +1502,14 @@ void DeviceController::failPtpStartup(const QString& reason, const bool rollback
bool DeviceController::startPtpConfigured(const QString& requestedRole,
const QVariantMap& profile) {
const QString role = requestedRole.trimmed().toUpper();
const bool benchRequested = profile.value(QStringLiteral("benchGlobalGm")).toBool();
// Boot PTP SOURCE is intentionally conservative before a Studio UTC
// seed arrives. An *explicit* bench START may commission it once with
// a verified stop/reconfigure, never on a routine SV STOP/START.
const bool mayCommission = role == QStringLiteral("SOURCE") &&
benchRequested && !running_ && scenarioStage_ == ScenarioStage::idle;
if (!deviceVerified_ || !connected_ || !ioWorkerReady_ || !ptpAvailable_ ||
ptpRunning_ || ptpStarting() ||
(ptpRunning_ && !mayCommission) || ptpStarting() ||
(role != QStringLiteral("SOURCE") &&
role != QStringLiteral("RECEIVER") &&
role != QStringLiteral("MONITOR"))) {
Expand Down Expand Up @@ -1532,6 +1545,8 @@ bool DeviceController::startPtpConfigured(const QString& requestedRole,
ptpLastStartFailure_.clear();
pendingPtpRole_ = role;
pendingPtpConfig_ = command;
pendingPtpBenchRequested_ = benchRequested;
ptpPreflightBenchReady_ = false;
// Verify actual firmware runtime before selecting another role. A stale
// local OFF flag after reconnect must not provoke a rejected PTPROLE or
// force-stop a transmitter still running on the board.
Expand All @@ -1549,6 +1564,15 @@ bool DeviceController::startPtpConfigured(const QString& requestedRole,
void DeviceController::advancePtpStartup(const QString& line) {
if (!ptpStarting() || ptpStartStage_ == PtpStartStage::stop_unconfirmed) return;
if (ptpStartStage_ == PtpStartStage::awaiting_preflight) {
const auto benchProof = kPtpBenchReadyExpression.match(line);
if (benchProof.hasMatch()) {
ptpPreflightBenchReady_ = benchProof.captured(2) == QStringLiteral("1");
// This is live SOURCE generation, not merely a saved role. It
// arrives before the authoritative PTP RUNNING line in SHOW.
if (pendingPtpRole_ == QStringLiteral("SOURCE") &&
benchProof.captured(3) == QStringLiteral("1"))
ptpRuntimeRoleSeen_ = true;
}
// PTP SHOW provides measured PTP2 role before status. Only adopt an
// already-active transmitter if THIS preflight saw the expected role;
// historical UI role cannot authorize mutation or claim an exact role.
Expand All @@ -1559,11 +1583,33 @@ void DeviceController::advancePtpStartup(const QString& line) {
if (!actual.hasMatch()) return;
if (actual.captured(1).compare(QStringLiteral("RUNNING"), Qt::CaseInsensitive) == 0) {
if (ptpRuntimeRoleSeen_) {
if (pendingPtpBenchRequested_ && !ptpPreflightBenchReady_) {
if (running_ || scenarioStage_ != ScenarioStage::idle) {
failPtpStartup(QStringLiteral(
"Cannot commission bench GM while SV is running. STOP SV first."), false);
return;
}
// The operator specifically requested bench-GM START.
// Replace the early LOCAL SOURCE with a UTC-seeded GM
// ONCE, not on any subsequent SV injection restart.
ptpStartStage_ = PtpStartStage::awaiting_stop_ack;
ptpStatus_ = QStringLiteral(
"Commissioning bench GM: verifying PTP STOP before UTC reseed...");
emit ptpStateChanged();
if (!sendCommand(QStringLiteral("PTP STOP"))) {
markPtpStopUnconfirmed(QStringLiteral(
"Could not queue PTP STOP for bench GM commissioning."));
return;
}
ptpStartTimer_.start(5000);
return;
}
const QString existingRole = pendingPtpRole_;
resetPtpStartup();
ptpStatus_ = QStringLiteral("PTP %1 already active on board; external sync unqualified").arg(existingRole);
ptpStatus_ = QStringLiteral(
"PTP %1 already active with verified requested timing profile.").arg(existingRole);
emit ptpStateChanged();
return; // No redundant role/config/start, no surprise STOP.
return;
}
ptpLastStartFailure_ = QStringLiteral(
"Board PTP is already RUNNING. Click Stop PTP, wait for verified STOPPED, then retry Start.");
Expand Down Expand Up @@ -1608,10 +1654,33 @@ void DeviceController::advancePtpStartup(const QString& line) {
// still deferred. It cannot verify the requested STOP.
if (!readback.hasMatch() || deferredPtpShow_) return;
if (readback.captured(1).compare(QStringLiteral("STOPPED"), Qt::CaseInsensitive) == 0) {
resetPtpStartup();
ptpRunning_ = false;
ptpStatus_ = QStringLiteral("PTP stopped — confirmed by board");
emit ptpStateChanged();
if (pendingPtpBenchRequested_ && !pendingPtpConfig_.isEmpty() &&
pendingPtpRole_ == QStringLiteral("SOURCE")) {
// UTC must be sampled after STOP confirmation: never seed
// from a host timestamp already stale in the serial queue.
const int marker = pendingPtpConfig_.lastIndexOf(QStringLiteral(" 1 "));
if (marker < 0) {
failPtpStartup(QStringLiteral("Bench GM UTC seed missing."), false);
return;
}
pendingPtpConfig_ = pendingPtpConfig_.left(marker) +
QStringLiteral(" 1 %1").arg(QDateTime::currentMSecsSinceEpoch());
ptpStartStage_ = PtpStartStage::awaiting_role_ack;
ptpStatus_ = QStringLiteral(
"PTP STOPPED; selecting SOURCE with a fresh bench UTC epoch...");
emit ptpStateChanged();
if (!sendCommand(QStringLiteral("PROFILE PTPROLE SOURCE"))) {
failPtpStartup(QStringLiteral(
"Could not select PTP SOURCE after verified STOP."), false);
return;
}
ptpStartTimer_.start(5000);
} else {
resetPtpStartup();
ptpStatus_ = QStringLiteral("PTP stopped — confirmed by board");
emit ptpStateChanged();
}
} else if (!ptpStopPollTimer_.isActive()) {
// Firmware stop is asynchronous. Poll on Qt timers without
// extending the absolute five-second verification deadline.
Expand Down
2 changes: 2 additions & 0 deletions apps/arstack_studio/src/DeviceController.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,8 @@ class DeviceController : public QObject {
QTimer logUpdateTimer_;
QString pendingPtpRole_;
QString pendingPtpConfig_;
bool pendingPtpBenchRequested_{false};
bool ptpPreflightBenchReady_{false};
bool ptpRuntimeRoleSeen_{false};
bool ptpRuntimeRunningSeen_{false};
bool ioWorkerReady_{false};
Expand Down
22 changes: 22 additions & 0 deletions apps/arstack_studio/tests/check_operator_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,28 @@ def main() -> None:
and "panel.session.requestPtpAdvancedPolicy(mode)" in expert,
"M3.D Advanced PTP policy must be firmware-enforced, board-readback and session-owned")

# Relay bench regression: boot AUTO SOURCE can be LOCAL until the
# operator's host UTC epoch is applied. Studio must never silently
# adopt that running SOURCE as if it were a seeded GM, nor kill PTP on
# an ordinary SV STOP/START.
require("PTPGM bench_configured=%u bench_ready=%u source_active=%u" in live_control
and "START rejected: BENCH_GM_NOT_READY" in live_control
and "CONFIG_AR_SMV_RELAY_BENCH_GLOBAL_GUARD=y" in
(project_root / "embedded/esp32p4_smv_injector/sdkconfig.defaults").read_text(encoding="utf-8")
and "kPtpBenchReadyExpression" in device_cpp
and "pendingPtpBenchRequested_" in device_cpp
and "PTP STOPPED; selecting SOURCE with a fresh bench UTC epoch" in device_cpp
and "QDateTime::currentMSecsSinceEpoch()" in device_cpp
and "Verify / Commission Bench GM" in expert
and "panel.device.ptpRunning && !commissioningBench" in expert
and "SvWarmSourceResumePolicy::may_preserve" in firmware_source
and "warm_source_resume_epoch" in firmware_source
and "expected_new_baseline" in firmware_source
and "g_sample_clock_context.launch_on_next_epoch.store(" in firmware_source
and "bench_global_gm = false;" in ptp_facade,
"Boot LOCAL PTP must be explicitly UTC commissioned, and healthy "
"STOP/START must preserve verified SOURCE phase without faking a cold MAC lock")

print("Studio operator workspace contract: PASS")


Expand Down
12 changes: 12 additions & 0 deletions embedded/esp32p4_smv_injector/main/Kconfig.projbuild
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ config AR_SMV_HW_TX_EVIDENCE_EXPERIMENTAL
unavailable, never substituted with esp_timer time or claimed as
proof that a relay has synchronized. Defaults OFF.

config AR_SMV_RELAY_BENCH_GLOBAL_GUARD
bool "Lab FAT: require seeded internal PTP GM before SOURCE SV injection"
default n
depends on AR_PTP_LAB_TX && AR_SMV_PTP_PHASE_SERVO_EXPERIMENTAL
help
In the isolated relay-test profile, refuse SV START while the local
PTP SOURCE has not passed the verified Studio UTC-seed/hardware
Grandmaster boot. This is safer than publishing LOCAL smpSynch=1 and
letting the relay silently reject the data. External PTP RECEIVER
remains governed by its independently measured clock discipline.
No change to the real PTP/SMV packet encoders.

config AR_PTP_BOOT_SOURCE_AUTOSTART
bool "LAB ONLY: start local PTP SOURCE and SV dry sample grid on boot"
default n
Expand Down
Loading
Loading