Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
0fa61c3
fix(p0): decouple SV publisher lifetime from Studio heartbeat; stop o…
masarray Oct 25, 2026
30231ea
fix(build): balance PTP capability feature guards in autonomous outpu…
masarray Oct 25, 2026
2542b55
fix(p0): keep independent LAN publisher running without Studio and gu…
masarray Oct 25, 2026
2b814d9
fix(p0): keep independent LAN publisher running without Studio and gu…
masarray Oct 25, 2026
40209d5
fix(p0): keep independent LAN publisher running without Studio and gu…
masarray Oct 25, 2026
a5ef206
fix(p0): keep independent LAN publisher running without Studio and gu…
masarray Oct 25, 2026
39aaaf8
fix(p0): keep independent LAN publisher running without Studio and gu…
masarray Oct 25, 2026
64ef0bd
fix(p0): keep independent LAN publisher running without Studio and gu…
masarray Oct 25, 2026
39e8cba
fix(p0): require real Ethernet link for start, independent PHY-loss STOP
masarray Oct 25, 2026
ec1f9c5
fix(p0): require real Ethernet link for start, independent PHY-loss STOP
masarray Oct 25, 2026
f9fd16f
fix(p0): require real Ethernet link for start, independent PHY-loss STOP
masarray Oct 25, 2026
0806712
fix(p0): require real Ethernet link for start, independent PHY-loss STOP
masarray Oct 25, 2026
e7f6dec
fix(p0): require real Ethernet link for start, independent PHY-loss STOP
masarray Oct 25, 2026
1bdff76
fix(p0): require real Ethernet link for start, independent PHY-loss STOP
masarray Oct 25, 2026
100f703
fix(p0): require real Ethernet link for start, independent PHY-loss STOP
masarray Oct 25, 2026
2795595
fix(p0): make PTP wire TX PHY-aware while preserving clock continuity…
masarray Oct 25, 2026
ca8be0c
fix(p0): make PTP wire TX PHY-aware while preserving clock continuity…
masarray Oct 25, 2026
a9058fc
fix(p0): make PTP wire TX PHY-aware while preserving clock continuity…
masarray Oct 25, 2026
468e210
fix(p0): make PTP wire TX PHY-aware while preserving clock continuity…
masarray Oct 25, 2026
16528c2
fix(p0): make PTP wire TX PHY-aware while preserving clock continuity…
masarray Oct 25, 2026
f25be34
fix(p0): make PTP wire TX PHY-aware while preserving clock continuity…
masarray Oct 25, 2026
d11a301
test(p0): require new SV LAN autonomy identity and non-disruptive Stu…
masarray Oct 25, 2026
1cdeee5
test(p0): require new SV LAN autonomy identity and non-disruptive Stu…
masarray Oct 25, 2026
4039911
test(p0): require new SV LAN autonomy identity and non-disruptive Stu…
masarray Oct 25, 2026
cc53f30
test(ui): gate autonomous LAN runtime, PHY loss and retained real STO…
masarray Oct 25, 2026
3785bec
fix(p0): close START/link-down race and driver STOP safety state
masarray Oct 25, 2026
9f2605f
fix(p0): close START/link-down race and driver STOP safety state
masarray Oct 25, 2026
fe5cf6d
build(contract): align Studio, firmware manifest and native CI on aut…
masarray Oct 25, 2026
1c3c2f6
build(contract): align Studio, firmware manifest and native CI on aut…
masarray Oct 25, 2026
0481af6
build(contract): align Studio, firmware manifest and native CI on aut…
masarray Oct 25, 2026
495b2fb
build(contract): align Studio, firmware manifest and native CI on aut…
masarray Oct 25, 2026
adb4f3d
build(contract): align Studio, firmware manifest and native CI on aut…
masarray Oct 25, 2026
0e59ecb
test(ui): verified COM reconnect adopts already-running autonomous bo…
masarray Oct 25, 2026
2d2d59a
feat(diag): surface board-owned autonomous LAN status independent of …
masarray Oct 25, 2026
fc69307
docs(p0): specify autonomous SV/PTP ownership and real USB/LAN fault …
masarray Oct 25, 2026
e9f041b
docs(code): align GUI/serial operator guidance with board-owned LAN p…
masarray Oct 25, 2026
b9e5b49
docs(code): align GUI/serial operator guidance with board-owned LAN p…
masarray Oct 25, 2026
52c1fe7
docs(code): align GUI/serial operator guidance with board-owned LAN p…
masarray Oct 25, 2026
90287a9
docs(code): align GUI/serial operator guidance with board-owned LAN p…
masarray Oct 25, 2026
3a7f7e9
docs(code): align GUI/serial operator guidance with board-owned LAN p…
masarray Oct 25, 2026
ef77d55
fix(ptp): guard each wire TX against link-down race, not only source-…
masarray Oct 25, 2026
1c55109
refactor(p0): remove obsolete lease cause and retain USB health telem…
masarray Oct 25, 2026
61137cd
feat(p0): coherent boot-and-generation tagged 4I4V bank SHOW for Stud…
masarray Oct 25, 2026
dafd954
feat(ui): expose coherent read-only ESP32 4I4V state to reconnecting …
masarray Oct 25, 2026
a78e69f
feat(ui): validate full eight-row ESP32 bank readback, boot generatio…
masarray Oct 25, 2026
a292bd0
feat(ui): adopt complete live 4I4V board waveform in GUI on reconnect…
masarray Oct 25, 2026
3a56632
test(ui): reject partial/wrong-boot 4I4V snapshots and adopt coherent…
masarray Oct 25, 2026
38cbc8a
fix(ui): accept native Qt QVariantList sequences in board snapshot re…
masarray Oct 25, 2026
64c5db8
ci(preview): validate legacy public GUI with its old firmware, new GU…
masarray Oct 25, 2026
a61ee63
ci(preview): remove invalid legacy firmware-contract cross-test; requ…
masarray Oct 25, 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
17 changes: 9 additions & 8 deletions .github/workflows/arstack-studio-native-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ jobs:
$manifest.broadcastMirrorEnabled -ne $false) {
throw 'FAT preview requires simulated Global GM BOOT + PTP-source autostart + measured SV phase/MAC evidence, never LOCAL-only firmware'
}
$expectedCapabilities = @('SMV-4I4V', 'PROFILE', 'PROFILE-BINARY-V1', 'LIVE-SETPOINTS', 'SESSION-LEASE', 'PTP-P2', 'SMPSYNCH-AUTO')
$expectedCapabilities = @('SMV-4I4V', 'PROFILE', 'PROFILE-BINARY-V1', 'LIVE-SETPOINTS', 'SV-LAN-AUTONOMY-V1', 'PTP-P2', 'SMPSYNCH-AUTO')
foreach ($capability in $expectedCapabilities) {
if ($manifest.capabilities -notcontains $capability) { throw "PR firmware lacks capability: $capability" }
}
Expand All @@ -142,16 +142,17 @@ jobs:
$native = Join-Path $PWD 'build-studio-preview\Release\arstack_studio.exe'
$logDir = Join-Path $env:RUNNER_TEMP 'studio-firmware-diagnostics'
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
$baselineStdErr = Join-Path $logDir 'baseline.stderr.log'
$baselineStdOut = Join-Path $logDir 'baseline.stdout.log'
$baselineContract = Start-Process -FilePath $legacyGui -ArgumentList '--check-firmware-contract' -Wait -PassThru -RedirectStandardError $baselineStdErr -RedirectStandardOutput $baselineStdOut
Write-Host "PUBLIC_BASELINE_FIRMWARE_CONTRACT_EXIT=$($baselineContract.ExitCode)"
Get-Content $baselineStdErr -ErrorAction SilentlyContinue
if ($baselineContract.ExitCode -ne 0) { throw 'Pinned public v0.1.1 GUI cannot validate its own firmware' }
# The frozen v0.1.1 archive is an SHA-pinned flasher/reference
# template source, NOT a supported autonomy firmware distribution.
# Its historical firmware manifest lacks the new capability and
# cross-checking it against this GUI would be a false CI failure.
# Verify ONLY the new exact-head GUI with its own manifest below.
$nativeStdErr = Join-Path $logDir 'native.stderr.log'
$nativeStdOut = Join-Path $logDir 'native.stdout.log'
# Exact paired firmware from THIS PR head; never the old release.
$env:ARSTACK_STUDIO_FIRMWARE_DIR = $newFirmware
$firmwareContract = Start-Process -FilePath $native -ArgumentList '--check-firmware-contract' -Wait -PassThru -RedirectStandardError $nativeStdErr -RedirectStandardOutput $nativeStdOut
Write-Host "NATIVE_WITH_PUBLIC_BASELINE_FIRMWARE_CONTRACT_EXIT=$($firmwareContract.ExitCode)"
Write-Host "NATIVE_WITH_MATCHED_PREVIEW_FIRMWARE_CONTRACT_EXIT=$($firmwareContract.ExitCode)"
Get-Content $nativeStdErr -ErrorAction SilentlyContinue
if ($firmwareContract.ExitCode -ne 0) {
$manifest = Get-Content -Raw (Join-Path $baseline 'firmware\firmware-manifest.json') | ConvertFrom-Json
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/arstack-studio-qt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ jobs:
"version": "0.1.1",
"sourceCommit": "0123456789abcdef0123456789abcdef01234567",
"protocol": 1,
"capabilities": ["SMV-4I4V", "PROFILE", "PROFILE-BINARY-V1", "LIVE-SETPOINTS", "SESSION-LEASE", "PTP-P2", "SMPSYNCH-AUTO"],
"capabilities": ["SMV-4I4V", "PROFILE", "PROFILE-BINARY-V1", "LIVE-SETPOINTS", "SV-LAN-AUTONOMY-V1", "PTP-P2", "SMPSYNCH-AUTO"],
"flashOffset": 0,
"image": "arstack-esp32p4-smv-0.1.1.bin",
"sha256": "$sha"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/esp32p4-smv-injector.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ jobs:
"chipRevisionPolicy": "pre-v3",
"version": os.environ["ARSTACK_FIRMWARE_VERSION"],
"protocol": int(os.environ["ARSTACK_PROTOCOL_VERSION"]),
"capabilities": ["SMV-4I4V", "PROFILE", "PROFILE-BINARY-V1", "LIVE-SETPOINTS", "SESSION-LEASE", "PTP-P2", "SMPSYNCH-AUTO"],
"capabilities": ["SMV-4I4V", "PROFILE", "PROFILE-BINARY-V1", "LIVE-SETPOINTS", "SV-LAN-AUTONOMY-V1", "PTP-P2", "SMPSYNCH-AUTO"],
"flashOffset": 0,
"image": image,
"sha256": sha256,
Expand Down
2 changes: 1 addition & 1 deletion apps/arstack_studio/qml/BoardConnectionIndicator.qml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Item {
if (stateKey === "running") return "SV transmitter running (board reported; wire still requires independent capture)"
if (stateKey === "preparing") return "ESP32-P4 connected and verified; preparing injection profile"
if (stateKey === "connecting") return "Searching for or verifying the serial connection"
return "No verified ESP32-P4 connection; offline edit is available"
return "Studio USB/COM offline. ESP32-P4 may still broadcast SV/PTP over LAN; output state is unknown until reconnect."
}

implicitHeight: 20
Expand Down
66 changes: 64 additions & 2 deletions apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,58 @@ ApplicationWindow {
interval: 400
onTriggered: scenarioSettings.lastScenario = JSON.stringify(root.scenarioSnapshot())
}
function adoptRunningBoardScenario(snapshot) {
if (!device.deviceVerified || !device.running ||
device.scenarioApplying || reconnectScenarioRevision < 0 ||
reconnectScenarioRevision !== scenarioRevision ||
restoringScenario || !snapshot || snapshot.boot !== device.bootId ||
!snapshot.current || snapshot.current.length !== 4 ||
!snapshot.voltage || snapshot.voltage.length !== 4)
return
if (typeof snapshot.hz !== "number" || !validFrequency(snapshot.hz))
return
var groups = [snapshot.current, snapshot.voltage]
var scales = [currentScale, voltageScale]
for (var group = 0; group < 2; ++group) {
for (var i = 0; i < 4; ++i) {
var channel = groups[group][i]
if (!channel || typeof channel.counts !== "number" ||
typeof channel.angleMdeg !== "number" ||
typeof channel.quality !== "number" ||
typeof channel.enabled !== "boolean" ||
!isFinite(channel.counts) || !isFinite(channel.angleMdeg) ||
!isFinite(channel.quality))
return
}
}
restoringScenario = true
for (var g = 0; g < 2; ++g) {
var model = g === 0 ? currentModel : voltageModel
for (var row = 0; row < 4; ++row) {
var b = groups[g][row]
model.setProperty(row, "magnitude", b.counts / scales[g])
model.setProperty(row, "phase", b.angleMdeg / 1000.0)
model.setProperty(row, "quality", b.quality)
model.setProperty(row, "enabled", b.enabled)
}
}
signalFrequency = snapshot.hz
previousAcFrequency = signalFrequency > 0 ? signalFrequency : 50
frequencyField.text = signalFrequency.toFixed(3)
// Board phase angles are authoritative, not the locally persisted
// three-phase editor coupling preference.
phaseLink = false
selectSignal(activeGroup, activeRow)
refreshPreview()
restoringScenario = false
autoLiveQueuedRevision = -1
autoLiveApplyTimer.stop()
pendingApply = false
reconnectScenarioRevision = -1
// Reconnection is a read-only adoption, NEVER a CFG, START or STOP.
root.showMessage("4I+4V values synchronized from the running ESP32-P4.", false)
}

function scenarioSnapshot() {
function values(model) {
var rows = []
Expand Down Expand Up @@ -161,6 +213,10 @@ ApplicationWindow {
property int scenarioRevision: 0
property int liveSubmissionRevision: -1
property int autoLiveQueuedRevision: -1
// Captured when the newly verified board boots/connects. If the operator
// edits any control before the full board readback arrives, do not replace
// their pending draft with stale serial evidence.
property int reconnectScenarioRevision: -1
// Session-only operator intent. Never persist across application launches:
// an explicit PTP ON can be restored after a firmware reflash/reconnect
// without introducing an uncontrolled power-on publisher.
Expand Down Expand Up @@ -200,8 +256,9 @@ ApplicationWindow {

font.family: root.uiFont

// Primary-window close owns application exit. C++ teardown owns STOP, COM
// close and worker retirement; QML must not race it with another async STOP.
// Closing Studio retires the serial observer only. The ESP32 owns its
// ongoing LAN SV/PTP session; only a deliberate STOP command ends it.
// C++ still owns bounded COM/worker retirement without async QML writes.
onClosing: function(close) {
close.accepted = true
if (root.applicationShutdownRequested) return
Expand Down Expand Up @@ -264,12 +321,17 @@ ApplicationWindow {
"). Open diagnostics for the current-board TX error.", true)
}
function onDeviceVerifiedChanged() {
root.reconnectScenarioRevision = device.deviceVerified
? root.scenarioRevision : -1
root.pendingApply = true
root.liveSubmissionRevision = -1
root.autoLiveQueuedRevision = -1
autoLiveApplyTimer.stop()
root.offlinePreviewRunning = false
}
function onBoardScenarioChanged() {
root.adoptRunningBoardScenario(device.boardScenario)
}
function onRunningChanged() {
// An operator STOP or a firmware fault cancels unsent live edits.
// Reconnect/START must never replay edits from a previous session.
Expand Down
80 changes: 78 additions & 2 deletions apps/arstack_studio/src/DeterministicSessionHarness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class DeterministicSessionHarness final {
{"live SV fault has one active incident and manual STOP stays quiet", liveSvStopIsNewIncidentOnly()},
{"PTP STOP requires firmware ACK and stopped readback", ptpStopConfirmationCannotBeGuessed()},
{"PTP STOP timeout blocks restart pending manual recovery", ptpUnverifiedStopBlocksRestart()},
{"App teardown must stop PTP without SV output", ptpOnlyShutdownIntent()},
{"Studio teardown never owns autonomous SV/PTP lifetime", ptpOnlyShutdownIntent()},
{"PTP role mismatch and new session revoke pending start", ptpGuidedRoleAndGenerationGuards()},
{"profile timeout -> bounded terminal error", profileTimeoutIsTerminal()},
{"profile failure waits visible bounded cooldown before retry", profileRetryCooldownIsVisible()},
Expand All @@ -92,6 +92,8 @@ class DeterministicSessionHarness final {
{"USB port loss requires two verified misses; replug emits one arrival", usbPhysicalPortLossAndArrivalGate()},
{"READY unplug/replug COM renumber -> READY stopped", readyReplugRecoversWithoutStart()},
{"RUNNING unplug/replug -> never auto-restarts", runningReplugNeverAutoStarts()},
{"RUNNING board remains independent while Studio reconnects", runningReplugAdoptsLiveBoardState()},
{"coherent board 4I4V readback requires eight rows from same boot/gen", coherentBoardBankReadback()},
{"wrong device during recovery -> SETUP ERROR", wrongDeviceRecoveryFailsClosed()},
{"CFG transport exclusivity cannot trigger false health loss", healthProbeDefersDuringScenario()},
{"control health misses -> bounded failure", healthTimeoutIsBounded()},
Expand Down Expand Up @@ -168,7 +170,7 @@ class DeterministicSessionHarness final {
QStringLiteral("PROFILE"),
QStringLiteral("PROFILE-BINARY-V1"),
QStringLiteral("LIVE-SETPOINTS"),
QStringLiteral("SESSION-LEASE"),
QStringLiteral("SV-LAN-AUTONOMY-V1"),
QStringLiteral("PTP-P2"),
QStringLiteral("SMPSYNCH-AUTO")};
return result;
Expand Down Expand Up @@ -1854,6 +1856,80 @@ class DeterministicSessionHarness final {
!fixture.device.running();
}

static bool runningReplugAdoptsLiveBoardState() {
Fixture fixture;
if (!fixture.profileReady) return false;
const auto board = identity();
seedVerified(fixture, board, QStringLiteral("COM15"), true);
if (fixture.session.state() != QStringLiteral("RUNNING")) return false;
simulateDisconnect(fixture);

// The GUI no longer has a COM observation; it may show DISCONNECTED,
// but MUST NOT assume the hardware SV stopped or request a restart.
if (fixture.device.connected() || fixture.device.running()) return false;

// Reconnect to the same boot and obtain a fresh authoritative board
// RUNNING readback: adopt it without START and without a new STOP.
seedVerified(fixture, board, QStringLiteral("COM16"), true);
return fixture.device.connected() && fixture.device.running() &&
!fixture.device.svUnexpectedStopObserved() &&
fixture.session.state() == QStringLiteral("RUNNING") &&
fixture.device.portName() == QStringLiteral("COM16");
}

static bool coherentBoardBankReadback() {
Fixture fixture;
if (!fixture.profileReady) return false;
seedVerified(fixture, identity(), QStringLiteral("COM15"), true);
auto& d = fixture.device;
const QString boot = d.bootId();
d.processLine(QStringLiteral(
"SVBANK BEGIN generation=13 boot=%1 hz_millihz=50000").arg(boot));
const QStringList rows{
QStringLiteral("IA rms_counts=1200 phase_mdeg=30000 quality=0x00000000 enabled=1"),
QStringLiteral("IB rms_counts=500 phase_mdeg=-120000 quality=0x00000000 enabled=1"),
QStringLiteral("IC rms_counts=1000 phase_mdeg=140000 quality=0x00000000 enabled=1"),
QStringLiteral("IN rms_counts=0 phase_mdeg=0 quality=0x00000000 enabled=1"),
QStringLiteral("UA rms_counts=2000 phase_mdeg=0 quality=0x00000000 enabled=1"),
QStringLiteral("UB rms_counts=6000 phase_mdeg=-120000 quality=0x00000000 enabled=1"),
QStringLiteral("UC rms_counts=3000 phase_mdeg=120000 quality=0x00000000 enabled=1"),
QStringLiteral("UN rms_counts=0 phase_mdeg=0 quality=0x00000000 enabled=0")
};
for (int i = 0; i < 7; ++i) d.processLine(rows[i]);
d.processLine(QStringLiteral("SVBANK END generation=13 boot=%1").arg(boot));
if (!d.boardScenario().isEmpty()) return false; // incomplete snapshot
d.processLine(QStringLiteral(
"SVBANK BEGIN generation=13 boot=%1 hz_millihz=50000").arg(boot));
for (const QString& row : rows) d.processLine(row);
d.processLine(QStringLiteral(
"SVBANK END generation=14 boot=%1").arg(boot));
if (!d.boardScenario().isEmpty()) return false; // wrong generation
d.processLine(QStringLiteral(
"SVBANK BEGIN generation=13 boot=FEDCBA9876543210 hz_millihz=50000"));
for (const QString& row : rows) d.processLine(row);
d.processLine(QStringLiteral(
"SVBANK END generation=13 boot=FEDCBA9876543210"));
if (!d.boardScenario().isEmpty()) return false; // foreign boot

d.processLine(QStringLiteral(
"SVBANK BEGIN generation=13 boot=%1 hz_millihz=50000").arg(boot));
for (const QString& row : rows) d.processLine(row);
d.processLine(QStringLiteral("SVBANK END generation=13 boot=%1").arg(boot));
const auto data = d.boardScenario();
if (data.value(QStringLiteral("boot")).toString() != boot ||
data.value(QStringLiteral("generation")).toString() != QStringLiteral("13") ||
data.value(QStringLiteral("hz")).toDouble() != 50.0) return false;
const auto currents = data.value(QStringLiteral("current")).toList();
const auto voltages = data.value(QStringLiteral("voltage")).toList();
if (currents.size() != 4 || voltages.size() != 4 ||
currents[0].toMap().value(QStringLiteral("counts")).toLongLong() != 1200 ||
currents[1].toMap().value(QStringLiteral("angleMdeg")).toLongLong() != -120000 ||
voltages[2].toMap().value(QStringLiteral("counts")).toLongLong() != 3000 ||
voltages[3].toMap().value(QStringLiteral("enabled")).toBool()) return false;
d.resetTelemetry();
return d.boardScenario().isEmpty(); // invalidate on COM/boot switch
}

static bool wrongDeviceRecoveryFailsClosed() {
Fixture fixture;
if (!fixture.profileReady) return false;
Expand Down
Loading
Loading