Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 26 additions & 1 deletion .github/workflows/arstack-studio-qt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,31 @@ jobs:
fi
echo 'S7 QML authority boundary: PASS · presentation submits intents through SmartSessionController and device mutators are not QML-invokable'

- name: Native binary profile authority regression
shell: bash
run: |
set -euo pipefail
controller=apps/arstack_studio/src/DeviceController.cpp
profile_model=apps/arstack_studio/src/SclProfileModel.cpp
firmware=embedded/esp32p4_smv_injector/main/profile_control.cpp
grep -q 'deviceProfileBinary' "$profile_model"
grep -q 'compile_esp32p4_device_profile' "$profile_model"
grep -q 'PROFILE BINSTATUS' "$controller"
grep -q 'PROFILE BINBEGIN' "$controller"
grep -q 'PROFILE BINCHUNK' "$controller"
grep -q 'PROFILE BINCOMMIT' "$controller"
grep -q 'PROFILE SHOW' "$controller"
grep -q 'PROFILE BINSTATUS active=' "$firmware"
if grep -q 'QStringLiteral("PROFILE BEGIN")' "$controller"; then
echo 'Native Studio regressed to legacy textual profile deployment.'
exit 1
fi
if grep -q 'apps/smv_injector_gui' apps/arstack_studio/CMakeLists.txt; then
echo 'Native Studio must not depend on the deprecated browser control surface.'
exit 1
fi
echo 'Native binary profile authority: PASS · C++ compiler -> DeviceController -> firmware V1'

- name: Configure
shell: bash
run: |
Expand Down Expand Up @@ -220,7 +245,7 @@ jobs:
"version": "0.1.1",
"sourceCommit": "0123456789abcdef0123456789abcdef01234567",
"protocol": 1,
"capabilities": ["SMV-4I4V", "PROFILE", "LIVE-SETPOINTS", "SESSION-LEASE", "PTP-P2", "SMPSYNCH-AUTO"],
"capabilities": ["SMV-4I4V", "PROFILE", "PROFILE-BINARY-V1", "LIVE-SETPOINTS", "SESSION-LEASE", "PTP-P2", "SMPSYNCH-AUTO"],
"flashOffset": 0,
"image": "arstack-esp32p4-smv-0.1.1.bin",
"sha256": "$sha"
Expand Down
12 changes: 9 additions & 3 deletions apps/arstack_studio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

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.

> **v0.1.0 is publicly released and stable.** Download: https://github.com/masarray/arstack61850/releases/tag/v0.1.0
> **v0.1.1 is the latest public ARStack Studio release.** Download: https://github.com/masarray/arstack61850/releases/tag/v0.1.1
>
> Production source target: `9c7fc7300220db4643e5643081240b955cfe12df` · accepted binary build head: `d9b5b6848415c7e6d1c52ec929e57c66b608058d`.
> 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.

## P0 public boundary

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

## Windows release artifacts

The stable `v0.1.0` public release is available at:
The latest public native Studio release is `v0.1.1`; the normal operator path is the Windows installer:

