Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fd7aeae
feat(sv): fast SOURCE-only AUTO lock after real phase and MAC evidenc…
masarray Oct 22, 2026
47738d3
fix(esp32): SOURCE AUTO uses measured self-clock fast acquisition, ex…
masarray Oct 22, 2026
79e2224
test(sv): self-GM AUTO lock at second live measured phase/MAC epoch a…
masarray Oct 22, 2026
d34e909
feat(sv): expose SOURCE_FAST acquisition progress and clear gate on p…
masarray Oct 22, 2026
1846fd0
ci(studio): rebuild matched Windows installer on firmware-only PTP/SV…
masarray Oct 22, 2026
017d9a8
feat(SV 9-2): deploy exact SCL smpRate/smpMod semantics on ESP32-P4
masarray Oct 22, 2026
25accab
test(SV/PTP): fix MSVC uint16 optional narrowing and update Studio fa…
masarray Oct 22, 2026
3ebe729
fix(Studio): render real SCL SmpPerSec sample mode correctly in SV wi…
masarray Oct 22, 2026
3acd78c
feat(PTP): external GM candidate admission and break-before-make fail…
masarray Oct 22, 2026
84d39cf
feat(PTP): observe real external GM Announce on SOURCE Ethernet RX, e…
masarray Oct 22, 2026
e35169b
fix(PTP policy): immediate qualified external switch; anti-flap coold…
masarray Oct 22, 2026
4684f78
feat(PTP): exclusive external-GM failover supervisor with physical st…
masarray Oct 22, 2026
6aa72b0
fix(PTP): declare shared physical EMAC handle before exclusive-role s…
masarray Oct 22, 2026
a246117
test(PTP): register real external failover CTest cases and guard opti…
masarray Oct 22, 2026
1f35d13
feat(Studio): opt-in external GM AUTO source takeover with firmware-b…
masarray Oct 22, 2026
1a7ff2b
feat(PTP): allow passive GM announcement observation while failover d…
masarray Oct 22, 2026
44e7d40
feat(PTP): passively report stable external GM before opt-in without …
masarray Oct 22, 2026
50aeb61
feat(Studio): display passive external GM detection while keeping swi…
masarray Oct 22, 2026
9f3067a
fix(PTP): block concurrent manual START during supervised EMAC clock …
masarray Oct 22, 2026
692d5cf
fix(PTP AUTO): retry stranded local SOURCE with bounded backoff after…
masarray Oct 22, 2026
056c7bf
feat(PTP): surface actual external GM identity, port and class in con…
masarray Oct 22, 2026
c0f3b4e
test(PTP): enforce and regression-test real external Announce domain/…
masarray Oct 22, 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
4 changes: 4 additions & 0 deletions .github/workflows/arstack-studio-native-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- 'apps/arstack_studio/**'
# Firmware-only timing fixes MUST create a matched ESP32 image and
# Windows Inno installer; do not leave users on stale embedded firmware.
- 'embedded/esp32p4_smv_injector/**'
- 'include/ariec61850/time_sync/**'
- '.github/workflows/arstack-studio-native-preview.yml'

permissions:
Expand Down
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,13 @@ if(ARIEC61850_BUILD_TESTS)
add_test(NAME ariec61850_${test_name}_tests COMMAND ariec61850_${test_name}_tests)
endforeach()

add_executable(ariec61850_ptp_external_failover_tests tests/test_ptp_external_failover.cpp)
target_link_libraries(ariec61850_ptp_external_failover_tests PRIVATE ARIEC61850::core)
target_compile_features(ariec61850_ptp_external_failover_tests PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_ptp_external_failover_tests)
ariec61850_apply_sanitizers(ariec61850_ptp_external_failover_tests)
add_test(NAME ariec61850_ptp_external_failover_tests COMMAND ariec61850_ptp_external_failover_tests)

