From 3eec4fe41a1b4031eab7326d41edad2144603cf4 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 26 Oct 2026 04:08:39 +0700 Subject: [PATCH 1/5] feat(m4-p0d2): expose per-SV signal snapshot and control route --- embedded/esp32p4_smv_injector/main/multi_profile_control.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/embedded/esp32p4_smv_injector/main/multi_profile_control.hpp b/embedded/esp32p4_smv_injector/main/multi_profile_control.hpp index 9a2438afc..d8fc06e84 100644 --- a/embedded/esp32p4_smv_injector/main/multi_profile_control.hpp +++ b/embedded/esp32p4_smv_injector/main/multi_profile_control.hpp @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include "ariec61850/sampled_values/compiled_device_profile.hpp" +#include "ariec61850/sampled_values/live_signal_state.hpp" +#include // The console/control task is the ONLY writer to the committed roster. // A TX START serially follows SELECT, and COMMIT is forbidden while live SV @@ -9,6 +11,9 @@ namespace ar::esp32p4::smv { void handle_multi_profile_command(char* arguments) noexcept; void multi_profile_abort_staging() noexcept; +void handle_svstream_command(char* arguments) noexcept; +[[nodiscard]] ar::iec61850::sampled_values::SvLiveSignalState +multi_profile_signal_snapshot(std::size_t slot) noexcept; // Null means legacy V1 START. Non-null is the selected, validated, committed // 3-profile V2 roster. Never retain across STOP and a future control commit. From b3a61057aba078afae8db898545cdde384edf4e1 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 26 Oct 2026 04:08:43 +0700 Subject: [PATCH 2/5] feat(m4-p0d2): commit independent per-MU live setpoints and readback over SVSTREAM --- .../main/multi_profile_control.cpp | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/embedded/esp32p4_smv_injector/main/multi_profile_control.cpp b/embedded/esp32p4_smv_injector/main/multi_profile_control.cpp index be228aa35..48f21c7e4 100644 --- a/embedded/esp32p4_smv_injector/main/multi_profile_control.cpp +++ b/embedded/esp32p4_smv_injector/main/multi_profile_control.cpp @@ -2,6 +2,7 @@ #include "multi_profile_control.hpp" #include "multi_profile_staging.hpp" #include "live_control.hpp" +#include "ariec61850/sampled_values/m4_three_sv_signals.hpp" #include "esp_log.h" @@ -24,6 +25,8 @@ 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{}; +// Exactly three independent atomic waveform banks; only console task writes. +ar::iec61850::sampled_values::M4ThreeSvSignalBanks g_mu_signals{}; // Selection changes only in STOPPED control-plane commands; the active // publisher reads the immutable committed roster once at START. std::atomic g_multi_tx_selected{false}; @@ -45,6 +48,26 @@ void upper(char* text) noexcept { return true; } + +[[nodiscard]] bool parse_signed(const char* text, std::int32_t& output) noexcept { + if (!text || !*text) return false; + errno = 0; + char* end = nullptr; + const long value = std::strtol(text, &end, 10); + if (errno || end == text || *end || value < std::numeric_limits::min() || + value > std::numeric_limits::max()) return false; + output = static_cast(value); + return true; +} + +[[nodiscard]] int stream_channel(char* name) noexcept { + if (!name) return -1; + upper(name); + static constexpr const char* names[] = {"IA", "IB", "IC", "IN", "UA", "UB", "UC", "UN"}; + for (int i = 0; i < 8; ++i) + if (std::strcmp(name, names[i]) == 0) return i; + return -1; +} [[nodiscard]] int nibble(const char ch) noexcept { if (ch >= '0' && ch <= '9') return ch - '0'; if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; @@ -129,6 +152,81 @@ multi_profile_tx_roster_if_selected() noexcept { return g_multi.committed_profiles().data(); } + +ar::iec61850::sampled_values::SvLiveSignalState +multi_profile_signal_snapshot(const std::size_t slot) noexcept { + ar::iec61850::sampled_values::SvLiveSignalState value{}; + // Invalid slot is only reachable by programming error, never UI input. + static_cast(g_mu_signals.snapshot(slot, value)); + return value; +} + +void handle_svstream_command(char* arguments) noexcept { + char* save = nullptr; + const char* slot_text = strtok_r(arguments, kDelimiters, &save); + std::uint32_t slot_number{}; + if (!parse_uint(slot_text, slot_number) || slot_number < 1U || slot_number > 3U || + !g_multi.configured() || + !g_multi_tx_selected.load(std::memory_order_acquire)) { + ESP_LOGE(kTag, "SVSTREAM rejected: select committed MULTISV roster first; slot 1..3"); + return; + } + const std::size_t slot = slot_number - 1U; + char* verb = strtok_r(nullptr, kDelimiters, &save); + if (!verb) { ESP_LOGE(kTag, "SVSTREAM usage: <1..3> SHOW|SET|FREQ"); return; } + upper(verb); + auto state = multi_profile_signal_snapshot(slot); + if (std::strcmp(verb, "SHOW") == 0) { + if (!no_extra(&save)) { ESP_LOGE(kTag, "SVSTREAM SHOW takes no extras"); return; } + ESP_LOGI(kTag, "SVSTREAM slot=%lu generation=%llu frequency_millihz=%lu", + static_cast(slot_number), + static_cast(state.generation), + static_cast(state.frequency_millihz)); + static constexpr const char* names[] = {"IA", "IB", "IC", "IN", "UA", "UB", "UC", "UN"}; + for (std::size_t i = 0; i < state.channels.size(); ++i) { + const auto& ch = state.channels[i]; + ESP_LOGI(kTag, "SVSTREAM slot=%lu channel=%s rms=%ld phase=%ld quality=%lu enabled=%u", + static_cast(slot_number), names[i], + static_cast(ch.rms_counts), + static_cast(ch.phase_millidegrees), + static_cast(ch.quality), ch.enabled ? 1U : 0U); + } + return; + } + if (std::strcmp(verb, "FREQ") == 0) { + std::uint32_t frequency{}; + if (!parse_uint(strtok_r(nullptr, kDelimiters, &save), frequency) || + !no_extra(&save)) { ESP_LOGE(kTag, "SVSTREAM FREQ "); return; } + state.frequency_millihz = frequency; + } else if (std::strcmp(verb, "SET") == 0) { + const int index = stream_channel(strtok_r(nullptr, kDelimiters, &save)); + std::int32_t rms{}, phase{}; + std::uint32_t quality{}; + if (index < 0 || + !parse_signed(strtok_r(nullptr, kDelimiters, &save), rms) || + !parse_signed(strtok_r(nullptr, kDelimiters, &save), phase) || + !parse_uint(strtok_r(nullptr, kDelimiters, &save), quality) || + !no_extra(&save)) { + ESP_LOGE(kTag, "SVSTREAM SET "); + return; + } + auto& channel = state.channels[static_cast(index)]; + channel.rms_counts = rms; + channel.phase_millidegrees = phase; + channel.quality = quality; + } else { + ESP_LOGE(kTag, "SVSTREAM unsupported verb; use SHOW/SET/FREQ"); + return; + } + if (!g_mu_signals.publish(slot, state)) { + ESP_LOGE(kTag, "SVSTREAM rejected: invalid signal values or busy bank"); + return; + } + ESP_LOGI(kTag, "SVSTREAM committed slot=%lu generation=%llu", + static_cast(slot_number), + static_cast(g_mu_signals.generation(slot))); +} + void handle_multi_profile_command(char* arguments) noexcept { char* save = nullptr; char* command = strtok_r(arguments, kDelimiters, &save); From 2aad5775ebc7b98d6b9061858232b08c94ca8b30 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 26 Oct 2026 04:09:06 +0700 Subject: [PATCH 3/5] feat(m4-p0d2): route SVSTREAM per-MU serial commands --- embedded/esp32p4_smv_injector/main/live_control.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/embedded/esp32p4_smv_injector/main/live_control.cpp b/embedded/esp32p4_smv_injector/main/live_control.cpp index 2af19a543..336ddfc9d 100644 --- a/embedded/esp32p4_smv_injector/main/live_control.cpp +++ b/embedded/esp32p4_smv_injector/main/live_control.cpp @@ -824,6 +824,11 @@ void handle_line(char* line) noexcept { print_help(); return; } + if (std::strcmp(command, "SVSTREAM") == 0) { + g_scenario_txn.reset(); + handle_svstream_command(save); + return; + } if (std::strcmp(command, "MULTISV") == 0) { g_scenario_txn.reset(); handle_multi_profile_command(save); From 635d1b8058d3418f8e2bec2022fcc0504c16cd0b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 26 Oct 2026 04:09:10 +0700 Subject: [PATCH 4/5] feat(m4-p0d2): render all three canonical SV payloads from isolated live bank snapshots --- embedded/esp32p4_smv_injector/main/app_main.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/embedded/esp32p4_smv_injector/main/app_main.cpp b/embedded/esp32p4_smv_injector/main/app_main.cpp index 232875745..785880db7 100644 --- a/embedded/esp32p4_smv_injector/main/app_main.cpp +++ b/embedded/esp32p4_smv_injector/main/app_main.cpp @@ -1296,12 +1296,9 @@ void publisher_task(void* argument) { live_control_force_stop(); continue; } - const auto base_signal = live_signal_snapshot(); for (std::size_t slot = 0U; slot < multi_extra_signals.size(); ++slot) { - // Physical per-MU live edits arrive with P0D; until then - // separate immutable initial copies prevent shared state - // mutation and each MU has an independent phase engine. - multi_extra_signals[slot] = base_signal; + multi_extra_signals[slot] = + ar::esp32p4::smv::multi_profile_signal_snapshot(slot + 1U); multi_extra_engines[slot].reset_phase(); } multi_tick_index = 0U; @@ -1547,7 +1544,15 @@ void publisher_task(void* argument) { #endif } - const auto signal_state = live_signal_snapshot(); + const auto signal_state = multi_active + ? ar::esp32p4::smv::multi_profile_signal_snapshot(0U) + : live_signal_snapshot(); + if (multi_active) { + for (std::size_t slot = 0U; slot < multi_extra_signals.size(); ++slot) { + multi_extra_signals[slot] = + ar::esp32p4::smv::multi_profile_signal_snapshot(slot + 1U); + } + } if (pending_ticks > 1U) { #if AR_SV_PTP_PHASE_SUPPORTED // A skipped frame breaks canonical timeline evidence, even if From aa78cdaa8ffbfe7c494511713aed8f68da70b34e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 26 Oct 2026 04:09:56 +0700 Subject: [PATCH 5/5] docs(m4-p0d2): per-MU live SET/FREQ/SHOW and remaining scenario transaction scope --- docs/M4_P0D2_STREAM_SETPOINT_CONTROL.md | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/M4_P0D2_STREAM_SETPOINT_CONTROL.md diff --git a/docs/M4_P0D2_STREAM_SETPOINT_CONTROL.md b/docs/M4_P0D2_STREAM_SETPOINT_CONTROL.md new file mode 100644 index 000000000..a59bcb616 --- /dev/null +++ b/docs/M4_P0D2_STREAM_SETPOINT_CONTROL.md @@ -0,0 +1,33 @@ +# M4-P0D2 — Three independent live SV setpoint banks on ESP32-P4 + +**Status:** feature branch / firmware CI. Continues merged P0C2B triple canonical SV TX preview and P0D1 independent four-buffer banks. Existing M3.H.3 stable release remains unchanged. + +## Real implementation + +In opted-in `MULTISV SELECT` TX preview mode, MU1, MU2 and MU3 now read their own bounded `M4ThreeSvSignalBanks` slot when rendering the sample for a common 250us tick. One GPTimer and one EMAC publisher task remain. The publisher reads snapshots without mutex and performs no heap allocations. + +- `SVSTREAM <1|2|3> SHOW`: read back all eight channel values, phase, quality, frequency, and generation of the **selected** MU. +- `SVSTREAM <1|2|3> SET `: atomically publish a full state snapshot for only this MU. +- `SVSTREAM <1|2|3> FREQ `: change fundamental frequency for only this MU. Uses the existing fixed-point sine engine. +- Commands require a committed `MULTISV` roster and explicit `MULTISV SELECT`; editing a legacy single-SV board does not activate a multi stream. Invalid values and slots are rejected without changing active signal state. +- A signal bank's generation persists across STOP/START within this powered session, with per-MU phase reset as in the existing START policy. Packet identities remain unchanged by waveform edits. +- Source of truth remains the firmware; command readback is `SVSTREAM SHOW`, not the Studio draft. + +### Engineer workflow + +```text +MULTISV STATUS +MULTISV SHOW +MULTISV SELECT # only while stopped, from a committed 3-MU SCD roster +SVSTREAM 1 SET IA 100000 0 0 +SVSTREAM 2 SET IA 200000 -120000 0 +SVSTREAM 3 SET IA 300000 120000 0 +SVSTREAM 2 SHOW +# Existing verified START handshake remains required. SVSTREAM edits work live. +``` + +### Explicit incomplete work + +This P0D2 interface uses **atomic single-command full-bank publication**, but it is NOT a full multi-command scenario prepare/commit handshake. That transactional UX/readback integration is the next requirement before polished 3-page Studio. Per-stream START/STOP is also still missing; `MULTISV` opts into a group START/STOP. Multi-SV physical packet capture at the final 12,000 canonical SV frames/s and PTP phase/jitter measurements are not yet available. + +**Next:** P0D3 multi-field setpoint transaction, P0E Qt `StreamScenarioModel` + Table/Waveform/Phasor pagination, P0F paired package with on-wire test. No extra PTP timers or per-MU TX tasks.