Skip to content

Commit 9e70a49

Browse files
authored
Merge pull request #151 from masarray/hardening/smv-p3a1-studio-binary-profile
P3-A1: native ARStack Studio binary profile deployment and installer candidate
2 parents 6255d88 + 12592a9 commit 9e70a49

12 files changed

Lines changed: 497 additions & 68 deletions

File tree

‎.github/workflows/arstack-studio-qt.yml‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,31 @@ jobs:
165165
fi
166166
echo 'S7 QML authority boundary: PASS · presentation submits intents through SmartSessionController and device mutators are not QML-invokable'
167167
168+
- name: Native binary profile authority regression
169+
shell: bash
170+
run: |
171+
set -euo pipefail
172+
controller=apps/arstack_studio/src/DeviceController.cpp
173+
profile_model=apps/arstack_studio/src/SclProfileModel.cpp
174+
firmware=embedded/esp32p4_smv_injector/main/profile_control.cpp
175+
grep -q 'deviceProfileBinary' "$profile_model"
176+
grep -q 'compile_esp32p4_device_profile' "$profile_model"
177+
grep -q 'PROFILE BINSTATUS' "$controller"
178+
grep -q 'PROFILE BINBEGIN' "$controller"
179+
grep -q 'PROFILE BINCHUNK' "$controller"
180+
grep -q 'PROFILE BINCOMMIT' "$controller"
181+
grep -q 'PROFILE SHOW' "$controller"
182+
grep -q 'PROFILE BINSTATUS active=' "$firmware"
183+
if grep -q 'QStringLiteral("PROFILE BEGIN")' "$controller"; then
184+
echo 'Native Studio regressed to legacy textual profile deployment.'
185+
exit 1
186+
fi
187+
if grep -q 'apps/smv_injector_gui' apps/arstack_studio/CMakeLists.txt; then
188+
echo 'Native Studio must not depend on the deprecated browser control surface.'
189+
exit 1
190+
fi
191+
echo 'Native binary profile authority: PASS · C++ compiler -> DeviceController -> firmware V1'
192+
168193
- name: Configure
169194
shell: bash
170195
run: |
@@ -220,7 +245,7 @@ jobs:
220245
"version": "0.1.1",
221246
"sourceCommit": "0123456789abcdef0123456789abcdef01234567",
222247
"protocol": 1,
223-
"capabilities": ["SMV-4I4V", "PROFILE", "LIVE-SETPOINTS", "SESSION-LEASE", "PTP-P2", "SMPSYNCH-AUTO"],
248+
"capabilities": ["SMV-4I4V", "PROFILE", "PROFILE-BINARY-V1", "LIVE-SETPOINTS", "SESSION-LEASE", "PTP-P2", "SMPSYNCH-AUTO"],
224249
"flashOffset": 0,
225250
"image": "arstack-esp32p4-smv-0.1.1.bin",
226251
"sha256": "$sha"

‎apps/arstack_studio/README.md‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
ARStack Studio is the **canonical native desktop operator surface** for the first ARStack61850 public Sampled Values release. It is a Qt 6 / C++ / QML application; the ESP32-P4 remains the deterministic real-time publisher.
44

5-
> **v0.1.0 is publicly released and stable.** Download: https://github.com/masarray/arstack61850/releases/tag/v0.1.0
5+
> **v0.1.1 is the latest public ARStack Studio release.** Download: https://github.com/masarray/arstack61850/releases/tag/v0.1.1
66
>
7-
> Production source target: `9c7fc7300220db4643e5643081240b955cfe12df` · accepted binary build head: `d9b5b6848415c7e6d1c52ec929e57c66b608058d`.
7+
> Public v0.1.1 tag source: `2bcf28dd948161bb70fdc6d5b338e9a600670bc1`. The earlier `9c7fc730...` / `d9b5b684...` pair remains the historical v0.1.0 P0 acceptance anchor, not the current development baseline.
88
99
## P0 public boundary
1010

