Repository navigation
chore: stage v0.1.1 lifecycle and PTP stabilization #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Stage v0.1.1 lifecycle and PTP stabilization | ||
| on: | ||
| push: | ||
| branches: | ||
| - feature/ptp-production-v0.1.1 | ||
| permissions: | ||
| contents: write | ||
| jobs: | ||
| stabilize: | ||
| if: github.actor != 'github-actions[bot]' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| ref: feature/ptp-production-v0.1.1 | ||
| fetch-depth: 0 | ||
| - name: Integrate bounded lifecycle ownership and stabilize PTP source UX | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
| # Carry the already-reviewed lifecycle ownership patch into the one | ||
| # v0.1.1 candidate. Keep the safety branch/PR intact for provenance. | ||
| if ! git log --format=%s | grep -Fxq 'fix(studio): retire workers before GUI event loop exits'; then | ||
| git cherry-pick 75e5f2dfd1fcf382bb2698a55ce9fcff57413390 | ||
| fi | ||
| python - <<'PY' | ||
| from pathlib import Path | ||
| def replace_once(path, old, new, label): | ||
| p = Path(path) | ||
| text = p.read_text(encoding='utf-8') | ||
| if old not in text: | ||
| raise SystemExit(f'missing anchor {label} in {path}') | ||
| if text.count(old) != 1: | ||
| raise SystemExit(f'expected one {label} in {path}, got {text.count(old)}') | ||
| p.write_text(text.replace(old, new, 1), encoding='utf-8') | ||
| # Normal operator close must request application shutdown directly. | ||
| replace_once( | ||
| 'apps/arstack_studio/qml/Main.qml', | ||
| ''' title: "ARStack Studio · SMV Injector"\n''', | ||
| ''' title: "ARStack Studio · SMV + PTP · v" + Qt.application.version\n''', | ||
| 'window title') | ||
| replace_once( | ||
| 'apps/arstack_studio/qml/Main.qml', | ||
| ''' property bool telemetryExpanded: false\n\n font.family: root.uiFont\n\n // Graceful window close is still a safety boundary. QML submits only the\n // Stop intent; the supervisor owns whether the current session may act.\n onClosing: function(close) {\n if (workflowBar.session && workflowBar.session.state === "RUNNING")\n workflowBar.session.requestStop()\n }\n''', | ||
| ''' property bool telemetryExpanded: false\n property bool applicationShutdownRequested: false\n\n font.family: root.uiFont\n\n // The main window is the application lifetime boundary. Do not queue a\n // second asynchronous STOP from QML while C++ teardown owns STOP/COM close.\n // Explicit Qt.quit() also prevents hidden Advanced/detached windows from\n // keeping the GUI process alive after the operator closes the main window.\n onClosing: function(close) {\n close.accepted = true\n if (root.applicationShutdownRequested)\n return\n root.applicationShutdownRequested = true\n configurationWindow.hide()\n detachedPhasorWindow.hide()\n detachedWaveformWindow.hide()\n Qt.quit()\n }\n''', | ||
| 'normal close boundary') | ||
| # PTP must never auto-start before esp_eth_start/link readiness. | ||
| p = Path('embedded/esp32p4_smv_injector/main/ethernet_port.c') | ||
| text = p.read_text(encoding='utf-8') | ||
| auto = '''\n#if CONFIG_AR_PTP_LAB_TX\n // Boot auto-start uses the same checked admission/readiness contract as the\n // live serial control path. A rejected or failed start is never reported as\n // running merely because an older runtime is still cleaning up.\n if (!ar_ptp_lab_try_start(handle)) {\n ESP_LOGW(TAG, "PTP auto-start was rejected or could not start");\n }\n#endif\n''' | ||
| if auto not in text: | ||
| raise SystemExit('missing PTP auto-start block') | ||
| text = text.replace(auto, '\n', 1) | ||
| old_start = '''bool ar_esp32p4_ptp_start(void)\n{\n#if CONFIG_AR_PTP_LAB_TX\n if (s_eth_handle == NULL) {\n return false;\n }\n return ar_ptp_lab_try_start(s_eth_handle);\n#else\n return false;\n#endif\n}\n''' | ||
| new_start = '''bool ar_esp32p4_ptp_start(void)\n{\n#if CONFIG_AR_PTP_LAB_TX\n if (s_eth_handle == NULL) {\n ESP_LOGE(TAG, "PTP start rejected: Ethernet driver is not initialized");\n return false;\n }\n bool link_up = false;\n const esp_err_t link_result = esp_eth_ioctl(s_eth_handle, ETH_CMD_G_LINK, &link_up);\n if (link_result != ESP_OK || !link_up) {\n ESP_LOGE(TAG, "PTP start rejected: Ethernet link is not up");\n return false;\n }\n return ar_ptp_lab_try_start(s_eth_handle);\n#else\n return false;\n#endif\n}\n''' | ||
| if old_start not in text: | ||
| raise SystemExit('missing ar_esp32p4_ptp_start anchor') | ||
| p.write_text(text.replace(old_start, new_start, 1), encoding='utf-8') | ||
| # Source RUNNING means frames have actually left the MAC, not merely\n # that a FreeRTOS task was created. | ||
| p = Path('embedded/esp32p4_smv_injector/main/ptp_lab_task.cpp') | ||
| text = p.read_text(encoding='utf-8') | ||
| text = text.replace( | ||
| 'std::atomic_bool g_ptp_started{false};\nstd::atomic_bool g_stop_requested{false};', | ||
| 'std::atomic_bool g_ptp_started{false};\nstd::atomic_bool g_ptp_ready{false};\nstd::atomic_bool g_stop_requested{false};', 1) | ||
| text = text.replace( | ||
| ' context.task_handle = nullptr;\n g_ptp_started.store(false, std::memory_order_release);', | ||
| ' context.task_handle = nullptr;\n g_ptp_ready.store(false, std::memory_order_release);\n g_ptp_started.store(false, std::memory_order_release);', 1) | ||
| old_success = ''' if (success) {\n consecutive_failures = 0U;\n context.runtime->clear_error();\n } else {\n''' | ||
| new_success = ''' if (success) {\n consecutive_failures = 0U;\n context.runtime->clear_error();\n const bool emitted_core_timing =\n g_announce_sent.load(std::memory_order_relaxed) > 0U &&\n g_sync_sent.load(std::memory_order_relaxed) > 0U &&\n g_follow_up_sent.load(std::memory_order_relaxed) > 0U;\n if (emitted_core_timing) {\n g_ptp_ready.store(true, std::memory_order_release);\n }\n } else {\n''' | ||
| if old_success not in text: | ||
| raise SystemExit('missing PTP success block') | ||
| text = text.replace(old_success, new_success, 1) | ||
| text = text.replace( | ||
| ' reset_live_status();\n g_ptp_accept_rx.store(false, std::memory_order_release);', | ||
| ' reset_live_status();\n g_ptp_ready.store(false, std::memory_order_release);\n g_ptp_accept_rx.store(false, std::memory_order_release);', 1) | ||
| old_create = ''' if (xTaskCreatePinnedToCore(\n &ptp_lab_task,\n "ar_ptp_lab",\n 7168,\n &g_ptp_context,\n 4,\n &g_ptp_context.task_handle,\n 0) != pdPASS) {\n g_ptp_context.task_handle = nullptr;\n g_ptp_started.store(false, std::memory_order_release);\n ESP_LOGE(kTag, "Failed to create PTP lab task on CPU0");\n }\n}\n''' | ||
| new_create = ''' if (xTaskCreatePinnedToCore(\n &ptp_lab_task,\n "ar_ptp_lab",\n 7168,\n &g_ptp_context,\n 4,\n &g_ptp_context.task_handle,\n 0) != pdPASS) {\n g_ptp_context.task_handle = nullptr;\n g_ptp_started.store(false, std::memory_order_release);\n ESP_LOGE(kTag, "Failed to create PTP lab task on CPU0");\n return;\n }\n\n // The serial command path is not realtime. Spend a bounded interval proving\n // that SOURCE emitted Announce + Sync + Follow_Up before reporting RUNNING.\n constexpr unsigned kStartReadyPolls = 160U;\n for (unsigned attempt = 0U; attempt < kStartReadyPolls; ++attempt) {\n if (g_ptp_ready.load(std::memory_order_acquire) ||\n !g_ptp_started.load(std::memory_order_acquire)) {\n return;\n }\n vTaskDelay(pdMS_TO_TICKS(10));\n }\n ESP_LOGE(kTag, "PTP source readiness timeout: no verified Announce/Sync/Follow_Up TX");\n stop_ptp_lab();\n}\n''' | ||
| if old_create not in text: | ||
| raise SystemExit('missing PTP task create block') | ||
| text = text.replace(old_create, new_create, 1) | ||
| text = text.replace( | ||
| '''[[nodiscard]] bool ptp_lab_is_running() noexcept {\n return g_ptp_started.load(std::memory_order_acquire);\n}\n''', | ||
| '''[[nodiscard]] bool ptp_lab_is_running() noexcept {\n return g_ptp_ready.load(std::memory_order_acquire);\n}\n''', 1) | ||
| text = text.replace( | ||
| ' status->is_running = ar::esp32p4::smv::g_ptp_started.load(std::memory_order_acquire);', | ||
| ' status->is_running = ar::esp32p4::smv::g_ptp_ready.load(std::memory_order_acquire);', 1) | ||
| p.write_text(text, encoding='utf-8') | ||
| replace_once( | ||
| 'embedded/esp32p4_smv_injector/main/live_control.cpp', | ||
| ''' if (!ar_esp32p4_ptp_start()) ESP_LOGE(kTag, "PTP start rejected: Ethernet is unavailable");\n else ESP_LOGI(kTag, "PTP start accepted");\n''', | ||
| ''' if (!ar_esp32p4_ptp_start()) {\n ESP_LOGE(kTag, "PTP start rejected: Ethernet link/source readiness failed; no verified timing frames were emitted");\n } else {\n ESP_LOGI(kTag, "PTP start accepted: verified Announce/Sync/Follow_Up transmission");\n }\n''', | ||
| 'PTP start truth message') | ||
| # Surface semantic PTP failures in Studio immediately. | ||
| p = Path('apps/arstack_studio/src/DeviceController.cpp') | ||
| text = p.read_text(encoding='utf-8') | ||
| anchor = ''' if (line.contains(QStringLiteral("PTP configuration accepted"), Qt::CaseInsensitive)) {\n emit deviceMessage(QStringLiteral("PTP expert profile accepted."));\n static_cast<void>(sendPtpShow());\n }\n''' | ||
| addition = anchor + ''' if (line.contains(QStringLiteral("PTP start accepted"), Qt::CaseInsensitive)) {\n ptpStatus_ = QStringLiteral("Timing frames verified");\n emit deviceMessage(QStringLiteral("PTP source is transmitting verified timing frames."));\n static_cast<void>(sendPtpShow());\n }\n if (line.contains(QStringLiteral("PTP start rejected"), Qt::CaseInsensitive) ||\n line.contains(QStringLiteral("PTP source readiness timeout"), Qt::CaseInsensitive)) {\n ptpRunning_ = false;\n ptpStatus_ = QStringLiteral("PTP start failed");\n emit ptpStateChanged();\n setError(QStringLiteral("PTP source did not emit timing frames. Check the Ethernet link and retry Start PTP Source."));\n }\n''' | ||
| if anchor not in text: | ||
| raise SystemExit('missing DeviceController PTP accepted anchor') | ||
| p.write_text(text.replace(anchor, addition, 1), encoding='utf-8') | ||
| # Pass the controller so the timing panel can use the common toast/status path. | ||
| replace_once( | ||
| 'apps/arstack_studio/qml/ConfigurationHub.qml', | ||
| ''' PtpExpertPanel {\n theme: hub.theme\n device: hub.device\n session: hub.session\n''', | ||
| ''' PtpExpertPanel {\n theme: hub.theme\n controller: hub.controller\n device: hub.device\n session: hub.session\n''', | ||
| 'PTP controller binding') | ||
| # Replace expert-first timing page with a product-first control surface. | ||
| Path('apps/arstack_studio/qml/PtpExpertPanel.qml').write_text(r'''// SPDX-License-Identifier: GPL-3.0-or-later | ||
| import QtQuick | ||
| import QtQuick.Controls | ||
| import QtQuick.Layouts | ||
| SurfacePanel { | ||
| id: panel | ||
| property var controller | ||
| property var device | ||
| property var session | ||
| property string uiFont: "Inter" | ||
| property string monoFont: "Inter" | ||
| property var roleValues: ["SOURCE", "RECEIVER", "MONITOR"] | ||
| property bool advancedVisible: false | ||
| property bool startPending: false | ||
| function roleIndex(role) { | ||
| if (role === "RECEIVER") return 1 | ||
| if (role === "MONITOR") return 2 | ||
| return 0 | ||
| } | ||
| function syncIndex(mode) { | ||
| if (mode === "FORCE_0") return 1 | ||
| if (mode === "FORCE_1_LOCAL") return 2 | ||
| if (mode === "FORCE_2_GLOBAL") return 3 | ||
| return 0 | ||
| } | ||
| function applyProfile() { | ||
| if (!panel.session) return false | ||
| return panel.session.requestConfigurePtp({ | ||
| domain: Number(domainField.text), | ||
| transportSpecific: Number(transportField.text), | ||
| vlanEnabled: vlanCheck.checked, | ||
| vlanId: Number(vlanField.text), | ||
| vlanPriority: Number(pcpField.text), | ||
| announceIntervalMs: Number(announceField.text), | ||
| syncIntervalMs: Number(syncField.text), | ||
| respondToPeerDelay: pdelayCheck.checked | ||
| }) | ||
| } | ||
| function startSelectedMode() { | ||
| if (!panel.session || !panel.device.ptpAvailable) return | ||
| var role = panel.roleValues[modeCombo.currentIndex] | ||
| if (!panel.session.requestPtpRole(role)) { | ||
| if (panel.controller) panel.controller.showMessage("PTP mode could not be selected.", true) | ||
| return | ||
| } | ||
| if (!panel.applyProfile()) { | ||
| if (panel.controller) panel.controller.showMessage("PTP settings were rejected before start.", true) | ||
| return | ||
| } | ||
| panel.startPending = true | ||
| if (!panel.session.requestStartPtp()) { | ||
| panel.startPending = false | ||
| if (panel.controller) panel.controller.showMessage("PTP start command could not be sent.", true) | ||
| } | ||
| } | ||
| readonly property string role: panel.device.ptpRole | ||
| readonly property bool sourceRole: role === "SOURCE" | ||
| readonly property bool receiverRole: role === "RECEIVER" | ||
| readonly property bool monitorRole: role === "MONITOR" | ||
| readonly property bool sourceEvidence: Number(panel.device.ptpAnnounceSent) > 0 | ||
| && Number(panel.device.ptpSyncSent) > 0 | ||
| && Number(panel.device.ptpFollowUpSent) > 0 | ||
| && Number(panel.device.ptpTxFailures) === 0 | ||
| readonly property string headlineState: panel.startPending ? "STARTING" | ||
| : panel.device.ptpRunning | ||
| ? (panel.receiverRole ? panel.device.ptpDiscipline : panel.role + " ACTIVE") | ||
| : panel.device.ptpStatus === "PTP start failed" ? "START FAILED" : "OFF" | ||
| Timer { | ||
| id: statusPoll | ||
| interval: 700 | ||
| repeat: true | ||
| running: panel.startPending || panel.device.ptpRunning | ||
| onTriggered: { | ||
| if (panel.session) panel.session.requestPtpRefresh() | ||
| if (panel.device.ptpRunning) panel.startPending = false | ||
| } | ||
| } | ||
| Timer { | ||
| id: startDeadline | ||
| interval: 3500 | ||
| repeat: false | ||
| running: panel.startPending | ||
| onTriggered: { | ||
| panel.startPending = false | ||
| if (panel.session) panel.session.requestPtpRefresh() | ||
| if (!panel.device.ptpRunning && panel.controller) | ||
| panel.controller.showMessage("PTP did not reach verified transmit state. Check Ethernet link and retry.", true) | ||
| } | ||
| } | ||
| Connections { | ||
| target: panel.device | ||
| function onPtpStateChanged() { | ||
| if (panel.device.ptpRunning) { | ||
| panel.startPending = false | ||
| if (panel.controller && panel.sourceRole && panel.sourceEvidence) | ||
| panel.controller.showMessage("PTP Source active · Announce + Sync + Follow_Up verified.", false) | ||
| } | ||
| } | ||
| } | ||
| ScrollView { | ||
| anchors.fill: parent | ||
| contentWidth: availableWidth | ||
| clip: true | ||
| ColumnLayout { | ||
| width: parent.width | ||
| spacing: 12 | ||
| RowLayout { | ||
| Layout.fillWidth: true | ||
| ColumnLayout { | ||
| spacing: 2 | ||
| Label { text: "PTP TIMING"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: panel.theme.captionSize; font.weight: Font.DemiBold; font.letterSpacing: 0.9 } | ||
| Label { text: "Network timing control"; color: panel.theme.text; font.family: panel.uiFont; font.pixelSize: 18; font.weight: Font.DemiBold } | ||
| } | ||
| Item { Layout.fillWidth: true } | ||
| StateBadge { | ||
| theme: panel.theme | ||
| monoFont: panel.uiFont | ||
| state: panel.headlineState | ||
| stateColor: panel.device.ptpDiscipline === "LOCKED" ? panel.theme.green | ||
| : panel.headlineState === "START FAILED" ? panel.theme.red | ||
| : panel.device.ptpRunning ? panel.theme.green | ||
| : panel.startPending ? panel.theme.amber : panel.theme.muted | ||
| } | ||
| } | ||
| Rectangle { | ||
| Layout.fillWidth: true | ||
| implicitHeight: 74 | ||
| radius: 8 | ||
| color: panel.theme.panelAlt | ||
| border.width: 1 | ||
| border.color: panel.theme.lineSoft | ||
| RowLayout { | ||
| anchors.fill: parent | ||
| anchors.margins: 12 | ||
| spacing: 14 | ||
| ColumnLayout { | ||
| Layout.fillWidth: true | ||
| spacing: 3 | ||
| Label { text: "Mode"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 9; font.weight: Font.DemiBold } | ||
| ComboBox { | ||
| id: modeCombo | ||
| Layout.fillWidth: true | ||
| model: ["Provide PTP timing", "Follow external PTP", "Monitor PTP only"] | ||
| currentIndex: panel.roleIndex(panel.role) | ||
| enabled: panel.session && panel.device.ptpAvailable && !panel.device.ptpRunning && !panel.startPending && !panel.session.updatingFirmware | ||
| font.family: panel.uiFont | ||
| font.pixelSize: 10 | ||
| } | ||
| } | ||
| ColumnLayout { | ||
| Layout.preferredWidth: 285 | ||
| spacing: 2 | ||
| Label { | ||
| text: modeCombo.currentIndex === 0 ? "Board sends Announce + two-step Sync/Follow_Up." | ||
| : modeCombo.currentIndex === 1 ? "Board follows external PTP and reports measured lock." | ||
| : "Listen only; no timing frames are transmitted." | ||
| wrapMode: Text.WordWrap | ||
| color: panel.theme.textSoft | ||
| font.family: panel.uiFont | ||
| font.pixelSize: 9 | ||
| } | ||
| Label { | ||
| text: modeCombo.currentIndex === 0 ? "SV smpSynch stays 0 unless receiver discipline is measured." | ||
| : modeCombo.currentIndex === 1 ? "AUTO smpSynch follows measured receiver evidence." | ||
| : "No clock discipline is applied." | ||
| wrapMode: Text.WordWrap | ||
| color: panel.theme.muted | ||
| font.family: panel.uiFont | ||
| font.pixelSize: 8 | ||
| } | ||
| } | ||
| } | ||
| } | ||
| RowLayout { | ||
| Layout.fillWidth: true | ||
| spacing: 8 | ||
| CalmButton { | ||
| theme: panel.theme | ||
| uiFont: panel.uiFont | ||
| tone: "accent" | ||
| text: panel.startPending ? "Starting…" | ||
| : 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 | ||
| onClicked: panel.startSelectedMode() | ||
| } | ||
| CalmButton { | ||
| theme: panel.theme | ||
| uiFont: panel.uiFont | ||
| tone: "danger" | ||
| text: "Stop PTP" | ||
| enabled: panel.session && (panel.device.ptpRunning || panel.startPending) && !panel.session.updatingFirmware | ||
| onClicked: { | ||
| panel.startPending = false | ||
| panel.session.requestStopPtp() | ||
| } | ||
| } | ||
| CalmButton { | ||
| theme: panel.theme | ||
| uiFont: panel.uiFont | ||
| text: "Refresh" | ||
| enabled: panel.session && panel.device.deviceVerified && !panel.session.updatingFirmware | ||
| onClicked: panel.session.requestPtpRefresh() | ||
| } | ||
| Item { Layout.fillWidth: true } | ||
| CheckBox { | ||
| text: "Advanced settings" | ||
| checked: panel.advancedVisible | ||
| onToggled: panel.advancedVisible = checked | ||
| font.family: panel.uiFont | ||
| font.pixelSize: 9 | ||
| } | ||
| } | ||
| Rectangle { | ||
| Layout.fillWidth: true | ||
| implicitHeight: 96 | ||
| radius: 8 | ||
| color: panel.sourceRole && panel.device.ptpRunning && panel.sourceEvidence ? "#10251d" : panel.theme.surface2 | ||
| border.width: 1 | ||
| border.color: panel.sourceRole && panel.device.ptpRunning && panel.sourceEvidence ? "#347a59" : panel.theme.lineSoft | ||
| GridLayout { | ||
| anchors.fill: parent | ||
| anchors.margins: 11 | ||
| columns: 5 | ||
| columnSpacing: 14 | ||
| rowSpacing: 6 | ||
| Label { text: panel.sourceRole ? "Announce TX" : "Announce RX"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 8 } | ||
| Label { text: panel.sourceRole ? "Sync TX" : "Sync RX"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 8 } | ||
| Label { text: panel.sourceRole ? "Follow_Up TX" : "Follow_Up RX"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 8 } | ||
| Label { text: panel.sourceRole ? "Pdelay TX" : "Pdelay RX / Req"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 8 } | ||
| Label { text: panel.sourceRole ? "TX failures" : "Accepted / rejected"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 8 } | ||
| Label { text: panel.sourceRole ? panel.device.ptpAnnounceSent : panel.device.ptpRxAnnounce; color: panel.theme.text; font.family: panel.monoFont; font.pixelSize: 15; font.weight: Font.DemiBold } | ||
| Label { text: panel.sourceRole ? panel.device.ptpSyncSent : panel.device.ptpRxSync; color: panel.theme.text; font.family: panel.monoFont; font.pixelSize: 15; font.weight: Font.DemiBold } | ||
| Label { text: panel.sourceRole ? panel.device.ptpFollowUpSent : panel.device.ptpRxFollowUp; color: panel.theme.text; font.family: panel.monoFont; font.pixelSize: 15; font.weight: Font.DemiBold } | ||
| Label { text: panel.sourceRole ? panel.device.ptpPdelayFrames : panel.device.ptpRxPdelay + " / " + panel.device.ptpPdelayRequests; color: panel.theme.text; font.family: panel.monoFont; font.pixelSize: 15; font.weight: Font.DemiBold } | ||
| Label { text: panel.sourceRole ? panel.device.ptpTxFailures : panel.device.ptpAccepted + " / " + panel.device.ptpRejected; color: Number(panel.device.ptpTxFailures) > 0 ? panel.theme.red : panel.theme.text; font.family: panel.monoFont; font.pixelSize: 15; font.weight: Font.DemiBold } | ||
| Label { | ||
| Layout.columnSpan: 5 | ||
| text: panel.sourceRole && panel.device.ptpRunning && panel.sourceEvidence | ||
| ? "Verified on wire path: source counters are advancing with zero TX failures." | ||
| : panel.sourceRole && panel.startPending ? "Waiting for the first verified Announce + Sync + Follow_Up transmission…" | ||
| : panel.device.ptpStatus | ||
| color: panel.sourceRole && panel.device.ptpRunning && panel.sourceEvidence ? panel.theme.green : panel.theme.textSoft | ||
| font.family: panel.uiFont | ||
| font.pixelSize: 9 | ||
| } | ||
| } | ||
| } | ||
| Rectangle { | ||
| visible: panel.receiverRole | ||
| Layout.fillWidth: true | ||
| implicitHeight: 86 | ||
| radius: 8 | ||
| color: panel.theme.panelAlt | ||
| border.width: 1 | ||
| border.color: panel.theme.lineSoft | ||
| GridLayout { | ||
| anchors.fill: parent | ||
| anchors.margins: 10 | ||
| columns: 4 | ||
| Label { text: "Discipline"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 8 } | ||
| Label { text: panel.device.ptpDiscipline; color: panel.device.ptpDiscipline === "LOCKED" ? panel.theme.green : panel.theme.amber; font.family: panel.monoFont; font.pixelSize: 10; font.weight: Font.DemiBold } | ||
| Label { text: "Selected source"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 8 } | ||
| Label { text: panel.device.ptpSource === "NONE" ? "—" : panel.device.ptpSource; color: panel.theme.text; font.family: panel.monoFont; font.pixelSize: 9 } | ||
| Label { text: "Offset"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 8 } | ||
| Label { text: panel.device.ptpOffsetNs === "NA" ? "—" : panel.device.ptpOffsetNs + " ns"; color: panel.theme.text; font.family: panel.monoFont; font.pixelSize: 10 } | ||
| Label { text: "Path delay"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 8 } | ||
| Label { text: panel.device.ptpPathDelayNs === "NA" ? "—" : panel.device.ptpPathDelayNs + " ns"; color: panel.theme.text; font.family: panel.monoFont; font.pixelSize: 10 } | ||
| } | ||
| } | ||
| ColumnLayout { | ||
| visible: panel.advancedVisible | ||
| Layout.fillWidth: true | ||
| 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 } | ||
| GridLayout { | ||
| Layout.fillWidth: true | ||
| columns: 4 | ||
| columnSpacing: 10 | ||
| rowSpacing: 7 | ||
| Label { text: "Domain"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 9 } | ||
| NumericField { id: domainField; theme: panel.theme; monoFont: panel.uiFont; text: panel.device.ptpDomain === "-" ? "0" : panel.device.ptpDomain; validator: IntValidator { bottom: 0; top: 255 } } | ||
| Label { text: "transportSpecific"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 9 } | ||
| NumericField { id: transportField; theme: panel.theme; monoFont: panel.uiFont; text: panel.device.ptpTransportSpecific === "-" ? "0" : String(Number(panel.device.ptpTransportSpecific)); validator: IntValidator { bottom: 0; top: 15 } } | ||
| Label { text: "Announce interval"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 9 } | ||
| NumericField { id: announceField; theme: panel.theme; monoFont: panel.uiFont; text: "1000"; validator: IntValidator { bottom: 100; top: 10000 } } | ||
| Label { text: "Sync interval"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 9 } | ||
| NumericField { id: syncField; theme: panel.theme; monoFont: panel.uiFont; text: "250"; validator: IntValidator { bottom: 20; top: 5000 } } | ||
| CheckBox { id: vlanCheck; text: "VLAN"; font.family: panel.uiFont; font.pixelSize: 9 } | ||
| NumericField { id: vlanField; enabled: vlanCheck.checked; theme: panel.theme; monoFont: panel.uiFont; text: "100"; validator: IntValidator { bottom: 1; top: 4094 } } | ||
| Label { text: "VLAN PCP"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: 9 } | ||
| NumericField { id: pcpField; enabled: vlanCheck.checked; theme: panel.theme; monoFont: panel.uiFont; text: "4"; validator: IntValidator { bottom: 0; top: 7 } } | ||
| CheckBox { id: pdelayCheck; text: "Peer-delay response"; checked: true; font.family: panel.uiFont; font.pixelSize: 9 } | ||
| CalmButton { theme: panel.theme; uiFont: panel.uiFont; text: "Apply advanced settings"; enabled: panel.session && panel.device.ptpAvailable && !panel.device.ptpRunning; onClicked: panel.applyProfile() } | ||
| } | ||
| Rectangle { Layout.fillWidth: true; height: 1; color: panel.theme.lineSoft } | ||
| Label { text: "SV synchronization flag · lab / interoperability"; color: panel.theme.text; font.family: panel.uiFont; font.pixelSize: 12; font.weight: Font.DemiBold } | ||
| RowLayout { | ||
| Layout.fillWidth: true | ||
| ComboBox { | ||
| Layout.fillWidth: true | ||
| model: ["AUTO — measured PTP only", "0 — Not synchronized", "1 — Local synchronized (simulation)", "2 — Global synchronized (simulation)"] | ||
| currentIndex: panel.syncIndex(panel.device.smpSynchMode) | ||
| enabled: panel.session && panel.device.deviceVerified && !panel.session.updatingFirmware | ||
| font.family: panel.uiFont | ||
| font.pixelSize: 9 | ||
| onActivated: panel.session.requestSmpSynch(["AUTO", "0", "1", "2"][currentIndex]) | ||
| } | ||
| StateBadge { | ||
| theme: panel.theme | ||
| monoFont: panel.uiFont | ||
| state: "smpSynch=" + panel.device.smpSynchValue | ||
| + (panel.device.smpSynchSimulated ? " SIMULATED" : panel.device.smpSynchMeasured ? " MEASURED" : " SAFE") | ||
| stateColor: panel.device.smpSynchSimulated ? panel.theme.amber : panel.device.smpSynchMeasured ? panel.theme.green : panel.theme.muted | ||
| } | ||
| } | ||
| Label { | ||
| Layout.fillWidth: true | ||
| text: "PTP Source transmission is not proof that the SV clock is externally synchronized. AUTO remains fail-closed unless receiver discipline is measured." | ||
| wrapMode: Text.WordWrap | ||
| color: panel.theme.textSoft | ||
| font.family: panel.uiFont | ||
| font.pixelSize: 8 | ||
| } | ||
| } | ||
| Item { Layout.minimumHeight: 8 } | ||
| } | ||
| } | ||
| } | ||
| ''', encoding='utf-8') | ||
| PY | ||
| git add apps/arstack_studio/qml/Main.qml \ | ||
| apps/arstack_studio/qml/ConfigurationHub.qml \ | ||
| apps/arstack_studio/qml/PtpExpertPanel.qml \ | ||
| apps/arstack_studio/src/DeviceController.cpp \ | ||
| embedded/esp32p4_smv_injector/main/ethernet_port.c \ | ||
| embedded/esp32p4_smv_injector/main/ptp_lab_task.cpp \ | ||
| embedded/esp32p4_smv_injector/main/live_control.cpp | ||
| git diff --cached --check | ||
| if ! git diff --cached --quiet; then | ||
| git commit -m "fix(v0.1.1): verify PTP source TX and normal GUI shutdown" | ||
| fi | ||
| git push origin HEAD:feature/ptp-production-v0.1.1 | ||