From c65622293c5ef6d6af32bf152c86a6643358d86b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:19:33 +0700 Subject: [PATCH 01/15] fix(ptp): require real driver START + PHY CONNECTED before enabling EMAC PTP/TX --- .../esp32p4_smv_injector/main/ethernet_port.c | 97 ++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/embedded/esp32p4_smv_injector/main/ethernet_port.c b/embedded/esp32p4_smv_injector/main/ethernet_port.c index b7217280b..5c631ec5f 100644 --- a/embedded/esp32p4_smv_injector/main/ethernet_port.c +++ b/embedded/esp32p4_smv_injector/main/ethernet_port.c @@ -3,6 +3,8 @@ #include "ethernet_port.h" #include "esp_err.h" +#include "esp_event.h" +#include "freertos/event_groups.h" #include "esp_eth_mac_esp.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" @@ -16,6 +18,62 @@ static const char *TAG = "ar_smv_eth"; static esp_eth_handle_t s_eth_handle = NULL; +// The owner of this evidence is the ESP-IDF ETH_EVENT stream, NOT GUI +// "connected" status, a configured PHY address, nor a stale PTP runtime. +static EventGroupHandle_t s_eth_events = NULL; +static const EventBits_t kDriverStarted = BIT0; +static const EventBits_t kPhyLinkUp = BIT1; +static const EventBits_t kTxReady = BIT0 | BIT1; +static const TickType_t kLinkWaitTicks = pdMS_TO_TICKS(2500); +static const char *s_ptp_preflight_failure = NULL; + +static void ar_esp32p4_eth_event(void *arg, esp_event_base_t base, + int32_t event_id, void *data) +{ + (void)arg; + (void)base; + (void)data; + if (s_eth_events == NULL) return; + switch (event_id) { + case ETHERNET_EVENT_START: + xEventGroupSetBits(s_eth_events, kDriverStarted); + break; + case ETHERNET_EVENT_CONNECTED: + xEventGroupSetBits(s_eth_events, kPhyLinkUp); + break; + case ETHERNET_EVENT_DISCONNECTED: + xEventGroupClearBits(s_eth_events, kPhyLinkUp); + break; + case ETHERNET_EVENT_STOP: + xEventGroupClearBits(s_eth_events, kTxReady); + break; + default: + break; + } +} + +static EventBits_t ar_eth_bits(void) +{ + return s_eth_events == NULL ? 0 : xEventGroupGetBits(s_eth_events); +} + +const char *ar_esp32p4_ptp_preflight_reason(void) +{ + return s_ptp_preflight_failure; +} + +void ar_esp32p4_ptp_print_link_status(void) +{ + const EventBits_t bits = ar_eth_bits(); + ESP_LOGI(TAG, + "PTP ETH driver=%s link=%s tx_ready=%u start_reason=%s", + (bits & kDriverStarted) ? "STARTED" : "STOPPED", + (bits & kPhyLinkUp) ? "UP" : "DOWN", + (bits & kTxReady) == kTxReady ? 1U : 0U, + s_ptp_preflight_failure == NULL ? "NONE" : s_ptp_preflight_failure); +} + + // Bench-validated ESP32-P4 Ethernet-board RMII wiring. // REF_CLK is supplied by the onboard PHY as a 50 MHz input to ESP32-P4. enum { @@ -98,6 +156,22 @@ esp_eth_handle_t ar_esp32p4_eth_init(void) phy->del(phy); return NULL; } + s_eth_events = xEventGroupCreate(); + if (s_eth_events == NULL) { + ESP_LOGE(TAG, "Cannot allocate Ethernet link evidence event group"); + esp_eth_driver_uninstall(handle); + return NULL; + } + const esp_err_t event_result = esp_event_handler_register( + ETH_EVENT, ESP_EVENT_ANY_ID, &ar_esp32p4_eth_event, NULL); + if (event_result != ESP_OK) { + ESP_LOGE(TAG, "Cannot register Ethernet link evidence: %s", + esp_err_to_name(event_result)); + vEventGroupDelete(s_eth_events); + s_eth_events = NULL; + esp_eth_driver_uninstall(handle); + return NULL; + } s_eth_handle = handle; ESP_LOGI(TAG, @@ -109,8 +183,27 @@ esp_eth_handle_t ar_esp32p4_eth_init(void) bool ar_esp32p4_ptp_start(void) { #if CONFIG_AR_PTP_LAB_TX - if (s_eth_handle == NULL) { - ESP_LOGE(TAG, "PTP start rejected: Ethernet driver is not initialized"); + s_ptp_preflight_failure = NULL; + if (s_eth_handle == NULL || s_eth_events == NULL) { + s_ptp_preflight_failure = "ETHERNET_DRIVER_UNAVAILABLE"; + ESP_LOGE(TAG, "PTP preflight failed: ETHERNET_DRIVER_UNAVAILABLE"); + return false; + } + + // Wait for BOTH the driver's START event and actual PHY CONNECTED event. + // A USB-verified board is NOT Ethernet-link proof. Do not enter PTP + // timestamp mode or attempt ESP-IDF TX against a non-transmitting MAC. + const EventBits_t ready = xEventGroupWaitBits( + s_eth_events, kTxReady, pdFALSE, pdTRUE, kLinkWaitTicks); + if ((ready & kTxReady) != kTxReady) { + s_ptp_preflight_failure = (ready & kDriverStarted) + ? "ETHERNET_LINK_DOWN" : "ETHERNET_DRIVER_NOT_STARTED"; + ESP_LOGE(TAG, + "PTP preflight failed: %s; Ethernet driver=%s PHY=%s; " + "attach an active Ethernet switch/cable, check LINK LED, retry", + s_ptp_preflight_failure, + (ready & kDriverStarted) ? "STARTED" : "STOPPED", + (ready & kPhyLinkUp) ? "UP" : "DOWN"); return false; } From 979db2b5cae60dd8f368e298df45ccc479e181cd Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:19:48 +0700 Subject: [PATCH 02/15] feat(ptp): expose real PHY/link readiness and classified preflight failure --- embedded/esp32p4_smv_injector/main/ethernet_port.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/embedded/esp32p4_smv_injector/main/ethernet_port.h b/embedded/esp32p4_smv_injector/main/ethernet_port.h index 2c52ffe40..74a456142 100644 --- a/embedded/esp32p4_smv_injector/main/ethernet_port.h +++ b/embedded/esp32p4_smv_injector/main/ethernet_port.h @@ -22,6 +22,12 @@ esp_eth_handle_t ar_esp32p4_eth_init(void); /** Restart the optional PTP lab runtime on the initialized Ethernet device. */ bool ar_esp32p4_ptp_start(void); +/** Last PTP START transport preflight failure, or NULL (no failure). */ +const char *ar_esp32p4_ptp_preflight_reason(void); + +/** Snapshot of actual ESP-IDF driver and physical PHY link state in PTP SHOW. */ +void ar_esp32p4_ptp_print_link_status(void); + #ifdef __cplusplus } #endif From 84c0acfbc83582c260a2f90e09480ba784943fdd Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:20:17 +0700 Subject: [PATCH 03/15] fix(ptp): preserve ESP_ERR_INVALID_STATE TX root cause and allow bounded 3-epoch physical SOURCE proof --- .../main/ptp_lab_task.cpp | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/embedded/esp32p4_smv_injector/main/ptp_lab_task.cpp b/embedded/esp32p4_smv_injector/main/ptp_lab_task.cpp index d8234be5b..34db58c09 100644 --- a/embedded/esp32p4_smv_injector/main/ptp_lab_task.cpp +++ b/embedded/esp32p4_smv_injector/main/ptp_lab_task.cpp @@ -88,7 +88,7 @@ enum class PtpSourceStartReason : std::uint8_t { ieee1588_clock_unavailable, mac_identity_unavailable, invalid_runtime_profile, runtime_start_rejected, pdelay_rx_timestamp_unavailable, source_tx_evidence_timeout, - source_task_create_failed, source_task_unsupported + ethernet_tx_invalid_state, source_task_create_failed, source_task_unsupported }; std::atomic g_source_start_reason{ PtpSourceStartReason::not_attempted}; @@ -125,6 +125,7 @@ std::atomic g_ptp_config_failure{PtpConfigFailure::none}; case PtpSourceStartReason::runtime_start_rejected: return "RUNTIME_START_REJECTED"; case PtpSourceStartReason::pdelay_rx_timestamp_unavailable: return "PDELAY_RX_TIMESTAMP_UNAVAILABLE"; case PtpSourceStartReason::source_tx_evidence_timeout: return "SOURCE_TX_EVIDENCE_TIMEOUT"; + case PtpSourceStartReason::ethernet_tx_invalid_state: return "ETHERNET_TX_INVALID_STATE"; case PtpSourceStartReason::source_task_create_failed: return "SOURCE_TASK_CREATE_FAILED"; case PtpSourceStartReason::source_task_unsupported: return "SOURCE_TASK_UNSUPPORTED"; } @@ -440,6 +441,9 @@ void disable_hardware_ptp(const esp_eth_handle_t eth_handle) noexcept { if (frame.empty()) return false; const auto result = esp_eth_transmit(eth_handle, frame.data(), frame.size()); if (result != ESP_OK) { + if (result == ESP_ERR_INVALID_STATE) + g_source_start_reason.store( + PtpSourceStartReason::ethernet_tx_invalid_state, std::memory_order_release); ESP_LOGE(kTag, "PTP general TX failed: %s", esp_err_to_name(result)); return false; } @@ -462,6 +466,9 @@ void disable_hardware_ptp(const esp_eth_handle_t eth_handle) noexcept { frame.data(), frame.size()); if (result != ESP_OK) { + if (result == ESP_ERR_INVALID_STATE) + g_source_start_reason.store( + PtpSourceStartReason::ethernet_tx_invalid_state, std::memory_order_release); ESP_LOGE(kTag, "PTP hardware-timestamp TX failed: %s", esp_err_to_name(result)); return false; } @@ -909,14 +916,23 @@ void start_ptp_lab(const esp_eth_handle_t eth_handle) { return; } - constexpr unsigned kStartReadyPolls = 160U; + // At 1 s Announce/Sync intervals the first TX after PTP MAC activation + // can fail transiently even with an established PHY link. Allow three + // full source epochs; do NOT call it ready without actual three-frame TX. + // GUI awaiting_start has a separate bounded 10-second deadline. + constexpr unsigned kStartReadyPolls = 350U; for (unsigned attempt = 0U; attempt < kStartReadyPolls; ++attempt) { if (source_has_fresh_evidence() || !g_ptp_started.load(std::memory_order_acquire)) return; vTaskDelay(pdMS_TO_TICKS(10)); } - g_source_start_reason.store( - PtpSourceStartReason::source_tx_evidence_timeout, std::memory_order_release); - ESP_LOGE(kTag, "PTP source readiness timeout: no verified Announce/Sync/Follow_Up TX"); + if (g_source_start_reason.load(std::memory_order_acquire) != + PtpSourceStartReason::ethernet_tx_invalid_state) { + g_source_start_reason.store( + PtpSourceStartReason::source_tx_evidence_timeout, std::memory_order_release); + } + ESP_LOGE(kTag, + "PTP source readiness timeout: no verified Announce/Sync/Follow_Up TX; reason=%s", + source_start_reason_name(g_source_start_reason.load(std::memory_order_acquire))); stop_ptp_lab(); } From 9892e12ac545d4c4a90c8c09d7b68afdff49120b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:20:33 +0700 Subject: [PATCH 04/15] feat(console): report physical EMAC+PHY state on PTP SHOW and precise SOURCE START rejection --- embedded/esp32p4_smv_injector/main/live_control.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/embedded/esp32p4_smv_injector/main/live_control.cpp b/embedded/esp32p4_smv_injector/main/live_control.cpp index ccf02a667..16df8d22d 100644 --- a/embedded/esp32p4_smv_injector/main/live_control.cpp +++ b/embedded/esp32p4_smv_injector/main/live_control.cpp @@ -291,6 +291,7 @@ void print_ptp_state() noexcept { ESP_LOGE(kTag, "PTP status unavailable"); return; } + ar_esp32p4_ptp_print_link_status(); // actual Ethernet driver + PHY event state ESP_LOGI(kTag, "PTP status=%s Announce=%llu Sync=%llu FollowUp=%llu PdelayFrames=%llu TXfail=%llu", status.is_running ? "RUNNING" : "STOPPED", @@ -395,8 +396,13 @@ void handle_ptp_command(char* save) noexcept { const bool is_source = ar_ptp_lab_get_config(&active) && active.role == AR_PTP_ROLE_LAB_SOURCE; if (is_source) { + const char* preflight = ar_esp32p4_ptp_preflight_reason(); ESP_LOGE(kTag, "PTP start rejected: source_reason=%s", - ar_ptp_source_last_start_reason()); + preflight != nullptr ? preflight : + ar_ptp_source_last_start_reason()); + // Distinguish an unplugged Ethernet cable from a MAC driver + // that rejected transmit while the physical link was UP. + ar_esp32p4_ptp_print_link_status(); } else { ESP_LOGE(kTag, "PTP start rejected: receiver/monitor runtime unavailable"); } From b9f82c3f1b1a401893303cbe618560e5366c1209 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:21:22 +0700 Subject: [PATCH 05/15] fix(studio): explain LINK_DOWN versus MAC_INVALID_STATE and avoid redundant PTP rollback on preflight reject --- apps/arstack_studio/src/DeviceController.cpp | 38 ++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/apps/arstack_studio/src/DeviceController.cpp b/apps/arstack_studio/src/DeviceController.cpp index 405ad07ca..2ae244ee8 100644 --- a/apps/arstack_studio/src/DeviceController.cpp +++ b/apps/arstack_studio/src/DeviceController.cpp @@ -1532,10 +1532,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) { @@ -2186,9 +2206,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); From fd7704b6893ea59b99a6e1ffbba5a36053a161e3 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:22:05 +0700 Subject: [PATCH 06/15] test(studio): reproduce PTP ETHERNET_LINK_DOWN and ESP_ERR_INVALID_STATE START transitions --- .../src/DeterministicSessionHarness.cpp | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/apps/arstack_studio/src/DeterministicSessionHarness.cpp b/apps/arstack_studio/src/DeterministicSessionHarness.cpp index 11af07dd7..434e0a4e4 100644 --- a/apps/arstack_studio/src/DeterministicSessionHarness.cpp +++ b/apps/arstack_studio/src/DeterministicSessionHarness.cpp @@ -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()}, @@ -746,6 +747,49 @@ class DeterministicSessionHarness final { d.ptpStatus_.contains(QStringLiteral("confirmed by board")); } + static bool ptpPhysicalEthernetPreflightAndMacFailure() { + { + 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; From d31421710a17efe48e494b920cea2a11fd6ea0eb Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:22:24 +0700 Subject: [PATCH 07/15] test(contract): require real Ethernet PHY and driver evidence before PTP SOURCE TX --- .../tests/check_operator_workspace.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 6714f689b..901841fc5 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -479,6 +479,27 @@ 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") + 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 "ar_esp32p4_ptp_print_link_status()" in eth_port + 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 "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") + + ptp_console = (STUDIO.parent.parent / "embedded/esp32p4_smv_injector/main/live_control.cpp").read_text(encoding="utf-8") require('SOURCE_REQUIRES_1S_ANNOUNCE_1S_SYNC_AND_PDELAY' in ptp_firmware and 'ar_ptp_lab_last_config_error()' in ptp_console and From 6fcfc39ef5cd053c3e98467cff727b002594f5cc Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:24:51 +0700 Subject: [PATCH 08/15] test(contract): check C function signature and actual PTP SHOW invocation precisely --- apps/arstack_studio/tests/check_operator_workspace.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 901841fc5..96fb9abb9 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -488,7 +488,8 @@ def main() -> None: and "kTxReady" in eth_port and "ETHERNET_LINK_DOWN" in eth_port and "ETHERNET_DRIVER_NOT_STARTED" in eth_port - and "ar_esp32p4_ptp_print_link_status()" 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 From b27574bc14d888c2e5310711d2ed2e6a7bab4764 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:25:14 +0700 Subject: [PATCH 09/15] test(contract): initialize PTP console evidence before new ETH preflight checks --- apps/arstack_studio/tests/check_operator_workspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 96fb9abb9..b72df0a7c 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -481,6 +481,7 @@ def main() -> None: 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 @@ -501,7 +502,6 @@ def main() -> None: "and never mark a failed START as RUNNING") - ptp_console = (STUDIO.parent.parent / "embedded/esp32p4_smv_injector/main/live_control.cpp").read_text(encoding="utf-8") 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 From 8387731e55496761d7d14e0cf719496b1524da7f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:26:39 +0700 Subject: [PATCH 10/15] feat(studio): expose measured EMAC driver and PHY link to native Time Sync UI --- apps/arstack_studio/src/DeviceController.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/arstack_studio/src/DeviceController.hpp b/apps/arstack_studio/src/DeviceController.hpp index aa55c036d..0402b896a 100644 --- a/apps/arstack_studio/src/DeviceController.hpp +++ b/apps/arstack_studio/src/DeviceController.hpp @@ -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) @@ -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; @@ -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("-")}; From 180b854c5935a59203071d7805359f357514a91d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:26:55 +0700 Subject: [PATCH 11/15] feat(studio): parse board PTP ETH driver/link snapshot without deriving from USB or status --- apps/arstack_studio/src/DeviceController.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/arstack_studio/src/DeviceController.cpp b/apps/arstack_studio/src/DeviceController.cpp index 2ae244ee8..39a130b75 100644 --- a/apps/arstack_studio/src/DeviceController.cpp +++ b/apps/arstack_studio/src/DeviceController.cpp @@ -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}; @@ -2123,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; @@ -2250,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("-"); From 59f7165a0471ae4ad9ca3d3c3fe43ce579b47878 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:27:21 +0700 Subject: [PATCH 12/15] feat(ui): display real ESP32-P4 driver/PHY readiness directly in Advanced Time Sync --- apps/arstack_studio/qml/PtpExpertPanel.qml | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/apps/arstack_studio/qml/PtpExpertPanel.qml b/apps/arstack_studio/qml/PtpExpertPanel.qml index 8d0ad365e..67f1cd4fc 100644 --- a/apps/arstack_studio/qml/PtpExpertPanel.qml +++ b/apps/arstack_studio/qml/PtpExpertPanel.qml @@ -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 From 37cabe90d776db29884c64642bfc34468b8045a4 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:27:33 +0700 Subject: [PATCH 13/15] feat(diagnostics): include exact board ETH driver and physical PHY link state --- apps/arstack_studio/src/SmartSessionController.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/arstack_studio/src/SmartSessionController.cpp b/apps/arstack_studio/src/SmartSessionController.cpp index c699d538f..96c83c686 100644 --- a/apps/arstack_studio/src/SmartSessionController.cpp +++ b/apps/arstack_studio/src/SmartSessionController.cpp @@ -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()); From 8033976ecbccb128b953a733d5ad5cb6c226b5ce Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:27:49 +0700 Subject: [PATCH 14/15] test(studio): verify PTP board ETH driver/link state on live GUI readback --- .../src/DeterministicSessionHarness.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/arstack_studio/src/DeterministicSessionHarness.cpp b/apps/arstack_studio/src/DeterministicSessionHarness.cpp index 434e0a4e4..2663d7d3a 100644 --- a/apps/arstack_studio/src/DeterministicSessionHarness.cpp +++ b/apps/arstack_studio/src/DeterministicSessionHarness.cpp @@ -748,6 +748,21 @@ class DeterministicSessionHarness final { } 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; From e44cf25099841e22268e394209767742b9499179 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 10:28:02 +0700 Subject: [PATCH 15/15] test(contract): enforce visible PTP PHY state and diagnostics, not only hidden console proof --- apps/arstack_studio/tests/check_operator_workspace.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index b72df0a7c..4422674b8 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -496,6 +496,11 @@ def main() -> None: 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, "