@@ -92,7 +92,13 @@ Protocol `1` is the P0 GUI/firmware capability contract for the supported `SMV-4
9292

9393
## Windows release artifacts
9494

95-
The stable `v0.1.0` public release is available at:
95+
The latest public native Studio release is `v0.1.1`; the normal operator path is the Windows installer:
96+
97+
- [GitHub Release — ARStack Studio v0.1.1](https://github.com/masarray/arstack61850/releases/tag/v0.1.1)
98+
- `ARStack-Studio-0.1.1-win-x64-setup.exe` — recommended installation
99+
- `ARStack-Studio-0.1.1-win-x64-portable.zip` — portable package
100+
101+
The original bounded `v0.1.0` milestone remains documented below for historical acceptance evidence:
96102

97103
- [GitHub Release — ARStack Studio / SMV Injector v0.1.0](https://github.com/masarray/arstack61850/releases/tag/v0.1.0)
98104
- `ARStack-Studio-0.1.0-win-x64-setup.exe` — recommended normal installation

‎apps/arstack_studio/src/DeterministicSessionHarness.cpp‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class DeterministicSessionHarness final {
4141
{"firmware probe before serial release -> blocked", firmwareProbeRequiresReleasedPort()},
4242
{"profile timeout -> bounded terminal error", profileTimeoutIsTerminal()},
4343
{"profile rejection -> bounded terminal error", profileRejectionIsTerminal()},
44+
{"binary stale ACK identity -> fail closed", binaryProfileStaleAckFailsClosed()},
45+
{"binary progress -> per-step deadline rearmed", binaryProfileProgressRearmsDeadline()},
4446
{"READY unplug/replug COM renumber -> READY stopped", readyReplugRecoversWithoutStart()},
4547
{"RUNNING unplug/replug -> never auto-restarts", runningReplugNeverAutoStarts()},
4648
{"wrong device during recovery -> SETUP ERROR", wrongDeviceRecoveryFailsClosed()},
@@ -116,6 +118,7 @@ class DeterministicSessionHarness final {
116118
result.capabilities = {
117119
QStringLiteral("SMV-4I4V"),
118120
QStringLiteral("PROFILE"),
121+
QStringLiteral("PROFILE-BINARY-V1"),
119122
QStringLiteral("LIVE-SETPOINTS"),
120123
QStringLiteral("SESSION-LEASE"),
121124
QStringLiteral("PTP-P2"),
@@ -547,6 +550,87 @@ class DeterministicSessionHarness final {
547550
!session.startReady();
548551
}
549552

553+
static bool binaryProfileStaleAckFailsClosed() {
554+
// Isolate DeviceController's parser boundary from the supervisor retry
555+
// policy. Production may immediately schedule a bounded retry after a
556+
// deployment failure; that must not be mistaken for stale-ACK acceptance.
557+
Fixture abortFixture;
558+
if (!abortFixture.profileReady) return false;
559+
seedVerified(abortFixture, identity(), QStringLiteral("COM7"), false);
560+
abortFixture.session.profileSyncStage_ =
561+
SmartSessionController::ProfileSyncStage::failed;
562+
auto& abortDevice = abortFixture.device;
563+
const QString abortGeneration = abortDevice.profileGeneration_;
564+
565+
abortDevice.profileDeploying_ = true;
566+
abortDevice.profileArmed_ = false;
567+
abortDevice.binaryProfileStage_ =
568+
DeviceController::BinaryProfileStage::waiting_abort;
569+
abortDevice.binaryProfile_.transaction = 42U;
570+
abortDevice.binaryProfile_.lastObservedTransaction = 42U;
571+
abortDevice.lastError_.clear();
572+
573+
const bool abortConsumed = abortDevice.processBinaryProfileLine(
574+
QStringLiteral("PROFILE BINABORT transaction=41 accepted"));
575+
const bool abortClosed =
576+
abortConsumed && !abortDevice.profileArmed_ &&
577+
abortDevice.profileGeneration_ == abortGeneration &&
578+
abortDevice.binaryProfileStage_ ==
579+
DeviceController::BinaryProfileStage::idle &&
580+
abortDevice.lastError_.contains(
581+
QStringLiteral("BINABORT"), Qt::CaseInsensitive);
582+
583+
Fixture commitFixture;
584+
if (!commitFixture.profileReady) return false;
585+
seedVerified(commitFixture, identity(), QStringLiteral("COM7"), false);
586+
commitFixture.session.profileSyncStage_ =
587+
SmartSessionController::ProfileSyncStage::failed;
588+
auto& commitDevice = commitFixture.device;
589+
const QString commitGeneration = commitDevice.profileGeneration_;
590+
591+
commitDevice.profileDeploying_ = true;
592+
commitDevice.profileArmed_ = false;
593+
commitDevice.binaryProfileStage_ =
594+
DeviceController::BinaryProfileStage::waiting_commit;
595+
commitDevice.binaryProfile_.transaction = 77U;
596+
commitDevice.binaryProfile_.svId = QStringLiteral("ARSTACK_SV01");
597+
commitDevice.binaryProfile_.appId = 0x4000U;
598+
commitDevice.binaryProfile_.rate = 4000U;
599+
commitDevice.binaryProfile_.modulus = 4000U;
600+
commitDevice.lastError_.clear();
601+
602+
const bool commitConsumed = commitDevice.processBinaryProfileLine(
603+
QStringLiteral(
604+
"PROFILE BINCOMMIT transaction=76 committed generation=8 "
605+
"svID=ARSTACK_SV01 APPID=0x4000 rate=4000 wrap=4000"));
606+
const bool commitClosed =
607+
commitConsumed && !commitDevice.profileArmed_ &&
608+
commitDevice.profileGeneration_ == commitGeneration &&
609+
commitDevice.binaryProfileStage_ ==
610+
DeviceController::BinaryProfileStage::idle &&
611+
commitDevice.lastError_.contains(
612+
QStringLiteral("BINCOMMIT"), Qt::CaseInsensitive);
613+
614+
return abortClosed && commitClosed;
615+
}
616+
617+
static bool binaryProfileProgressRearmsDeadline() {
618+
Fixture fixture;
619+
if (!fixture.profileReady) return false;
620+
seedVerified(fixture, identity(), QStringLiteral("COM7"), false);
621+
auto& session = fixture.session;
622+
auto& device = fixture.device;
623+
624+
session.profileSyncStage_ = SmartSessionController::ProfileSyncStage::deploying;
625+
session.profileSyncTimer_.stop();
626+
emit device.profileDeploymentProgress();
627+
const bool armed = session.profileSyncTimer_.isActive() &&
628+
session.profileSyncTimer_.interval() == SmartSessionController::profileSyncTimeoutMs();
629+
session.profileSyncTimer_.stop();
630+
session.profileSyncStage_ = SmartSessionController::ProfileSyncStage::idle;
631+
return armed;
632+
}
633+
550634
static bool readyReplugRecoversWithoutStart() {
551635
Fixture fixture;
552636
if (!fixture.profileReady) return false;

0 commit comments

Comments
 (0)