Skip to content
Open
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
13 changes: 12 additions & 1 deletion apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -1337,7 +1337,8 @@ ApplicationWindow {
id: m4PaginationStrip
visible: sclProfiles.m4DraftSupported || m4Pages.active ||
(workflowBar.session && workflowBar.session.canInspectMultiSvBoard) ||
device.m4BoardInspecting || device.m4BoardSnapshots.length === 3
device.m4BoardInspecting || device.m4BoardSnapshots.length === 3 ||
device.m4ReconnectMode === "SELECTED"
Layout.fillWidth: true
Layout.preferredHeight: visible ? 31 : 0
spacing: 5
Expand Down Expand Up @@ -1371,6 +1372,16 @@ ApplicationWindow {
}
}
Item { Layout.fillWidth: true }
Label {
visible: device.m4ReconnectMode === "SELECTED"
text: "BOARD ROSTER / SCD · " +
(workflowBar.session ? workflowBar.session.m4SclRosterMatch : "NOT VERIFIED") +
" · START GATED"
color: workflowBar.session && workflowBar.session.m4SclRosterMatch === "MATCHED"
? studioTheme.green : studioTheme.amber
font.family: root.uiFont
font.pixelSize: 9
}
Label {
visible: m4Pages.active
text: device.m4ScenarioApplying ? "APPLYING · MU BANK READBACK REQUIRED"
Expand Down
19 changes: 19 additions & 0 deletions apps/arstack_studio/src/DeviceController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,9 @@ bool DeviceController::deployMultiSv(const QByteArray& arm3) {
return false;
}
cancelM4Scenarios(QStringLiteral("New ARM3 roster invalidates previous per-MU banks."));
// Firmware may already have committed a DIFFERENT roster at any failed
// upload stage. The old reconnect SHOW can never remain board authority.
resetM4Reconnect(QStringLiteral("UNKNOWN"));
m4Ready_ = false;
m4DeployedHash_.clear();
m4Deploying_ = true;
Expand Down Expand Up @@ -1246,12 +1249,26 @@ bool DeviceController::processM4Line(const QString& line) {
return true;
}
if (m4Upload_.verified()) {
const QStringList authoritativeRows = m4Upload_.selectedReadbackStreamLines();
if (authoritativeRows.size() != 3) {
cancelM4(QStringLiteral("M4 SELECT was acknowledged without three authoritative SHOW identities."));
return true;
}
m4Deploying_ = false;
m4Ready_ = true;
m4DeployedHash_ = m4Upload_.identityHash();
m4Status_ = QStringLiteral("3 MU identities read back; selected roster generation %1, hash %2. START awaits per-MU scenario gate.")
.arg(m4Upload_.generation()).arg(m4Upload_.identityHash());
m4InFlightCommand_.clear();
// Adopt NEW firmware-authoritative roster, never the stale
// reconnect snapshot. This fires the same cached SCD reconciliation.
m4ReconnectBoot_ = identity_.bootId;
m4ReconnectRosterHash_ = m4Upload_.identityHash();
m4ReconnectRosterGeneration_ = m4Upload_.generation();
m4ReconnectRosterLines_ = authoritativeRows;
m4ReconnectStreamMask_ = 0x07U;
m4ReconnectMode_ = QStringLiteral("SELECTED");
emit m4ReconnectModeChanged();
emit m4DeploymentChanged();
return true;
}
Expand Down Expand Up @@ -2444,6 +2461,7 @@ void DeviceController::resetM4Reconnect(const QString& mode) {
m4ReconnectAwaitingTx_ = false;
m4ReconnectRosterGeneration_ = 0;
m4ReconnectRosterHash_.clear();
m4ReconnectRosterLines_.clear();
m4ReconnectBoot_.clear();
if (m4ReconnectMode_ != mode) {
m4ReconnectMode_ = mode;
Expand Down Expand Up @@ -2508,6 +2526,7 @@ bool DeviceController::processM4ReconnectLine(const QString& line) {
resetM4Reconnect(QStringLiteral("UNKNOWN"));
} else {
m4ReconnectStreamMask_ |= bit;
m4ReconnectRosterLines_.push_back(reply);
if (m4ReconnectStreamMask_ == 7U) {
m4ReconnectTimer_.stop();
m4ReconnectMode_ = QStringLiteral("SELECTED");
Expand Down
5 changes: 5 additions & 0 deletions apps/arstack_studio/src/DeviceController.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class DeviceController : public QObject {
Q_PROPERTY(bool m4ScenariosVerified READ m4ScenariosVerified NOTIFY m4DeploymentChanged)
Q_PROPERTY(bool m4BoardInspecting READ m4BoardInspecting NOTIFY m4BoardChanged)
Q_PROPERTY(QString m4ReconnectMode READ m4ReconnectMode NOTIFY m4ReconnectModeChanged)
Q_PROPERTY(QString m4BoardRosterHash READ m4BoardRosterHash NOTIFY m4ReconnectModeChanged)
Q_PROPERTY(QVariantList m4BoardSnapshots READ m4BoardSnapshots NOTIFY m4BoardChanged)
Q_PROPERTY(QString m4BoardStatus READ m4BoardStatus NOTIFY m4BoardChanged)
Q_PROPERTY(QString m4ScenarioStatus READ m4ScenarioStatus NOTIFY m4DeploymentChanged)
Expand Down Expand Up @@ -203,6 +204,9 @@ class DeviceController : public QObject {
[[nodiscard]] QVariantList m4BoardSnapshots() const { return m4BoardSnapshots_; }
[[nodiscard]] QString m4BoardStatus() const { return m4BoardStatus_; }
[[nodiscard]] QString m4ReconnectMode() const { return m4ReconnectMode_; }
[[nodiscard]] QString m4BoardRosterHash() const { return m4ReconnectRosterHash_; }
// Exact three original firmware STREAM records, only exposed for SELECTED.
[[nodiscard]] QStringList m4BoardRosterLines() const { return m4ReconnectRosterLines_; }
[[nodiscard]] QString m4ScenarioStatus() const { return m4ScenarioStatus_; }
[[nodiscard]] QString profileDeploymentProgressSummary() const;
[[nodiscard]] bool ptpAvailable() const noexcept;
Expand Down Expand Up @@ -559,6 +563,7 @@ class DeviceController : public QObject {
bool m4ReconnectAwaitingTx_{false};
quint64 m4ReconnectRosterGeneration_{0};
QString m4ReconnectRosterHash_;
QStringList m4ReconnectRosterLines_;
QString m4BoardInFlight_;
quint64 m4BoardSession_{0};
bool m4BoardInspecting_{false};
Expand Down
48 changes: 47 additions & 1 deletion apps/arstack_studio/src/M4MultiSvUploadHarness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ void advanceThroughChunks(M4MultiSvUploadTransaction& x) {
void happyPath(const QByteArray& image) {
M4MultiSvUploadTransaction x;
check(x.start(image, kBoot), "begin validated transaction");
check(x.selectedReadbackStreamLines().isEmpty(),
"no SHOW identity proof before verified SELECT");
check(x.takeCommand() == QStringLiteral("MULTISV STATUS"), "preflight status first");
ack(x, status(false, 0, 8));
check(x.takeCommand() == QStringLiteral("MULTISV BEGIN 9 ") +
Expand Down Expand Up @@ -107,7 +109,50 @@ void happyPath(const QByteArray& image) {
ack(x, QStringLiteral("MULTISV TXMODE selected=1; next verified START uses THREE_SV"));
check(x.verified() && x.generation() == 17 && x.takeCommand().isEmpty(),
"three verified stream identities and selected firmware roster");
check(x.selectedReadbackStreamLines() == QStringList{stream(1), stream(2), stream(3)} &&
M4MultiSvUploadTransaction::matchesBoardRoster(
image, x.identityHash(), x.selectedReadbackStreamLines()),
"selected exact firmware SHOW replaces any stale reconnect SCD roster");

}
void reconnectRosterSclMatch(const QByteArray& image) {
M4MultiSvUploadTransaction tx;
check(tx.start(image, kBoot), "real SCD ARM3 must decode");
const QString hash = tx.identityHash();
QStringList rows{stream(1), stream(2), stream(3)};
const auto matches = [&image, &hash](const QStringList& wire) {
return M4MultiSvUploadTransaction::matchesBoardRoster(image, hash, wire);
};
check(matches(rows), "exact three firmware identities match original SCD");
const auto rejectChange = [&matches, &rows](const QString& original,
const QString& changed) {
auto modified = rows;
modified[1].replace(original, changed);
check(!matches(modified), "changed firmware roster field must fail closed");
};
rejectChange(QStringLiteral("svID=MU01_SV2"), QStringLiteral("svID=WRONG_MU"));
rejectChange(QStringLiteral("APPID=0x4000"), QStringLiteral("APPID=0x4001"));
rejectChange(QStringLiteral("MAC=01:0C:CD:04:00:02"),
QStringLiteral("MAC=01:0C:CD:04:00:09"));
rejectChange(QStringLiteral("VLAN=1/0/4"), QStringLiteral("VLAN=1/23/4"));
rejectChange(QStringLiteral("rate=4000"), QStringLiteral("rate=3999"));
rejectChange(QStringLiteral("wrap=4000"), QStringLiteral("wrap=8192"));
rejectChange(QStringLiteral("confRev=1"), QStringLiteral("confRev=2"));
auto duplicate = rows;
duplicate[1] = duplicate[0];
check(!matches(duplicate), "duplicate MU cannot satisfy 3/3 roster");
check(!M4MultiSvUploadTransaction::matchesBoardRoster(
image, QStringLiteral("FFFFFFFFFFFFFFFF"), rows),
"different ARM3 whole-envelope hash cannot match");
check(!M4MultiSvUploadTransaction::matchesBoardRoster(
image, hash, {rows[0], rows[1]}),
"incomplete firmware roster cannot match SCD");
auto corrupt = image;
corrupt[corrupt.size()-1] = static_cast<char>(corrupt.back() ^ 1);
check(!M4MultiSvUploadTransaction::matchesBoardRoster(corrupt, hash, rows),
"CRC-corrupt expected SCD binary cannot match board");
}

void negativeCases(const QByteArray& image) {
M4MultiSvUploadTransaction x;
auto corrupt = image;
Expand Down Expand Up @@ -311,10 +356,11 @@ int main(int argc, char** argv) {
try {
const auto image = fixtureEnvelope();
happyPath(image);
reconnectRosterSclMatch(image);
negativeCases(image);
testThreeMuScenarioApply();
testBoardSnapshots();
qInfo("PASS: complete 3-MU atomic firmware snapshot hash/CT/quality/reboot, scenario preflight/COMMIT/READBACK, and ACK-driven M4 48B chunks, BEGIN/VALIDATE/COMMIT/SHOW/SELECT, "
qInfo("PASS: exact 3-MU SCD/board roster matcher plus complete 3-MU atomic firmware snapshot hash/CT/quality/reboot, scenario preflight/COMMIT/READBACK, and ACK-driven M4 48B chunks, BEGIN/VALIDATE/COMMIT/SHOW/SELECT, "
"full hash and three-MU readback, stale transaction and boot-fault guards");
return 0;
} catch (const std::exception& e) {
Expand Down
25 changes: 25 additions & 0 deletions apps/arstack_studio/src/M4MultiSvUploadTransaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ bool M4MultiSvUploadTransaction::start(
offset_ = 0;
expectedReceived_ = 0;
rowsVerified_ = 0;
verifiedStreamLines_.clear();
statusVerified_ = false;
return true;
}
Expand All @@ -48,6 +49,7 @@ void M4MultiSvUploadTransaction::fail(const QString& reason) {
if (!active()) return;
error_ = reason;
pendingCommand_.clear();
verifiedStreamLines_.clear();
stage_ = Stage::failed;
}

Expand All @@ -60,6 +62,28 @@ QString M4MultiSvUploadTransaction::identityHash() const {
.rightJustified(16, QLatin1Char('0')).toUpper();
}

bool M4MultiSvUploadTransaction::matchesBoardRoster(
const QByteArray& expectedArm3, const QString& boardHash,
const QStringList& firmwareStreamLines) {
if (expectedArm3.isEmpty() || expectedArm3.size() >= 4096 ||
firmwareStreamLines.size() != 3 || boardHash.size() != 16) return false;
const auto source = std::span<const std::uint8_t>{
reinterpret_cast<const std::uint8_t*>(expectedArm3.constData()),
static_cast<std::size_t>(expectedArm3.size())};
const QString canonicalHash = QString::number(
static_cast<qulonglong>(sv_binary_profile_identity_hash(source)), 16)
.rightJustified(16, QLatin1Char('0')).toUpper();
if (canonicalHash != boardHash.toUpper()) return false;
M4MultiSvUploadTransaction checker;
const auto decoded = M4ThreeSvBinaryCodec::decode(source, checker.profiles_);
if (!decoded.success() || decoded.bytes != source.size()) return false;
for (const QString& line : firmwareStreamLines) {
if (!line.startsWith(QStringLiteral("MULTISV STREAM ")) ||
!checker.validateStream(line)) return false;
}
return checker.rowsVerified_ == 0x07U;
}

QString M4MultiSvUploadTransaction::value(
const QString& line, const char* key) {
const QString prefix = QString::fromLatin1(key) + QLatin1Char('=');
Expand Down Expand Up @@ -147,6 +171,7 @@ bool M4MultiSvUploadTransaction::validateStream(const QString& line) {
present != static_cast<unsigned>(profile.vlan_present) ||
vid != profile.vlan_id || pcp != profile.vlan_priority) return false;
rowsVerified_ = static_cast<std::uint8_t>(rowsVerified_ | bit);
verifiedStreamLines_.push_back(line);
return true;
}

Expand Down
12 changes: 12 additions & 0 deletions apps/arstack_studio/src/M4MultiSvUploadTransaction.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include <QByteArray>
#include <QString>
#include <QStringList>
#include <array>
#include <cstdint>

Expand All @@ -30,6 +31,16 @@ class M4MultiSvUploadTransaction final {
[[nodiscard]] bool verified() const noexcept { return stage_ == Stage::verified; }
[[nodiscard]] QString error() const { return error_; }
[[nodiscard]] QString identityHash() const;
// Authoritative exact STREAM rows from this transaction's own SHOW,
// only exposed after complete SELECT ACK and 3/3 identity verification.
[[nodiscard]] QStringList selectedReadbackStreamLines() const {
return verified() ? verifiedStreamLines_ : QStringList{};
}
// Read-only reconnect proof: same ARM3 decoder and per-stream validator as
// the ACK-gated deploy; never trust a matching hash alone.
[[nodiscard]] static bool matchesBoardRoster(
const QByteArray& expectedArm3, const QString& boardHash,
const QStringList& firmwareStreamLines);
[[nodiscard]] std::uint64_t generation() const noexcept { return generation_; }
[[nodiscard]] std::uint32_t transaction() const noexcept { return transaction_; }

Expand Down Expand Up @@ -60,5 +71,6 @@ class M4MultiSvUploadTransaction final {
qsizetype offset_{};
qsizetype expectedReceived_{};
std::uint8_t rowsVerified_{};
QStringList verifiedStreamLines_;
bool statusVerified_{};
};
33 changes: 32 additions & 1 deletion apps/arstack_studio/src/SmartSessionController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "DeviceController.hpp"
#include "FirmwareManager.hpp"
#include "SclProfileModel.hpp"
#include "M4MultiSvUploadTransaction.hpp"

#include <QVariantMap>
#include <QStringList>
Expand Down Expand Up @@ -1109,6 +1110,9 @@ QString SmartSessionController::diagnosticsText() const {
: firmwareIsCompatible() ? QStringLiteral("yes") : QStringLiteral("no"));
add(QStringLiteral("M4 roster verified"), device_->m4Ready() ? QStringLiteral("yes") : QStringLiteral("no"));
add(QStringLiteral("M4 deployed identity hash"), device_->m4DeployedHash());
add(QStringLiteral("M4 board-selected roster mode"), device_->m4ReconnectMode());
add(QStringLiteral("M4 board-selected roster hash"), device_->m4BoardRosterHash());
add(QStringLiteral("M4 board roster versus loaded SCD"), m4SclRosterMatch_);
add(QStringLiteral("M4 roster upload"), device_->m4DeploymentStatus());
add(QStringLiteral("M4 3-bank readback"), device_->m4ScenarioStatus());
add(QStringLiteral("M4 all scenarios verified"), device_->m4ScenariosVerified() ? QStringLiteral("yes") : QStringLiteral("no"));
Expand Down Expand Up @@ -1340,8 +1344,34 @@ bool SmartSessionController::deviceControlAvailable() const noexcept {
(firmware_ == nullptr || !firmware_->busy());
}

void SmartSessionController::refreshM4SclRosterMatch() {
QString key;
QString next = QStringLiteral("NOT VERIFIED");
if (device_ != nullptr && profiles_ != nullptr &&
device_->deviceVerified() &&
device_->m4ReconnectMode() == QStringLiteral("SELECTED")) {
key = device_->bootId() + QLatin1Char('|') +
device_->m4BoardRosterHash() + QLatin1Char('|') +
profiles_->m4ConfigurationHash() + QLatin1Char('|') +
device_->m4BoardRosterLines().join(QLatin1Char('\n'));
if (key == m4SclRosterCacheKey_) return;
Comment thread
masarray marked this conversation as resolved.
Resolved
next = !profiles_->m4DraftSupported() ? QStringLiteral("NO SCD")
: M4MultiSvUploadTransaction::matchesBoardRoster(
profiles_->m4ConfigurationBinary(), device_->m4BoardRosterHash(),
device_->m4BoardRosterLines())
? QStringLiteral("MATCHED") : QStringLiteral("MISMATCH");
}
if (key == m4SclRosterCacheKey_ && next == m4SclRosterMatch_) return;
m4SclRosterCacheKey_ = std::move(key);
if (next != m4SclRosterMatch_) {
m4SclRosterMatch_ = std::move(next);
emit stateChanged();
}
}

void SmartSessionController::reconcile() {
if (shuttingDown_) return;
refreshM4SclRosterMatch();
if (device_ == nullptr || profiles_ == nullptr || firmware_ == nullptr) {
setPresentation(
QStringLiteral("INITIALIZING"),
Expand Down Expand Up @@ -1489,7 +1519,8 @@ void SmartSessionController::reconcile() {
? QStringLiteral("M4 BOARD ADOPTED (READ ONLY)")
: QStringLiteral("BOARD MODE UNVERIFIED"),
device_->m4ReconnectMode() == QStringLiteral("SELECTED")
? QStringLiteral("Firmware selected 3-SV roster. Reading all three banks without modifying board or GUI drafts; START gated.")
? QStringLiteral("Firmware selected 3-SV roster; loaded SCD: %1. 3-MU bank inspection remains read-only; START gated.")
.arg(m4SclRosterMatch_)
: QStringLiteral("Waiting for authoritative MULTISV SHOW. No automatic V1 PROFILE or START."),
false, false);
return;
Expand Down
5 changes: 5 additions & 0 deletions apps/arstack_studio/src/SmartSessionController.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class SmartSessionController : public QObject {
Q_PROPERTY(bool canDeployMultiSv READ canDeployMultiSv NOTIFY stateChanged)
Q_PROPERTY(bool canApplyMultiSvScenarios READ canApplyMultiSvScenarios NOTIFY stateChanged)
Q_PROPERTY(bool canInspectMultiSvBoard READ canInspectMultiSvBoard NOTIFY stateChanged)
Q_PROPERTY(QString m4SclRosterMatch READ m4SclRosterMatch NOTIFY stateChanged)
Q_PROPERTY(bool liveControlReady READ liveControlReady NOTIFY stateChanged)
Q_PROPERTY(bool engineeringEditable READ engineeringEditable NOTIFY stateChanged)
Q_PROPERTY(bool firmwareUpdateRequired READ firmwareUpdateRequired NOTIFY stateChanged)
Expand Down Expand Up @@ -83,6 +84,7 @@ class SmartSessionController : public QObject {
[[nodiscard]] bool canDeployMultiSv() const noexcept;
[[nodiscard]] bool canApplyMultiSvScenarios() const noexcept;
[[nodiscard]] bool canInspectMultiSvBoard() const noexcept;
[[nodiscard]] QString m4SclRosterMatch() const { return m4SclRosterMatch_; }
[[nodiscard]] bool liveControlReady() const noexcept;
[[nodiscard]] bool engineeringEditable() const noexcept;
[[nodiscard]] bool firmwareUpdateRequired() const noexcept;
Expand Down Expand Up @@ -378,6 +380,7 @@ class SmartSessionController : public QObject {
void reconnectDeviceSignals();
void reconnectProfileSignals();
void reconnectFirmwareSignals();
void refreshM4SclRosterMatch();
void refreshRecoveryOfferFromIdentity();
void clearBlankBoardContext();
void latchFirmwareFailure(QString message);
Expand Down Expand Up @@ -427,6 +430,8 @@ class SmartSessionController : public QObject {
bool wasReady_{false};
bool needsProfileSync_{true};
bool m4Mode_{false}; // suppress V1 automatic profile sync after M4 operator intent
QString m4SclRosterMatch_{QStringLiteral("NOT VERIFIED")};
QString m4SclRosterCacheKey_;
bool updateRequested_{false};
bool updateWasOptional_{false};
bool blankBoardDetected_{false};
Expand Down
Loading
Loading