add_executable(ariec61850_live_signal_tests tests/test_live_signal.cpp)
target_link_libraries(ariec61850_live_signal_tests PRIVATE ARIEC61850::core)
target_compile_features(ariec61850_live_signal_tests PRIVATE cxx_std_20)
Expand Down
12 changes: 12 additions & 0 deletions apps/arstack_studio/qml/ProfileInspector.qml
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,18 @@ SurfacePanel {
font.family: panel.monoFont
font.pixelSize: panel.theme.captionSize
}
Label { text: "ASDU smpRate/smpMod"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: panel.theme.captionSize }
Label {
text: panel.profile.includeSampleRate
? (panel.profile.sampleRate + " / "
+ (panel.profile.sampleMode === "SmpPerPeriod" ? "0 · per period"
: panel.profile.sampleMode === "SmpPerSec" ? "1 · per second"
: "unsupported"))
: "omitted · SCL SmvOpts"
color: panel.theme.textSoft
font.family: panel.monoFont
font.pixelSize: panel.theme.captionSize
}
Label { text: "Frames"; color: panel.theme.muted; font.family: panel.uiFont; font.pixelSize: panel.theme.captionSize }
Label {
text: panel.profile.frameRateHz
Expand Down
61 changes: 61 additions & 0 deletions apps/arstack_studio/qml/PtpExpertPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,67 @@ SurfacePanel {
}
}

RowLayout {
visible: panel.device && panel.device.ptpExternalAutoAvailable
Layout.fillWidth: true
spacing: 8
ColumnLayout {
Layout.fillWidth: true
spacing: 3
Label {
text: "External GM · opt-in AUTO failover"
font.family: panel.uiFont
color: panel.theme.text
font.pixelSize: panel.theme.captionSize
}
Label {
Layout.fillWidth: true
text: panel.device.ptpExternalSwitching
? "Switching EMAC authority · SV synchronization temporarily revoked"
: panel.device.ptpExternalAutoEnabled
? "External Announce " +
(panel.device.ptpExternalCandidateReady ? "qualified" : "acquiring") +
" · " + panel.device.ptpExternalStableAnnounces + "/3" +
(panel.device.ptpExternalCandidateReady
? " · GM " + panel.device.ptpExternalCandidateGm +
" class " + panel.device.ptpExternalCandidateClass
: "") + " · receiver LOCK must be measured"
: (panel.device.ptpRole === "RECEIVER"
? "Off · Receiver stays active until Stop PTP"
: "Off · local SOURCE selected") + " · " +
(panel.device.ptpExternalCandidateReady ? "external PTP Announce detected" : "detecting external GM")
font.family: panel.uiFont
color: panel.theme.textSoft
font.pixelSize: panel.theme.captionSize
wrapMode: Text.WordWrap
}
}
CalmButton {
theme: panel.theme
uiFont: panel.uiFont
text: panel.device.ptpExternalAutoEnabled ? "Disable AUTO" : "Enable AUTO"
enabled: panel.session && !panel.startPending &&
!panel.device.ptpExternalSwitching &&
!panel.session.updatingFirmware &&
(panel.device.ptpExternalAutoEnabled ||
(panel.device.ptpRunning && panel.device.ptpRole === "SOURCE"))
onClicked: {
if (!panel.session.requestPtpExternalAuto(!panel.device.ptpExternalAutoEnabled)
&& panel.controller)
panel.controller.showMessage(
"External PTP AUTO requires running SOURCE, matching VLAN/domain and verified firmware.", true)
}
}
Label {
text: "to RX " + panel.device.ptpExternalHandovers +
" · local " + panel.device.ptpExternalReturns +
" · fail " + panel.device.ptpExternalFailures
font.family: panel.monoFont
color: panel.theme.muted
font.pixelSize: panel.theme.captionSize
}
}

