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
33db85b
feat(m3d): typed fail-closed four-mode operator time source policy
masarray Oct 23, 2026
b9adb6e
feat(m3d): firmware ABI for checked runtime PTP policy and mode readback
masarray Oct 23, 2026
a3b6c20
feat(m3d): atomic four-mode PTP policy transaction with single EMAC o…
masarray Oct 23, 2026
28a5a6d
feat(m3d): verified serial PTP POLICY command, safe rejection, and li…
masarray Oct 23, 2026
f65c518
feat(m3d): background timebase supervisor follows selected four-mode …
masarray Oct 23, 2026
a22c9fa
fix(m3d): source policy never reuses an old bench-GNSS epoch claim
masarray Oct 23, 2026
305c4f8
feat(m3d): strongly typed Studio properties for board PTP policy/stat…
masarray Oct 23, 2026
8818154
feat(m3d): expose guided advanced time-source selection to QML
masarray Oct 23, 2026
4b7e3a4
feat(m3d): session gate denies PTP mode mutation during SV/clock/firm…
masarray Oct 23, 2026
6fe5398
feat(m3d): validate policy command and parse actual ESP32 PTPPOLICY r…
masarray Oct 23, 2026
db0b716
feat(m3d): board capability handshake identifies real policy control …
masarray Oct 23, 2026
89971f2
feat(m3d): advanced PTP policy selector with verified board state and…
masarray Oct 23, 2026
822f369
test(m3d): pure four-mode policy matrix rejects unsafe GM promotion a…
masarray Oct 23, 2026
4a45ab6
feat(m3d): generation fence for boot supervisor policy changes
masarray Oct 23, 2026
7157f13
fix(m3d): generation-tag policy updates prevent stale ready_once latc…
masarray Oct 23, 2026
cf3c46e
fix(m3d): retarget dry-grid supervisor once per verified stopped poli…
masarray Oct 23, 2026
915f9e9
ci(m3d): restrict portable PTP and ESP firmware Draft builds to exact…
masarray Oct 23, 2026
f0ce256
ci(m3d): restrict portable PTP and ESP firmware Draft builds to exact…
masarray Oct 23, 2026
8a5562b
ci(policy): prove scoped M3.D Draft firmware/PTP exceptions do not wi…
masarray Oct 23, 2026
9a3ceac
test(m3d): structural end-to-end firmware policy handshake and Studio…
masarray Oct 23, 2026
da0f0b5
fix(m3d): enforce SV TX STOP in the firmware EMAC-policy transaction …
masarray Oct 23, 2026
3a33575
fix(m3d): surface actual firmware policy rejects and force board read…
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
2 changes: 1 addition & 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/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' }}
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' }}
runs-on: ubuntu-latest
timeout-minutes: 25
env:
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') }}
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') }}
strategy:
fail-fast: false
matrix:
Expand Down
89 changes: 88 additions & 1 deletion apps/arstack_studio/qml/PtpExpertPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ SurfacePanel {
property string monoFont: "Inter"
property var roleValues: ["SOURCE", "RECEIVER", "MONITOR"]
property bool advancedVisible: false
property int policyDraftIndex: -1
readonly property var ptpPolicyModes: [
"INTERNAL_GM_ONLY", "AUTO_PREFER_EXTERNAL",
"EXTERNAL_ONLY", "MONITOR_ONLY"
]
function ptpPolicyIndex(mode) {
const index = panel.ptpPolicyModes.indexOf(mode)
return index >= 0 ? index : 0
}
readonly property bool canConfigurePolicy: panel.session &&
panel.device && panel.device.deviceVerified &&
panel.device.ptpAvailable && !panel.device.running &&
!panel.device.ptpRunning && !panel.startPending &&
!panel.session.updatingFirmware

readonly property bool startPending: panel.device && panel.device.ptpStarting

function roleIndex(role) {
Expand Down Expand Up @@ -603,7 +618,79 @@ SurfacePanel {
spacing: 10

Rectangle { Layout.fillWidth: true; height: 1; color: panel.theme.lineSoft }
Label { text: "Advanced PTP profile"; color: panel.theme.text; font.family: panel.uiFont; font.pixelSize: 12; font.weight: Font.DemiBold }
Label { text: "Advanced PTP policy & profile"; color: panel.theme.text; font.family: panel.uiFont; font.pixelSize: 12; font.weight: Font.DemiBold }

RowLayout {
Layout.fillWidth: true
spacing: 10
Label {
text: "Time source"
color: panel.theme.muted
font.family: panel.uiFont
font.pixelSize: 10
}
StudioDarkComboBox {
id: policyCombo
Layout.fillWidth: true
model: [
"Internal GM only — laboratory SOURCE",
"AUTO — prefer better external GM",
"External only — PTP RECEIVER",
"Monitor only — passive observation"
]
currentIndex: panel.policyDraftIndex >= 0
? panel.policyDraftIndex
: panel.ptpPolicyIndex(panel.device.ptpPolicy)
enabled: panel.canConfigurePolicy
onActivated: function(index) {
panel.policyDraftIndex = index
}
}
CalmButton {
theme: panel.theme
uiFont: panel.uiFont
text: "Set policy"
enabled: panel.canConfigurePolicy &&
panel.device.ptpPolicy !== "UNKNOWN"
onClicked: {
const mode = panel.ptpPolicyModes[policyCombo.currentIndex]
if (panel.session.requestPtpAdvancedPolicy(mode)) {
panel.policyDraftIndex = -1
if (panel.controller)
panel.controller.showMessage(
"PTP policy queued — confirming board readback.", false)
} else if (panel.controller) {
panel.controller.showMessage(
"PTP policy rejected. Stop SV/PTP first and inspect diagnostics.", true)
}
}
}
}
Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: "Board policy: " + panel.device.ptpPolicy +
" · Actual role: " + panel.device.ptpRole +
" · Supervisor: " +
(panel.device.ptpPolicyBootAuto === "1" ? "ENABLED" : "MANUAL/INACTIVE") +
" · External candidate: " +
(panel.device.ptpCandidate === "1" ? "STABLE ANNOUNCE (not LOCK)" : "NONE") +
" · Source generation: " + panel.device.ptpSourceGeneration
color: panel.theme.textSoft
font.family: panel.monoFont
font.pixelSize: 10
}
Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: "Changing source policy requires SV STOP and verified PTP STOP. " +
"AUTO handover occurs only while SV is OFF; external LOCK requires " +
"measured Pdelay/offset. Policy is kept in board RAM until reboot. " +
"All internal-GM and automatic fallback modes are for isolated lab networks."
color: panel.theme.muted
font.family: panel.uiFont
font.pixelSize: 10
}

GridLayout {
Layout.fillWidth: true
Expand Down
61 changes: 61 additions & 0 deletions apps/arstack_studio/src/DeviceController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ const QRegularExpression kPtpEthernetExpression{
const QRegularExpression kPtpConfigExpression{
QStringLiteral("PTP config domain=(\\d+) transportSpecific=0x([0-9A-Fa-f]+) VLAN=(\\S+) Announce=(\\d+) ms Sync=(\\d+) ms Pdelay=(ON|OFF)"),
QRegularExpression::CaseInsensitiveOption};
const QRegularExpression kPtpAdvancedPolicyExpression{
QStringLiteral(
"PTPPOLICY mode=(INTERNAL_GM_ONLY|AUTO_PREFER_EXTERNAL|EXTERNAL_ONLY|MONITOR_ONLY) "
"role=(SOURCE|RECEIVER|MONITOR) boot_auto=([01]) ext_candidate=([01]) "
"source_generation=(\\d+) sv_running=([01])"),
QRegularExpression::CaseInsensitiveOption};
const QRegularExpression kPtpRoleExpression{
QStringLiteral("PTPROLE role=(SOURCE|RECEIVER|MONITOR) value=(\\d+) running=([01])"),
QRegularExpression::CaseInsensitiveOption};
Expand Down Expand Up @@ -1354,6 +1360,32 @@ bool DeviceController::setCtSaturation(
}

bool DeviceController::sendPtpShow() { return sendCommand(QStringLiteral("PTP SHOW")); }

bool DeviceController::setPtpAdvancedPolicy(const QString& requested) {
const QString mode = requested.trimmed().toUpper();
if (mode != QStringLiteral("INTERNAL_GM_ONLY") &&
mode != QStringLiteral("AUTO_PREFER_EXTERNAL") &&
mode != QStringLiteral("EXTERNAL_ONLY") &&
mode != QStringLiteral("MONITOR_ONLY")) {
setError(QStringLiteral("Invalid PTP source policy."));
return false;
}
// The host preflight is not proof of board STOP; the firmware repeats
// this gate against live PTP/SV ownership and reports its exact refusal.
if (!deviceVerified_ || !connected_ || !ptpAvailable_ ||
running_ || ptpRunning_ || ptpStarting() ||
scenarioStage_ != ScenarioStage::idle || profileDeploying_) {
setError(QStringLiteral(
"STOP SV injection and verify PTP STOPPED before changing time source policy."));
return false;
}
if (!identity_.capabilities.contains(
QStringLiteral("PTP-POLICY-M3D-V1"), Qt::CaseInsensitive)) {
setError(QStringLiteral("Firmware lacks M3.D runtime timing-source policy support."));
return false;
}
return sendCommand(QStringLiteral("PTP POLICY %1").arg(mode));
}
bool DeviceController::startPtp() {
// Legacy direct START must not bypass a guided START, in-flight STOP,
// or an unverified cancellation. Both paths share this device authority.
Expand Down Expand Up @@ -2356,6 +2388,19 @@ void DeviceController::processLine(const QString& rawLine) {
emit ptpStateChanged();
}

match = kPtpAdvancedPolicyExpression.match(line);
if (match.hasMatch()) {
ptpAvailable_ = true;
ptpPolicy_ = match.captured(1).toUpper();
ptpRole_ = match.captured(2).toUpper();
ptpPolicyBootAuto_ = match.captured(3);
ptpCandidate_ = match.captured(4);
ptpSourceGeneration_ = match.captured(5);
// A policy readback does not authorize any claim about actual relay
// PTP LOCK, nor override measured PTP2 DISCIPLINE state.
emit ptpStateChanged();
}

match = kPtpRoleExpression.match(line);
if (match.hasMatch()) {
ptpAvailable_ = true;
Expand Down Expand Up @@ -2404,6 +2449,18 @@ void DeviceController::processLine(const QString& rawLine) {
ptpStatus_ = QStringLiteral("Not enabled in firmware");
emit ptpStateChanged();
}
if (line.contains(QStringLiteral("PTP POLICY rejected:"), Qt::CaseInsensitive)) {
setError(QStringLiteral("Board rejected timing policy: %1").arg(line));
// Board's previous mode is still authoritative. Do not silently
// adopt a requested GUI setting after firmware rejected it.
static_cast<void>(sendPtpShow());
}
if (line.contains(QStringLiteral("PTP POLICY accepted mode="),
Qt::CaseInsensitive) && !ptpStarting()) {
emit deviceMessage(QStringLiteral(
"PTP source policy accepted by board; verifying live mode/role."));
static_cast<void>(sendPtpShow());
}
if (line.contains(QStringLiteral("PTP configuration accepted"), Qt::CaseInsensitive) && !ptpStarting()) {
emit deviceMessage(QStringLiteral("PTP expert profile accepted."));
static_cast<void>(sendPtpShow());
Expand Down Expand Up @@ -2473,6 +2530,10 @@ void DeviceController::resetPtpState() {
ptpPdelayFrames_ = QStringLiteral("-");
ptpTxFailures_ = QStringLiteral("-");
ptpRole_ = QStringLiteral("SOURCE");
ptpPolicy_ = QStringLiteral("UNKNOWN");
ptpPolicyBootAuto_ = QStringLiteral("0");
ptpCandidate_ = QStringLiteral("0");
ptpSourceGeneration_ = QStringLiteral("0");
ptpDiscipline_ = QStringLiteral("UNLOCKED");
ptpSource_ = QStringLiteral("NONE");
ptpOffsetNs_ = QStringLiteral("NA");
Expand Down
13 changes: 13 additions & 0 deletions apps/arstack_studio/src/DeviceController.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ class DeviceController : public QObject {
Q_PROPERTY(QString ptpPdelayFrames READ ptpPdelayFrames NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpTxFailures READ ptpTxFailures NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpRole READ ptpRole NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpPolicy READ ptpPolicy NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpPolicyBootAuto READ ptpPolicyBootAuto NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpCandidate READ ptpCandidate NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpSourceGeneration READ ptpSourceGeneration NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpDiscipline READ ptpDiscipline NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpSource READ ptpSource NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpOffsetNs READ ptpOffsetNs NOTIFY ptpStateChanged)
Expand Down Expand Up @@ -180,6 +184,10 @@ class DeviceController : public QObject {
[[nodiscard]] QString ptpPdelayFrames() const { return ptpPdelayFrames_; }
[[nodiscard]] QString ptpTxFailures() const;
[[nodiscard]] QString ptpRole() const { return ptpRole_; }
[[nodiscard]] QString ptpPolicy() const { return ptpPolicy_; }
[[nodiscard]] QString ptpPolicyBootAuto() const { return ptpPolicyBootAuto_; }
[[nodiscard]] QString ptpCandidate() const { return ptpCandidate_; }
[[nodiscard]] QString ptpSourceGeneration() const { return ptpSourceGeneration_; }
[[nodiscard]] QString ptpDiscipline() const { return ptpDiscipline_; }
[[nodiscard]] QString ptpSource() const { return ptpSource_; }
[[nodiscard]] QString ptpOffsetNs() const { return ptpOffsetNs_; }
Expand Down Expand Up @@ -257,6 +265,7 @@ class DeviceController : public QObject {
void abandonProfileDeployment();

bool sendPtpShow();
bool setPtpAdvancedPolicy(const QString& mode);
bool startPtp();
bool startPtpConfigured(const QString& role, const QVariantMap& profile);
bool stopPtp();
Expand Down Expand Up @@ -490,6 +499,10 @@ class DeviceController : public QObject {
QString ptpPdelayFrames_{QStringLiteral("-")};
QString ptpTxFailures_{QStringLiteral("-")};
QString ptpRole_{QStringLiteral("SOURCE")};
QString ptpPolicy_{QStringLiteral("UNKNOWN")};
QString ptpPolicyBootAuto_{QStringLiteral("0")};
QString ptpCandidate_{QStringLiteral("0")};
QString ptpSourceGeneration_{QStringLiteral("0")};
QString ptpDiscipline_{QStringLiteral("UNLOCKED")};
QString ptpSource_{QStringLiteral("NONE")};
QString ptpOffsetNs_{QStringLiteral("NA")};
Expand Down
7 changes: 7 additions & 0 deletions apps/arstack_studio/src/SmartSessionController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,13 @@ bool SmartSessionController::requestSmpSynch(const QString& mode) {
bool SmartSessionController::requestConfigurePtp(const QVariantMap& profile) {
return deviceControlAvailable() && device_ != nullptr && device_->configurePtp(profile);
}
bool SmartSessionController::requestPtpAdvancedPolicy(const QString& mode) {
return deviceControlAvailable() && device_ != nullptr &&
!device_->running() && !device_->ptpRunning() &&
!device_->ptpStarting() && !device_->scenarioApplying() &&
!device_->profileDeploying() && !updateRequested_ &&
device_->setPtpAdvancedPolicy(mode);
}

bool SmartSessionController::requestStartPtp() {
return deviceControlAvailable() && device_ != nullptr && device_->startPtp();
Expand Down
1 change: 1 addition & 0 deletions apps/arstack_studio/src/SmartSessionController.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ class SmartSessionController : public QObject {
Q_INVOKABLE bool requestPtpRole(const QString& role);
Q_INVOKABLE bool requestSmpSynch(const QString& mode);
Q_INVOKABLE bool requestConfigurePtp(const QVariantMap& profile);
Q_INVOKABLE bool requestPtpAdvancedPolicy(const QString& mode);
Q_INVOKABLE bool requestStartPtp();
Q_INVOKABLE bool requestStartPtpConfigured(const QString& role, const QVariantMap& profile);
Q_INVOKABLE bool requestStopPtp();
Expand Down
23 changes: 23 additions & 0 deletions apps/arstack_studio/tests/check_operator_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,29 @@ def main() -> None:
and "!/^[+-]?" in main_qml,
"numeric editors must validate local draft and commit only once at edit completion")

# M3.D is an actual firmware operator-policy transaction, not a cosmetic
# Studio selector; generation and current-board readback are mandatory.
live_control = (project_root / "embedded/esp32p4_smv_injector/main/live_control.cpp").read_text(encoding="utf-8")
policy_header = (project_root / "include/ariec61850/time_sync/ptp_advanced_policy.hpp").read_text(encoding="utf-8")
session_header = (STUDIO / "src/SmartSessionController.hpp").read_text(encoding="utf-8")
require("PTP-POLICY-M3D-V1" in live_control
and "PTP POLICY rejected: STOP_SV_AND_PTP_FIRST" in live_control
and "ar_ptp_lab_set_advanced_policy(desired)" in live_control
and "PTPPOLICY mode=%s role=%s boot_auto=%u" in live_control
and "g_ptp_policy_generation" in ptp_facade
and "Advanced::auto_handover(mode)" in firmware_source
and "Advanced::starts_receiver(mode)" in firmware_source
and "Advanced::starts_monitor(mode)" in firmware_source
and "permits_local_fallback" in policy_header
and "requestPtpAdvancedPolicy" in session_header
and "device_->setPtpAdvancedPolicy(mode)" in session_cpp
and "kPtpAdvancedPolicyExpression" in device_cpp
and "PTP-POLICY-M3D-V1" in device_cpp
and "id: policyCombo" in expert
and "panel.device.ptpPolicy" in expert
and "panel.session.requestPtpAdvancedPolicy(mode)" in expert,
"M3.D Advanced PTP policy must be firmware-enforced, board-readback and session-owned")

print("Studio operator workspace contract: PASS")


Expand Down
Loading
Loading