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
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,16 @@ if(ARIEC61850_BUILD_TESTS)
add_test(NAME ariec61850_m4_three_sv_binary_tests
COMMAND ariec61850_m4_three_sv_binary_tests)

add_executable(ariec61850_m4_multi_profile_staging_tests tests/test_m4_multi_profile_staging.cpp)
target_link_libraries(ariec61850_m4_multi_profile_staging_tests PRIVATE ARIEC61850::core)
target_compile_definitions(ariec61850_m4_multi_profile_staging_tests PRIVATE
ARIEC61850_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
target_compile_features(ariec61850_m4_multi_profile_staging_tests PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_m4_multi_profile_staging_tests)
ariec61850_apply_sanitizers(ariec61850_m4_multi_profile_staging_tests)
add_test(NAME ariec61850_m4_multi_profile_staging_tests
COMMAND ariec61850_m4_multi_profile_staging_tests)

add_executable(ariec61850_simulation_tests tests/test_simulation.cpp)
target_link_libraries(ariec61850_simulation_tests PRIVATE ARIEC61850::core)
target_compile_definitions(ariec61850_simulation_tests PRIVATE
Expand Down
46 changes: 46 additions & 0 deletions docs/M4_P0C1_MULTISV_FIRMWARE_PROTOCOL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# M4-P0C1 — ESP32-P4 Multi-SV V2 Control Plane (Not Yet the TX Engine)

**Status:** implementation on feature branch / PR under test. **Do not use M3.H.3 official firmware** for these commands; the new `MULTISV` protocol is available only in a firmware build from this change.

## Design boundary

The existing `PROFILE` command, single-stream 4I+4V SV publisher, live signal bank, autonomous board behavior, PTP task, and V1 START are **unchanged**. A new **control-plane-only** `MultiSvConfigStore` receives the M4 host compiler's `ARM3` V2 envelope and commits three validated profiles to a separate fixed-capacity RAM bank. This step **does not activate triple-stream Ethernet TX**. An operator may still use the existing single SV output through the old command set.

Firmware identifies support using `MULTISV-CONFIG-STAGING-V2`. The capability `SV-MULTI3-V1` must **not** appear until M4-P0C2 actually implements and measures three on-wire canonical SV publishers. All `MULTISV STATUS/SHOW/COMMITTED` responses contain `output=INACTIVE_UNTIL_M4_P0C2`.

## Serial command reference (192-byte ASCII command limit)

```text
MULTISV STATUS
MULTISV SHOW
MULTISV BEGIN <transaction_id_decimal> <encoded_length_decimal>
MULTISV CHUNK <transaction_id_decimal> <offset_decimal> <hex_data_max_48_bytes>
MULTISV VALIDATE <transaction_id_decimal>
MULTISV COMMIT <transaction_id_decimal>
MULTISV ABORT <transaction_id_decimal>
```

Recommended Studio flow:

1. Use **existing** `SclParser` + `M4ThreeSvRosterCompiler`: compile **exactly 3** explicit 9-2LE profiles for the provided SCD; all 3 must be supported 4I+4V, 4000 Hz, 64B, `nofASDU=1`, verified counter wrap 4000.
2. `M4ThreeSvBinaryCodec::encode_into()` writes a single `ARM3 V2` binary envelope with three existing nested `ARSV V1` records. Compute `sv_binary_profile_identity_hash` of the **entire** envelope, not three separately.
3. With SV **STOPPED**, call `BEGIN` with strictly increasing transaction ID and the exact V2 binary size. Send `CHUNK` bytes sequentially, offset zero first, no overlap/gaps/replay, max **48 bytes** per command, 96 hex characters. A malformed chunk automatically aborts the transfer.
4. `VALIDATE` only succeeds after receiving the entire correctly checksummed V2 payload and decoding **all three** profiles. It also checks shared 4000 Hz and distinct complete wire identities; the SCD's **repeated APPID 0x4000 on different MACs** is valid.
5. `COMMIT` replaces the stored control-plane roster **only after VALIDATE**. The previous committed roster is unchanged after missing/duplicate chunks, CRC error, missing VALIDATE, or failed COMMIT. `STATUS` exposes committed generation/hash/length; `SHOW` additionally prints all three MAC/svID/APPID/VLAN/rate/ASDU/configRevision entries for engineer review.
6. A standalone old single-stream `START` aborts incomplete `MULTISV` upload; the committed three-stream configuration remains inert. Closing Studio does **not** erase a committed roster in powered board RAM. A reboot resets the volatile roster and its transaction sequence.

The console control task has 7168 bytes of stack in the current ESP-IDF build. The streaming buffer is statically allocated, as are candidate and committed rosters, with **no heap allocation or XML parsing** in the MCU control path. The full V2 decode uses preexisting firmware-safe V1 records and remains on the low-priority console control task, **never ISR/publisher**.

## Files

- `embedded/esp32p4_smv_injector/main/multi_profile_staging.hpp`: reusable bounded upload + all-or-nothing V2 validated commit.
- `embedded/esp32p4_smv_injector/main/multi_profile_control.cpp`: 192-byte legacy console parsing / response / STATUS / SHOW.
- `embedded/esp32p4_smv_injector/main/live_control.cpp`: command routing, capability tagging, V1 START transfer abort.
- `embedded/esp32p4_smv_injector/components/arstack_process_bus/CMakeLists.txt`: links the shared V2 codec into firmware.
- `tests/test_m4_multi_profile_staging.cpp`: GCC/Clang/MSVC host verification of real SCD-equivalent three-stream V2 upload and rollback.

## Engineering acceptance and next gate

- C++ CI must compile and run `ariec61850_m4_multi_profile_staging_tests` for GCC/Clang/MSVC; ESP32-P4 build must compile/link the new `MULTISV` command route and shared `m4_three_sv_binary.cpp`.
- No claim of three simultaneous Ethernet SV outputs, a shared measured PTP epoch, or three independent live injection setpoints is made at this checkpoint.
- **Next — P0C2:** connect an atomic committed triple roster to `SvStreamRuntime[3]` and one EMAC/PTP-anchored GPTimer. Implement the **three packet send loop** before advertising `SV-MULTI3-V1`, then implement per-slot bank/GUI and capture the 12,000 fps target.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ idf_component_register(
"${ARSTACK_ROOT}/src/sampled_values/pdu_codec.cpp"
"${ARSTACK_ROOT}/src/sampled_values/frame_codec.cpp"
"${ARSTACK_ROOT}/src/sampled_values/compiled_device_profile.cpp"
"${ARSTACK_ROOT}/src/sampled_values/m4_three_sv_binary.cpp"
"${ARSTACK_ROOT}/src/time_sync/ptp.cpp"
INCLUDE_DIRS
"${ARSTACK_ROOT}/include")
Expand Down
1 change: 1 addition & 0 deletions embedded/esp32p4_smv_injector/main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ idf_component_register(
"ethernet_port.c"
"live_control.cpp"
"profile_control.cpp"
"multi_profile_control.cpp"
"runtime_profile.cpp"
"smp_synch_lab.cpp"
${AR_PTP_LAB_SRCS}
Expand Down
15 changes: 13 additions & 2 deletions embedded/esp32p4_smv_injector/main/live_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "live_control.hpp"
#include "ethernet_port.h"
#include "profile_control.hpp"
#include "multi_profile_control.hpp"
#include "smp_synch_lab.hpp"
#include "ariec61850/sampled_values/live_signal_transaction.hpp"

Expand Down Expand Up @@ -254,6 +255,8 @@ void print_help() noexcept {
ESP_LOGI(kTag, " PTP POLICY <INTERNAL_GM_ONLY|AUTO_PREFER_EXTERNAL|EXTERNAL_ONLY|MONITOR_ONLY> (PTP+SV must be STOPPED)");
ESP_LOGI(kTag, " PTP CONFIG <domain> <transport> <vlan> <vid> <pcp> <announce_ms> <sync_ms> <pdelay> [bench_global_0_1 UTC_epoch_seconds]");
ESP_LOGI(kTag, "GUI profile deployment uses bounded PROFILE subcommands while STOPPED.");
ESP_LOGI(kTag, " MULTISV STATUS|SHOW|BEGIN <tx> <bytes>|CHUNK <tx> <offset> <hex>|VALIDATE <tx>|COMMIT <tx>|ABORT <tx>");
ESP_LOGI(kTag, " MULTISV V2 is control-plane ONLY until M4-P0C2; it does not start three SV publishers.");
}

void print_identity() noexcept {
Expand All @@ -266,10 +269,10 @@ void print_identity() noexcept {
const char* version = app != nullptr ? app->version : "unknown";
#if CONFIG_AR_PTP_LAB_TX
const char* capabilities =
"SMV-4I4V,PROFILE,LIVE-SETPOINTS,SV-LAN-AUTONOMY-V1,SV-CONFIG-TXN-V1,PTP-P2,PTP-GM-BENCH-V1,PTP-POLICY-M3D-V1,SMPSYNCH-AUTO,PROFILE-BINARY-V1,PROFILE-REUSE-V1";
"SMV-4I4V,PROFILE,LIVE-SETPOINTS,SV-LAN-AUTONOMY-V1,SV-CONFIG-TXN-V1,PTP-P2,PTP-GM-BENCH-V1,PTP-POLICY-M3D-V1,SMPSYNCH-AUTO,PROFILE-BINARY-V1,PROFILE-REUSE-V1,MULTISV-CONFIG-STAGING-V2";
#else
const char* capabilities =
"SMV-4I4V,PROFILE,LIVE-SETPOINTS,SV-LAN-AUTONOMY-V1,SV-CONFIG-TXN-V1,PROFILE-BINARY-V1,PROFILE-REUSE-V1";
"SMV-4I4V,PROFILE,LIVE-SETPOINTS,SV-LAN-AUTONOMY-V1,SV-CONFIG-TXN-V1,PROFILE-BINARY-V1,PROFILE-REUSE-V1,MULTISV-CONFIG-STAGING-V2";
#endif
ESP_LOGI(kTag,
"ARSTACK identity product=SMV-INJECTOR target=ESP32-P4 protocol=1 device_id=%02X%02X%02X%02X%02X%02X firmware=%s build=%s boot_id=%016llX capabilities=%s",
Expand Down Expand Up @@ -762,6 +765,9 @@ void handle_line(char* line) noexcept {
return;
}
profile_control_abort_staging();
// A V1 START invalidates any incomplete three-stream upload, but
// preserves a fully committed, inert M4 control-plane roster.
multi_profile_abort_staging();
// Serialize against the asynchronous Ethernet DOWN callback. A
// link-down that wins this short critical section cannot be undone
// by a late USB START on the other CPU.
Expand Down Expand Up @@ -817,6 +823,11 @@ void handle_line(char* line) noexcept {
print_help();
return;
}
if (std::strcmp(command, "MULTISV") == 0) {
g_scenario_txn.reset();
handle_multi_profile_command(save);
return;
}
if (std::strcmp(command, "PROFILE") == 0) {
g_scenario_txn.reset();
handle_profile_command(save);
Expand Down
211 changes: 211 additions & 0 deletions embedded/esp32p4_smv_injector/main/multi_profile_control.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "multi_profile_control.hpp"
#include "multi_profile_staging.hpp"
#include "live_control.hpp"

#include "esp_log.h"

#include <array>
#include <cerrno>
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <span>

namespace ar::esp32p4::smv {
namespace {
constexpr char kTag[] = "ar_smv_multi";
constexpr char kDelimiters[] = " \t\r\n";

// One UART control-task owner; real-time publisher does NOT touch this bank
// until P0C2 adds a separately synchronized activation protocol.
MultiSvConfigStore g_multi{};

void upper(char* text) noexcept {
if (text == nullptr) return;
for (; *text; ++text)
*text = static_cast<char>(std::toupper(static_cast<unsigned char>(*text)));
}

[[nodiscard]] bool parse_uint(const char* text, std::uint32_t& output) noexcept {
if (!text || !*text || *text == '-') return false;
errno = 0;
char* end = nullptr;
const auto parsed = std::strtoul(text, &end, 10);
if (errno || end == text || *end || parsed > std::numeric_limits<std::uint32_t>::max())
return false;
output = static_cast<std::uint32_t>(parsed);
return true;
}

[[nodiscard]] int nibble(const char ch) noexcept {
if (ch >= '0' && ch <= '9') return ch - '0';
if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;
if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;
return -1;
}

[[nodiscard]] bool decode_chunk(
const char* hex,
std::array<std::uint8_t, MultiSvConfigStore::max_chunk_bytes>& output,
std::size_t& count) noexcept {
count = 0U;
if (!hex) return false;
const auto length = std::strlen(hex);
if (!length || (length % 2U) || length > 2U * output.size()) return false;
for (std::size_t i = 0U; i < length; i += 2U) {
const int hi = nibble(hex[i]), lo = nibble(hex[i + 1U]);
if (hi < 0 || lo < 0) return false;
output[count++] = static_cast<std::uint8_t>((hi << 4) | lo);
}
return true;
}

[[nodiscard]] bool no_extra(char** save) noexcept {
return strtok_r(nullptr, kDelimiters, save) == nullptr;
}

void print_status() noexcept {
ESP_LOGI(kTag,
"MULTISV STATUS staging=%u validated=%u transaction=%lu received=%lu expected=%lu last=%lu configured=%u generation=%llu hash=%016llX bytes=%lu output=INACTIVE_UNTIL_M4_P0C2",
g_multi.uploading() ? 1U : 0U,
g_multi.validated() ? 1U : 0U,
static_cast<unsigned long>(g_multi.transaction()),
static_cast<unsigned long>(g_multi.received()),
static_cast<unsigned long>(g_multi.expected()),
static_cast<unsigned long>(g_multi.last_transaction()),
g_multi.configured() ? 1U : 0U,
static_cast<unsigned long long>(g_multi.generation()),
static_cast<unsigned long long>(g_multi.committed_hash()),
static_cast<unsigned long>(g_multi.committed_bytes()));
}

void print_roster() noexcept {
print_status();
if (!g_multi.configured()) return;
const auto& profiles = g_multi.committed_profiles();
for (std::size_t i = 0U; i < profiles.size(); ++i) {
const auto& p = profiles[i];
ESP_LOGI(kTag,
"MULTISV STREAM slot=%u svID=%s APPID=0x%04X MAC=%02X:%02X:%02X:%02X:%02X:%02X VLAN=%u/%u/%u rate=%lu wrap=%u noASDU=%u confRev=%lu output=INACTIVE",
static_cast<unsigned>(i + 1U),
p.sv_id.data(),
static_cast<unsigned>(p.app_id),
static_cast<unsigned>(p.destination_mac[0]),
static_cast<unsigned>(p.destination_mac[1]),
static_cast<unsigned>(p.destination_mac[2]),
static_cast<unsigned>(p.destination_mac[3]),
static_cast<unsigned>(p.destination_mac[4]),
static_cast<unsigned>(p.destination_mac[5]),
p.vlan_present ? 1U : 0U,
static_cast<unsigned>(p.vlan_id),
static_cast<unsigned>(p.vlan_priority),
static_cast<unsigned long>(p.frame_rate_hz),
static_cast<unsigned>(p.sample_counter_modulus),
static_cast<unsigned>(p.no_asdu),
static_cast<unsigned long>(p.configuration_revision));
}
}
} // namespace

void multi_profile_abort_staging() noexcept { g_multi.abort(); }

void handle_multi_profile_command(char* arguments) noexcept {
char* save = nullptr;
char* command = strtok_r(arguments, kDelimiters, &save);
if (!command) {
ESP_LOGE(kTag, "MULTISV subcommand required; use STATUS/SHOW/BEGIN/CHUNK/VALIDATE/COMMIT/ABORT");
return;
}
upper(command);

if (!std::strcmp(command, "STATUS") || !std::strcmp(command, "SHOW")) {
if (!no_extra(&save)) {
ESP_LOGE(kTag, "MULTISV SHOW/STATUS takes no arguments");
return;
}
if (!std::strcmp(command, "SHOW")) print_roster();
else print_status();
return;
}

std::uint32_t txn{};
if (!parse_uint(strtok_r(nullptr, kDelimiters, &save), txn) || txn == 0U) {
ESP_LOGE(kTag, "MULTISV rejected: missing or invalid transaction ID");
return;
}
if (!std::strcmp(command, "ABORT")) {
if (!no_extra(&save) || !g_multi.owns(txn)) {
ESP_LOGE(kTag, "MULTISV ABORT rejected: unknown transaction");
return;
}
g_multi.abort();
ESP_LOGI(kTag, "MULTISV ABORT transaction=%lu accepted", static_cast<unsigned long>(txn));
return;
}

// The old V1 publisher is never silently upgraded to a three-stream TX
// runtime. M4.0 identity/layout mutations require a STOPPED board.
if (live_tx_running()) {
g_multi.abort();
ESP_LOGE(kTag, "MULTISV rejected: STOP_SV_FIRST");
return;
}

if (!std::strcmp(command, "BEGIN")) {
std::uint32_t total{};
if (!parse_uint(strtok_r(nullptr, kDelimiters, &save), total) ||
!no_extra(&save) || !g_multi.begin(txn, total)) {
ESP_LOGE(kTag, "MULTISV BEGIN rejected: busy, replay or invalid record size");
return;
}
ESP_LOGI(kTag, "MULTISV BEGIN transaction=%lu bytes=%lu accepted",
static_cast<unsigned long>(txn), static_cast<unsigned long>(total));
return;
}
if (!std::strcmp(command, "CHUNK")) {
std::uint32_t offset{};
const char* offset_text = strtok_r(nullptr, kDelimiters, &save);
const char* chunk_text = strtok_r(nullptr, kDelimiters, &save);
std::array<std::uint8_t, MultiSvConfigStore::max_chunk_bytes> buffer{};
std::size_t count{};
if (!parse_uint(offset_text, offset) || !no_extra(&save) ||
!g_multi.owns(txn) || !decode_chunk(chunk_text, buffer, count) ||
!g_multi.append(txn, offset, std::span<const std::uint8_t>{buffer}.first(count))) {
g_multi.abort();
ESP_LOGE(kTag, "MULTISV CHUNK rejected: invalid hex, offset or transaction");
return;
}
ESP_LOGI(kTag, "MULTISV CHUNK transaction=%lu received=%lu",
static_cast<unsigned long>(txn), static_cast<unsigned long>(g_multi.received()));
return;
}
if (!std::strcmp(command, "VALIDATE")) {
if (!no_extra(&save) || !g_multi.validate(txn)) {
ESP_LOGE(kTag, "MULTISV VALIDATE rejected: incomplete, bad CRC or unsupported stream layout");
return;
}
ESP_LOGI(kTag, "MULTISV VALIDATE transaction=%lu accepted count=3 rate=4000",
static_cast<unsigned long>(txn));
return;
}
if (!std::strcmp(command, "COMMIT")) {
if (!no_extra(&save) || !g_multi.commit(txn)) {
ESP_LOGE(kTag, "MULTISV COMMIT rejected: VALIDATE required or transaction invalid");
return;
}
ESP_LOGI(kTag,
"MULTISV COMMITTED transaction=%lu generation=%llu hash=%016llX bytes=%lu output=INACTIVE_UNTIL_M4_P0C2",
static_cast<unsigned long>(txn),
static_cast<unsigned long long>(g_multi.generation()),
static_cast<unsigned long long>(g_multi.committed_hash()),
static_cast<unsigned long>(g_multi.committed_bytes()));
return;
}
ESP_LOGE(kTag, "Unknown MULTISV subcommand");
}

} // namespace ar::esp32p4::smv
6 changes: 6 additions & 0 deletions embedded/esp32p4_smv_injector/main/multi_profile_control.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
namespace ar::esp32p4::smv {
void handle_multi_profile_command(char* arguments) noexcept;
void multi_profile_abort_staging() noexcept;
}
Loading
Loading