Skip to content

chore: stage v0.1.1 lifecycle and PTP stabilization #1

chore: stage v0.1.1 lifecycle and PTP stabilization

chore: stage v0.1.1 lifecycle and PTP stabilization #1

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

Check failure on line 125 in .github/workflows/stabilize-v011-runtime.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/stabilize-v011-runtime.yml

Invalid workflow file

You have an error in your yaml syntax on line 125
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