Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
c656222
fix(ptp): require real driver START + PHY CONNECTED before enabling E…
masarray Oct 21, 2026
979db2b
feat(ptp): expose real PHY/link readiness and classified preflight fa…
masarray Oct 21, 2026
84c0acf
fix(ptp): preserve ESP_ERR_INVALID_STATE TX root cause and allow boun…
masarray Oct 21, 2026
9892e12
feat(console): report physical EMAC+PHY state on PTP SHOW and precise…
masarray Oct 21, 2026
b9f82c3
fix(studio): explain LINK_DOWN versus MAC_INVALID_STATE and avoid red…
masarray Oct 21, 2026
fd7704b
test(studio): reproduce PTP ETHERNET_LINK_DOWN and ESP_ERR_INVALID_ST…
masarray Oct 21, 2026
d314217
test(contract): require real Ethernet PHY and driver evidence before …
masarray Oct 21, 2026
6fcfc39
test(contract): check C function signature and actual PTP SHOW invoca…
masarray Oct 21, 2026
b27574b
test(contract): initialize PTP console evidence before new ETH prefli…
masarray Oct 21, 2026
8387731
feat(studio): expose measured EMAC driver and PHY link to native Time…
masarray Oct 21, 2026
180b854
feat(studio): parse board PTP ETH driver/link snapshot without derivi…
masarray Oct 21, 2026
59f7165
feat(ui): display real ESP32-P4 driver/PHY readiness directly in Adva…
masarray Oct 21, 2026
37cabe9
feat(diagnostics): include exact board ETH driver and physical PHY li…
masarray Oct 21, 2026
8033976
test(studio): verify PTP board ETH driver/link state on live GUI read…
masarray Oct 21, 2026
e44cf25
test(contract): enforce visible PTP PHY state and diagnostics, not on…
masarray Oct 21, 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
39 changes: 39 additions & 0 deletions apps/arstack_studio/qml/PtpExpertPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,45 @@ SurfacePanel {
}
}

// Board-issued Ethernet event evidence, never a USB/COM guess.
// A DOWN or UNKNOWN PHY cannot authorize SOURCE START.
RowLayout {
visible: panel.device && panel.device.deviceVerified
Layout.fillWidth: true
spacing: 10
Label {
text: "Ethernet (board)"
color: panel.theme.muted
font.family: panel.uiFont
font.pixelSize: 10
}
Label {
text: "Driver " + panel.device.ptpEthernetDriver +
" · PHY " + panel.device.ptpEthernetLink
color: panel.device.ptpEthernetLink === "DOWN" ||
panel.device.ptpEthernetDriver === "STOPPED"
? panel.theme.red
: panel.device.ptpEthernetLink === "UP" &&
panel.device.ptpEthernetDriver === "STARTED"
? panel.theme.green : panel.theme.amber
font.family: panel.monoFont
font.pixelSize: 10
font.weight: Font.DemiBold
}
Item { Layout.fillWidth: true }
Label {
text: panel.device.ptpEthernetLink === "DOWN"
? "Check Ethernet cable / active switch"
: panel.device.ptpEthernetLink === "UNKNOWN"
? "Refresh to read physical link"
: "Link status from ESP32-P4"
color: panel.device.ptpEthernetLink === "DOWN"
? panel.theme.red : panel.theme.muted
font.family: panel.uiFont
font.pixelSize: 9
}
}