StudioDarkCheckBox {
id: benchGmCheck
Layout.fillWidth: true
Expand Down
48 changes: 48 additions & 0 deletions apps/arstack_studio/src/DeviceController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ const QRegularExpression kPtpEthernetExpression{
const QRegularExpression kPtpConfigExpression{
QStringLiteral("PTP config domain=(\\d+) transportSpecific=0x([0-9A-Fa-f]+) VLAN=(\\S+) Announce=(\\d+) ms Sync=(\\d+) ms Pdelay=(ON|OFF)"),
QRegularExpression::CaseInsensitiveOption};
const QRegularExpression kPtpExternalAutoExpression{
QStringLiteral("PTP AUTOSWITCH enabled=([01]) candidateFresh=([01]) stableAnnounces=(\\d+) switching=([01]) handovers=(\\d+) returns=(\\d+) failures=(\\d+) candidateGM=([A-Fa-f0-9]{16})/(\\d+) class=(\\d+)"),
QRegularExpression::CaseInsensitiveOption};
const QRegularExpression kPtpRoleExpression{
QStringLiteral("PTPROLE role=(SOURCE|RECEIVER|MONITOR) value=(\\d+) running=([01])"),
QRegularExpression::CaseInsensitiveOption};
Expand Down Expand Up @@ -1320,6 +1323,23 @@ bool DeviceController::setCtSaturation(
}

bool DeviceController::sendPtpShow() { return sendCommand(QStringLiteral("PTP SHOW")); }
bool DeviceController::sendPtpExternalAutoShow() {
return ptpExternalAutoAvailable() &&
sendQuietCommand(QStringLiteral("PTP AUTOSWITCH SHOW"));
}
bool DeviceController::setPtpExternalAuto(const bool enabled) {
if (!deviceVerified_ || !connected_ || !ioWorkerReady_ ||
!ptpExternalAutoAvailable() || ptpStarting() || profileDeploying_) {
setError(QStringLiteral("PTP external AUTO is not available in this verified firmware/session."));
return false;
}
if (enabled && (!ptpRunning_ || ptpRole_ != QStringLiteral("SOURCE"))) {
setError(QStringLiteral("Start and verify PTP SOURCE before enabling external AUTO switchover."));
return false;
}
// No optimistic GUI LOCK or switchover state. Next readback is firmware-owned.
return sendCommand(QStringLiteral("PTP AUTOSWITCH %1").arg(enabled ? 1 : 0));
}
bool DeviceController::startPtp() {
// Legacy direct START must not bypass a guided START, in-flight STOP,
// or an unverified cancellation. Both paths share this device authority.
Expand Down Expand Up @@ -2253,6 +2273,25 @@ void DeviceController::processLine(const QString& rawLine) {
emit ptpStateChanged();
}

match = kPtpExternalAutoExpression.match(line);
if (match.hasMatch()) {
ptpExternalAutoEnabled_ = match.captured(1) == QStringLiteral("1");
ptpExternalCandidateReady_ = match.captured(2) == QStringLiteral("1");
ptpExternalStableAnnounces_ = match.captured(3);
ptpExternalSwitching_ = match.captured(4) == QStringLiteral("1");
ptpExternalHandovers_ = match.captured(5);
ptpExternalReturns_ = match.captured(6);
ptpExternalFailures_ = match.captured(7);
const QString gm = match.captured(8).toUpper();
ptpExternalCandidateGm_ = gm == QStringLiteral("0000000000000000")
? QStringLiteral("NONE") : gm + QStringLiteral("/") + match.captured(9);
ptpExternalCandidateClass_ = match.captured(10);
emit ptpStateChanged();
}
if (line.contains(QStringLiteral("PTP AUTOSWITCH accepted"), Qt::CaseInsensitive)) {
static_cast<void>(sendPtpExternalAutoShow());
}

match = kPtpRoleExpression.match(line);
if (match.hasMatch()) {
ptpAvailable_ = true;
Expand Down Expand Up @@ -2369,6 +2408,15 @@ void DeviceController::resetPtpState() {
ptpFollowUpSent_ = QStringLiteral("-");
ptpPdelayFrames_ = QStringLiteral("-");
ptpTxFailures_ = QStringLiteral("-");
ptpExternalAutoEnabled_ = false;
ptpExternalCandidateReady_ = false;
ptpExternalSwitching_ = false;
ptpExternalStableAnnounces_ = QStringLiteral("0");
ptpExternalCandidateGm_ = QStringLiteral("NONE");
ptpExternalCandidateClass_ = QStringLiteral("-");
ptpExternalHandovers_ = QStringLiteral("0");
ptpExternalReturns_ = QStringLiteral("0");
ptpExternalFailures_ = QStringLiteral("0");
ptpRole_ = QStringLiteral("SOURCE");
ptpDiscipline_ = QStringLiteral("UNLOCKED");
ptpSource_ = QStringLiteral("NONE");
Expand Down
34 changes: 34 additions & 0 deletions apps/arstack_studio/src/DeviceController.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ class DeviceController : public QObject {
Q_PROPERTY(QString ptpFollowUpSent READ ptpFollowUpSent NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpPdelayFrames READ ptpPdelayFrames NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpTxFailures READ ptpTxFailures NOTIFY ptpStateChanged)
Q_PROPERTY(bool ptpExternalAutoAvailable READ ptpExternalAutoAvailable NOTIFY ptpStateChanged)
Q_PROPERTY(bool ptpExternalAutoEnabled READ ptpExternalAutoEnabled NOTIFY ptpStateChanged)
Q_PROPERTY(bool ptpExternalCandidateReady READ ptpExternalCandidateReady NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpExternalStableAnnounces READ ptpExternalStableAnnounces NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpExternalCandidateGm READ ptpExternalCandidateGm NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpExternalCandidateClass READ ptpExternalCandidateClass NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpExternalHandovers READ ptpExternalHandovers NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpExternalReturns READ ptpExternalReturns NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpExternalFailures READ ptpExternalFailures NOTIFY ptpStateChanged)
Q_PROPERTY(bool ptpExternalSwitching READ ptpExternalSwitching NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpRole READ ptpRole NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpDiscipline READ ptpDiscipline NOTIFY ptpStateChanged)
Q_PROPERTY(QString ptpSource READ ptpSource NOTIFY ptpStateChanged)
Expand Down Expand Up @@ -177,6 +187,19 @@ class DeviceController : public QObject {
[[nodiscard]] QString ptpFollowUpSent() const { return ptpFollowUpSent_; }
[[nodiscard]] QString ptpPdelayFrames() const { return ptpPdelayFrames_; }
[[nodiscard]] QString ptpTxFailures() const;
[[nodiscard]] bool ptpExternalAutoAvailable() const noexcept {
return deviceVerified_ && identity_.capabilities.contains(
QStringLiteral("PTP-EXTERNAL-AUTO-V1"), Qt::CaseInsensitive);
}
[[nodiscard]] bool ptpExternalAutoEnabled() const noexcept { return ptpExternalAutoEnabled_; }
[[nodiscard]] bool ptpExternalCandidateReady() const noexcept { return ptpExternalCandidateReady_; }
[[nodiscard]] QString ptpExternalStableAnnounces() const { return ptpExternalStableAnnounces_; }
[[nodiscard]] QString ptpExternalCandidateGm() const { return ptpExternalCandidateGm_; }
[[nodiscard]] QString ptpExternalCandidateClass() const { return ptpExternalCandidateClass_; }
[[nodiscard]] QString ptpExternalHandovers() const { return ptpExternalHandovers_; }
[[nodiscard]] QString ptpExternalReturns() const { return ptpExternalReturns_; }
[[nodiscard]] QString ptpExternalFailures() const { return ptpExternalFailures_; }
[[nodiscard]] bool ptpExternalSwitching() const noexcept { return ptpExternalSwitching_; }
[[nodiscard]] QString ptpRole() const { return ptpRole_; }
[[nodiscard]] QString ptpDiscipline() const { return ptpDiscipline_; }
[[nodiscard]] QString ptpSource() const { return ptpSource_; }
Expand Down Expand Up @@ -254,6 +277,8 @@ class DeviceController : public QObject {
void abandonProfileDeployment();

bool sendPtpShow();
bool sendPtpExternalAutoShow();
bool setPtpExternalAuto(bool enabled);
bool startPtp();
bool startPtpConfigured(const QString& role, const QVariantMap& profile);
bool stopPtp();
Expand Down Expand Up @@ -479,6 +504,15 @@ class DeviceController : public QObject {
QString ptpFollowUpSent_{QStringLiteral("-")};
QString ptpPdelayFrames_{QStringLiteral("-")};
QString ptpTxFailures_{QStringLiteral("-")};
bool ptpExternalAutoEnabled_{false};
bool ptpExternalCandidateReady_{false};
bool ptpExternalSwitching_{false};
QString ptpExternalStableAnnounces_{QStringLiteral("0")};
QString ptpExternalCandidateGm_{QStringLiteral("NONE")};
QString ptpExternalCandidateClass_{QStringLiteral("-")};
QString ptpExternalHandovers_{QStringLiteral("0")};
QString ptpExternalReturns_{QStringLiteral("0")};
QString ptpExternalFailures_{QStringLiteral("0")};
QString ptpRole_{QStringLiteral("SOURCE")};
QString ptpDiscipline_{QStringLiteral("UNLOCKED")};
QString ptpSource_{QStringLiteral("NONE")};
Expand Down
8 changes: 8 additions & 0 deletions apps/arstack_studio/src/SmartSessionController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -455,9 +455,17 @@ bool SmartSessionController::requestPtpRefresh() {
device_->scenarioApplying()) return false;
const bool ptp = device_->sendPtpShow();
const bool sync = device_->sendSmpSynchShow();
if (device_->ptpExternalAutoAvailable())
static_cast<void>(device_->sendPtpExternalAutoShow());
return ptp && sync;
}

bool SmartSessionController::requestPtpExternalAuto(const bool enabled) {
if (!deviceControlAvailable() || device_ == nullptr || updateRequested_ ||
device_->ptpStarting() || device_->profileDeploying()) return false;
return device_->setPtpExternalAuto(enabled);
}

bool SmartSessionController::requestSyncDiagnostics() {
// Read-only operator request; serialize behind the existing device
// session. Never interleave with START/CFG or firmware port handoff.
Expand Down
1 change: 1 addition & 0 deletions apps/arstack_studio/src/SmartSessionController.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ class SmartSessionController : public QObject {
double clipPercent);
Q_INVOKABLE bool requestZero();
Q_INVOKABLE bool requestPtpRefresh();
Q_INVOKABLE bool requestPtpExternalAuto(bool enabled);
Q_INVOKABLE bool requestSyncDiagnostics();
Q_INVOKABLE bool requestPtpRole(const QString& role);
Q_INVOKABLE bool requestSmpSynch(const QString& mode);
Expand Down
10 changes: 7 additions & 3 deletions apps/arstack_studio/tests/check_operator_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,20 @@ def main() -> None:
and "local_acquisition.observe_preflight(" in firmware_source
and "local_acquisition.observe_live(" in firmware_source
and "local_acquisition.reset();" in firmware_source
and "live_clean=%lu/%lu warm_phase=%lu/%lu live_age=%lu" in firmware_source
and "live_clean=%lu/%lu source_fast=%lu/%lu warm_phase=%lu/%lu" in firmware_source
and "SvSourceSelfClockAcquisition source_fast_acquisition;" in firmware_source
and "source_fast_acquisition.observe(" in firmware_source
and "source_fast_acquisition.reset();" in firmware_source
and "check_fast_source_auto_qualification_with_fresh_evidence()" in sv_tests
and "kRequiredCleanEpochs = 10U" in sv_gate
and "kWarmRequiredLiveEpochs = 3U" in sv_gate
and "kMaxWarmMacAcquisitionEpochs = 8U" in sv_gate
and "if (had_mac_evidence_) reset();" in sv_gate
and "check_no_intermitttent_local_at_start()" in sv_tests
and "check_warm_phase_preflight_cannot_fake_canonical_mac()" in sv_tests
and "check_warm_preflight_negative_session_and_clock_faults()" in sv_tests,
"LOCAL REF: ten LIVE epochs for COLD, ten stable phase-only PREFLIGHT epochs "
"plus three verified LIVE MAC epochs for WARM; invalidate on loss of evidence")
"LOCAL REF: retain conservative COLD/WARM acquisition and require measured "
"SOURCE_FAST MAC plus consecutive phase epochs; invalidate on loss of evidence")
phase_header = (project_root / "include/ariec61850/time_sync/sv_ptp_phase_authority.hpp").read_text(encoding="utf-8")
phase_tests = (project_root / "tests/test_sv_ptp_phase_authority.cpp").read_text(encoding="utf-8")
require("g_preflight_phase_clock_active" in firmware_source
Expand Down
Loading
Loading