- [GitHub Release — ARStack Studio v0.1.1](https://github.com/masarray/arstack61850/releases/tag/v0.1.1)
- `ARStack-Studio-0.1.1-win-x64-setup.exe` — recommended installation
- `ARStack-Studio-0.1.1-win-x64-portable.zip` — portable package

The original bounded `v0.1.0` milestone remains documented below for historical acceptance evidence:

- [GitHub Release — ARStack Studio / SMV Injector v0.1.0](https://github.com/masarray/arstack61850/releases/tag/v0.1.0)
- `ARStack-Studio-0.1.0-win-x64-setup.exe` — recommended normal installation
Expand Down
84 changes: 84 additions & 0 deletions apps/arstack_studio/src/DeterministicSessionHarness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class DeterministicSessionHarness final {
{"firmware probe before serial release -> blocked", firmwareProbeRequiresReleasedPort()},
{"profile timeout -> bounded terminal error", profileTimeoutIsTerminal()},
{"profile rejection -> bounded terminal error", profileRejectionIsTerminal()},
{"binary stale ACK identity -> fail closed", binaryProfileStaleAckFailsClosed()},
{"binary progress -> per-step deadline rearmed", binaryProfileProgressRearmsDeadline()},
{"READY unplug/replug COM renumber -> READY stopped", readyReplugRecoversWithoutStart()},
{"RUNNING unplug/replug -> never auto-restarts", runningReplugNeverAutoStarts()},
{"wrong device during recovery -> SETUP ERROR", wrongDeviceRecoveryFailsClosed()},
Expand Down Expand Up @@ -116,6 +118,7 @@ class DeterministicSessionHarness final {
result.capabilities = {
QStringLiteral("SMV-4I4V"),
QStringLiteral("PROFILE"),
QStringLiteral("PROFILE-BINARY-V1"),
QStringLiteral("LIVE-SETPOINTS"),
QStringLiteral("SESSION-LEASE"),
QStringLiteral("PTP-P2"),
Expand Down Expand Up @@ -547,6 +550,87 @@ class DeterministicSessionHarness final {
!session.startReady();
}

static bool binaryProfileStaleAckFailsClosed() {
// Isolate DeviceController's parser boundary from the supervisor retry
// policy. Production may immediately schedule a bounded retry after a
// deployment failure; that must not be mistaken for stale-ACK acceptance.
Fixture abortFixture;
if (!abortFixture.profileReady) return false;
seedVerified(abortFixture, identity(), QStringLiteral("COM7"), false);
abortFixture.session.profileSyncStage_ =
SmartSessionController::ProfileSyncStage::failed;
auto& abortDevice = abortFixture.device;
const QString abortGeneration = abortDevice.profileGeneration_;

abortDevice.profileDeploying_ = true;
abortDevice.profileArmed_ = false;
abortDevice.binaryProfileStage_ =
DeviceController::BinaryProfileStage::waiting_abort;
abortDevice.binaryProfile_.transaction = 42U;
abortDevice.binaryProfile_.lastObservedTransaction = 42U;
abortDevice.lastError_.clear();

const bool abortConsumed = abortDevice.processBinaryProfileLine(
QStringLiteral("PROFILE BINABORT transaction=41 accepted"));
const bool abortClosed =
abortConsumed && !abortDevice.profileArmed_ &&
abortDevice.profileGeneration_ == abortGeneration &&
abortDevice.binaryProfileStage_ ==
DeviceController::BinaryProfileStage::idle &&
abortDevice.lastError_.contains(
QStringLiteral("BINABORT"), Qt::CaseInsensitive);

Fixture commitFixture;
if (!commitFixture.profileReady) return false;
seedVerified(commitFixture, identity(), QStringLiteral("COM7"), false);
commitFixture.session.profileSyncStage_ =
SmartSessionController::ProfileSyncStage::failed;
auto& commitDevice = commitFixture.device;
const QString commitGeneration = commitDevice.profileGeneration_;

commitDevice.profileDeploying_ = true;
commitDevice.profileArmed_ = false;
commitDevice.binaryProfileStage_ =
DeviceController::BinaryProfileStage::waiting_commit;
commitDevice.binaryProfile_.transaction = 77U;
commitDevice.binaryProfile_.svId = QStringLiteral("ARSTACK_SV01");
commitDevice.binaryProfile_.appId = 0x4000U;
commitDevice.binaryProfile_.rate = 4000U;
commitDevice.binaryProfile_.modulus = 4000U;
commitDevice.lastError_.clear();

const bool commitConsumed = commitDevice.processBinaryProfileLine(
QStringLiteral(
"PROFILE BINCOMMIT transaction=76 committed generation=8 "
"svID=ARSTACK_SV01 APPID=0x4000 rate=4000 wrap=4000"));
const bool commitClosed =
commitConsumed && !commitDevice.profileArmed_ &&
commitDevice.profileGeneration_ == commitGeneration &&
commitDevice.binaryProfileStage_ ==
DeviceController::BinaryProfileStage::idle &&
commitDevice.lastError_.contains(
QStringLiteral("BINCOMMIT"), Qt::CaseInsensitive);

return abortClosed && commitClosed;
}

static bool binaryProfileProgressRearmsDeadline() {
Fixture fixture;
if (!fixture.profileReady) return false;
seedVerified(fixture, identity(), QStringLiteral("COM7"), false);
auto& session = fixture.session;
auto& device = fixture.device;

session.profileSyncStage_ = SmartSessionController::ProfileSyncStage::deploying;
session.profileSyncTimer_.stop();
emit device.profileDeploymentProgress();
const bool armed = session.profileSyncTimer_.isActive() &&
session.profileSyncTimer_.interval() == SmartSessionController::profileSyncTimeoutMs();
session.profileSyncTimer_.stop();
session.profileSyncStage_ = SmartSessionController::ProfileSyncStage::idle;
return armed;
}

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