Rectangle {
Layout.fillWidth: true
implicitHeight: 82
Expand Down
59 changes: 59 additions & 0 deletions apps/arstack_studio/src/DeterministicSessionHarness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class DeterministicSessionHarness final {
{"PTP runtime requires matching PTP2 role", ptpRuntimeRoleEvidenceRequired()},
{"PTP RUNNING before PTP2 is bounded success", ptpRuntimeOutOfOrderProof()},
{"PTP source rejection survives verified safety STOP", ptpSourceReasonSurvivesSafetyStop()},
{"PTP LINK_DOWN is classified early; MAC INVALID_STATE keeps STOP verification", ptpPhysicalEthernetPreflightAndMacFailure()},
{"PTP rejected config never starts", ptpGuidedRejectedConfigStops()},
{"PTP STOP cancels pending start after verified readback", ptpGuidedStopCancelsStart()},
{"legacy bare SV START refuses stale eight-channel state", legacyBareStartFailsClosed()},
Expand Down Expand Up @@ -746,6 +747,64 @@ class DeterministicSessionHarness final {
d.ptpStatus_.contains(QStringLiteral("confirmed by board"));
}

static bool ptpPhysicalEthernetPreflightAndMacFailure() {
{
Fixture f;
auto& d = f.device;
d.processLine(QStringLiteral(
"I ar_smv_eth: PTP ETH driver=STARTED link=DOWN tx_ready=0 start_reason=NONE"));
if (d.ptpEthernetDriver_ != QStringLiteral("STARTED") ||
d.ptpEthernetLink_ != QStringLiteral("DOWN") || d.ptpRunning_) return false;
d.processLine(QStringLiteral(
"PTP ETH driver=STARTED link=UP tx_ready=1 start_reason=NONE"));
if (d.ptpEthernetLink_ != QStringLiteral("UP") || d.ptpRunning_) return false;
d.processLine(QStringLiteral(
"PTP ETH driver=STOPPED link=DOWN tx_ready=0 start_reason=ETHERNET_DRIVER_NOT_STARTED"));
if (d.ptpEthernetDriver_ != QStringLiteral("STOPPED") ||
d.ptpEthernetLink_ != QStringLiteral("DOWN")) return false;
}
{
Fixture f;
if (!setupGuidedPtp(f, QStringLiteral("SOURCE"))) return false;
auto& d = f.device;
d.processLine(QStringLiteral("PTPROLE accepted"));
d.processLine(QStringLiteral("PTPROLE role=SOURCE value=0 running=0"));
d.processLine(QStringLiteral("PTP configuration accepted"));
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_start)
return false;
d.processLine(QStringLiteral(
"PTP start rejected: source_reason=ETHERNET_LINK_DOWN"));
// ETH preflight has not launched ANY source runtime, therefore
// a generic rollback PTP STOP would be unsafe and misleading.
if (d.ptpStarting() || d.ptpRunning_ ||
!d.ptpLastStartFailure_.contains(QStringLiteral("link is DOWN")) ||
!d.ptpStatus_.contains(QStringLiteral("link is DOWN")))
return false;
if (d.logText_.contains(QStringLiteral("→ PTP STOP"))) return false;
}
{
Fixture f;
if (!setupGuidedPtp(f, QStringLiteral("SOURCE"))) return false;
auto& d = f.device;
d.processLine(QStringLiteral("PTPROLE accepted"));
d.processLine(QStringLiteral("PTPROLE role=SOURCE value=0 running=0"));
d.processLine(QStringLiteral("PTP configuration accepted"));
d.processLine(QStringLiteral(
"PTP start rejected: source_reason=ETHERNET_TX_INVALID_STATE"));
// A source TX error is NOT physical evidence PTP is OFF until
// its teardown/STOP is acknowledged by the board.
if (d.ptpStartStage_ != DeviceController::PtpStartStage::awaiting_stop_ack ||
!d.ptpLastStartFailure_.contains(QStringLiteral("ESP_ERR_INVALID_STATE")) ||
d.ptpRunning_) return false;
d.processLine(QStringLiteral("PTP stop requested"));
d.processLine(ptpStoppedStatus());
if (d.ptpStarting() || d.ptpRunning_ ||
!d.ptpLastStartFailure_.contains(QStringLiteral("ESP_ERR_INVALID_STATE")))
return false;
}
return true;
}

static bool ptpSourceReasonSurvivesSafetyStop() {
Fixture f;
if (!setupGuidedPtp(f, QStringLiteral("SOURCE"))) return false;
Expand Down
52 changes: 46 additions & 6 deletions apps/arstack_studio/src/DeviceController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ const QRegularExpression kIdentityExpression{
const QRegularExpression kPtpStatusExpression{
QStringLiteral("PTP status=(RUNNING|STOPPED) Announce=(\\d+) Sync=(\\d+) FollowUp=(\\d+) PdelayFrames=(\\d+) TXfail=(\\d+)"),
QRegularExpression::CaseInsensitiveOption};
const QRegularExpression kPtpEthernetExpression{
QStringLiteral("PTP ETH driver=(STARTED|STOPPED) link=(UP|DOWN) tx_ready=([01]) start_reason=([A-Z_]+)"),
QRegularExpression::CaseInsensitiveOption};
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};
Expand Down Expand Up @@ -1532,10 +1535,30 @@ void DeviceController::advancePtpStartup(const QString& line) {
setError(ptpLastStartFailure_);
return;
}
const bool rollback = ptpStartStage_ == PtpStartStage::awaiting_start ||
ptpStartStage_ == PtpStartStage::awaiting_runtime;
const bool phyDown = line.contains(QStringLiteral("source_reason=ETHERNET_LINK_DOWN"),
Qt::CaseInsensitive);
const bool driverDown = line.contains(QStringLiteral("source_reason=ETHERNET_DRIVER_"),
Qt::CaseInsensitive);
const bool txInvalid = line.contains(QStringLiteral("source_reason=ETHERNET_TX_INVALID_STATE"),
Qt::CaseInsensitive);
// A preflight rejection happens BEFORE any PTP runtime is launched;
// never send a redundant STOP that could race a later user action.
const bool rollback = !phyDown && !driverDown &&
(ptpStartStage_ == PtpStartStage::awaiting_start ||
ptpStartStage_ == PtpStartStage::awaiting_runtime);
const QString step = ptpStartPhase();
failPtpStartup(QStringLiteral("Board rejected PTP %1 command: %2").arg(step, line), rollback);
const QString actionable = phyDown
? QStringLiteral("Ethernet PHY link is DOWN. Connect a live switch/cable to the ESP32-P4 Ethernet port, wait for LINK LED, then retry PTP START.")
: driverDown
? QStringLiteral("ESP32-P4 Ethernet driver is not STARTED. Inspect hardware Ethernet initialization and reset the board if the driver remains stopped.")
: txInvalid
? QStringLiteral("Ethernet reports link but EMAC rejected PTP transmission (ESP_ERR_INVALID_STATE). Inspect PTP ETH readback and firmware/driver state; PTP remains OFF.")
: QStringLiteral("Board rejected PTP %1 command: %2").arg(step, line);
appendLog(QStringLiteral("PTP"), QStringLiteral(
"START_REJECTED step=%1 preflight_link_down=%2 driver_down=%3 tx_invalid=%4 cause=%5")
.arg(step).arg(phyDown ? 1 : 0).arg(driverDown ? 1 : 0)
.arg(txInvalid ? 1 : 0).arg(line));
failPtpStartup(actionable, rollback);
return;
}
if (ptpStartStage_ == PtpStartStage::awaiting_role_ack) {
Expand Down Expand Up @@ -2103,6 +2126,15 @@ void DeviceController::processLine(const QString& rawLine) {
markDeviceVerified();
}

match = kPtpEthernetExpression.match(line);
if (match.hasMatch()) {
// Only the board's ETH_EVENT stream may establish this state;
// NEVER infer it from COM, profile selection or PTP SOURCE intent.
ptpEthernetDriver_ = match.captured(1).toUpper();
ptpEthernetLink_ = match.captured(2).toUpper();
emit ptpStateChanged();
}

match = kPtpStatusExpression.match(line);
if (match.hasMatch()) {
ptpAvailable_ = true;
Expand Down Expand Up @@ -2186,9 +2218,15 @@ void DeviceController::processLine(const QString& rawLine) {
ptpRunning_ = false;
ptpStatus_ = QStringLiteral("PTP start failed: %1").arg(line);
emit ptpStateChanged();
setError(ptpRole_ == QStringLiteral("SOURCE")
? QStringLiteral("PTP source did not emit Announce/Sync/Follow_Up. Check the Ethernet link and retry Start PTP Source.")
: QStringLiteral("PTP %1 could not start. Check the Ethernet link and retry.").arg(ptpRole_.toLower()));
// Preserve the exact classified first-hand board failure rather
// than replacing it with an unhelpful generic "retry" toast.
const QString hint = line.contains(QStringLiteral("ETHERNET_TX_INVALID_STATE"),
Qt::CaseInsensitive)
? QStringLiteral("Ethernet MAC TX rejected by driver state; inspect PTP ETH driver/link.")
: line.contains(QStringLiteral("ETHERNET_LINK_DOWN"), Qt::CaseInsensitive)
? QStringLiteral("Physical Ethernet link is DOWN; verify cable and switch LINK.")
: QStringLiteral("PTP SOURCE did not establish real Announce + Sync + Follow_Up TX.");
setError(hint);
}

advancePtpStartup(line);
Expand Down Expand Up @@ -2224,6 +2262,8 @@ void DeviceController::resetPtpState() {
emit syncDiagnosticsChanged();
ptpAvailable_ = false;
ptpRunning_ = false;
ptpEthernetDriver_ = QStringLiteral("UNKNOWN");
ptpEthernetLink_ = QStringLiteral("UNKNOWN");
ptpStatus_ = QStringLiteral("Waiting for device");
ptpDomain_ = QStringLiteral("-");
ptpTransportSpecific_ = QStringLiteral("-");
Expand Down
7 changes: 7 additions & 0 deletions apps/arstack_studio/src/DeviceController.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ class DeviceController : public QObject {
Q_PROPERTY(bool ptpRunning READ ptpRunning NOTIFY ptpStateChanged)
Q_PROPERTY(bool ptpStarting READ ptpStarting NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpStatus READ ptpStatus NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpEthernetDriver READ ptpEthernetDriver NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpEthernetLink READ ptpEthernetLink NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpLastStartFailure READ ptpLastStartFailure NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpStartPhase READ ptpStartPhase NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpDomain READ ptpDomain NOTIFY ptpStateChanged)
Expand Down Expand Up @@ -163,6 +165,8 @@ class DeviceController : public QObject {
[[nodiscard]] bool ptpRunning() const noexcept;
[[nodiscard]] bool ptpStarting() const noexcept { return ptpStartStage_ != PtpStartStage::idle; }
[[nodiscard]] QString ptpStatus() const;
[[nodiscard]] QString ptpEthernetDriver() const { return ptpEthernetDriver_; }
[[nodiscard]] QString ptpEthernetLink() const { return ptpEthernetLink_; }
[[nodiscard]] QString ptpLastStartFailure() const { return ptpLastStartFailure_; }
[[nodiscard]] QString ptpStartPhase() const;
[[nodiscard]] QString ptpDomain() const;
Expand Down Expand Up @@ -458,6 +462,9 @@ class DeviceController : public QObject {
bool pendingAutoDetect_{false};
double signalFrequencyHz_{50.0};
QString ptpStatus_{QStringLiteral("Waiting for device")};
// Actual ESP-IDF ETH_EVENT proof. UNKNOWN never means link is UP.
QString ptpEthernetDriver_{QStringLiteral("UNKNOWN")};
QString ptpEthernetLink_{QStringLiteral("UNKNOWN")};
QString ptpLastStartFailure_;
QString ptpDomain_{QStringLiteral("-")};
QString ptpTransportSpecific_{QStringLiteral("-")};
Expand Down
2 changes: 2 additions & 0 deletions apps/arstack_studio/src/SmartSessionController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,8 @@ QString SmartSessionController::diagnosticsText() const {
add(QStringLiteral("SMV missed reported"), device_->missed());
add(QStringLiteral("SMV TX failures reported"), device_->txFailures());
add(QStringLiteral("PTP status reported"), device_->ptpStatus());
add(QStringLiteral("PTP Ethernet driver (board ETH_EVENT)"), device_->ptpEthernetDriver());
add(QStringLiteral("PTP PHY link (board ETH_EVENT)"), device_->ptpEthernetLink());
add(QStringLiteral("PTP transactional phase"), device_->ptpStartPhase());
add(QStringLiteral("Last PTP START failure (same boot)"),
device_->ptpLastStartFailure());
Expand Down
27 changes: 27 additions & 0 deletions apps/arstack_studio/tests/check_operator_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,34 @@ def main() -> None:
"busy: Boolean(panel.startPending)" in ptp_panel,
"Firmware tab must show build provenance and SOURCE 9-3 preflight")
ptp_firmware = (STUDIO.parent.parent / "embedded/esp32p4_smv_injector/main/ptp_lab_task.cpp").read_text(encoding="utf-8")
eth_port = (STUDIO.parent.parent / "embedded/esp32p4_smv_injector/main/ethernet_port.c").read_text(encoding="utf-8")
ptp_studio = (STUDIO / "src/DeviceController.cpp").read_text(encoding="utf-8")
ptp_console = (STUDIO.parent.parent / "embedded/esp32p4_smv_injector/main/live_control.cpp").read_text(encoding="utf-8")
require("ETHERNET_EVENT_START" in eth_port
and "ETHERNET_EVENT_CONNECTED" in eth_port
and "ETHERNET_EVENT_DISCONNECTED" in eth_port
and "xEventGroupWaitBits(" in eth_port
and "kTxReady" in eth_port
and "ETHERNET_LINK_DOWN" in eth_port
and "ETHERNET_DRIVER_NOT_STARTED" in eth_port
and "void ar_esp32p4_ptp_print_link_status(void)" in eth_port
and "ar_esp32p4_ptp_print_link_status();" in ptp_console
and "ethernet_tx_invalid_state" in ptp_firmware
and "ETHERNET_TX_INVALID_STATE" in ptp_firmware
and "kStartReadyPolls = 350U" in ptp_firmware
and "source_reason=ETHERNET_LINK_DOWN" in ptp_studio
and "source_reason=ETHERNET_TX_INVALID_STATE" in ptp_studio
and "kPtpEthernetExpression" in ptp_studio
and "ptpEthernetLink" in source("PtpExpertPanel.qml")
and "ptpEthernetDriver" in source("PtpExpertPanel.qml")
and "PTP PHY link (board ETH_EVENT)" in
(STUDIO / "src/SmartSessionController.cpp").read_text(encoding="utf-8")
and "ptpPhysicalEthernetPreflightAndMacFailure()" in
(STUDIO / "src/DeterministicSessionHarness.cpp").read_text(encoding="utf-8"),
"PTP SOURCE must prove ETH driver and PHY before TX, classify MAC INVALID_STATE, "
"and never mark a failed START as RUNNING")


require('SOURCE_REQUIRES_1S_ANNOUNCE_1S_SYNC_AND_PDELAY' in ptp_firmware and
'ar_ptp_lab_last_config_error()' in ptp_console and
'PDELAY_RX_TIMESTAMP_UNAVAILABLE' in ptp_firmware and
Expand Down
Loading
Loading