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
33 changes: 33 additions & 0 deletions docs/M4_P0D2_STREAM_SETPOINT_CONTROL.md
Original file line number Diff line number Diff line change
@@ -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 <IA|IB|IC|IN|UA|UB|UC|UN> <rms_wire_counts> <phase_mdeg> <quality_u32>`: atomically publish a full state snapshot for only this MU.
- `SVSTREAM <1|2|3> FREQ <millihertz>`: 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.
17 changes: 11 additions & 6 deletions embedded/esp32p4_smv_injector/main/app_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions embedded/esp32p4_smv_injector/main/live_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
98 changes: 98 additions & 0 deletions embedded/esp32p4_smv_injector/main/multi_profile_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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<bool> g_multi_tx_selected{false};
Expand All @@ -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<std::int32_t>::min() ||
value > std::numeric_limits<std::int32_t>::max()) return false;
output = static_cast<std::int32_t>(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;
Expand Down Expand Up @@ -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<void>(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<unsigned long>(slot_number),
static_cast<unsigned long long>(state.generation),
static_cast<unsigned long>(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<unsigned long>(slot_number), names[i],
static_cast<long>(ch.rms_counts),
static_cast<long>(ch.phase_millidegrees),
static_cast<unsigned long>(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 <millihertz>"); 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 <IA|IB|IC|IN|UA|UB|UC|UN> <rms> <phase_mdeg> <quality>");
return;
}
auto& channel = state.channels[static_cast<std::size_t>(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<unsigned long>(slot_number),
static_cast<unsigned long long>(g_mu_signals.generation(slot)));
}

void handle_multi_profile_command(char* arguments) noexcept {
char* save = nullptr;
char* command = strtok_r(arguments, kDelimiters, &save);
Expand Down
5 changes: 5 additions & 0 deletions embedded/esp32p4_smv_injector/main/multi_profile_control.hpp
Original file line number Diff line number Diff line change
@@ -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 <cstddef>

// 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
Expand All @@ -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.
Expand Down
Loading