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
3 changes: 2 additions & 1 deletion .github/workflows/esp32p4-smv-injector.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ jobs:
"chipRevisionPolicy": "pre-v3",
"version": os.environ["ARSTACK_FIRMWARE_VERSION"],
"protocol": int(os.environ["ARSTACK_PROTOCOL_VERSION"]),
"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": image,
"sha256": sha256,
Expand All @@ -166,6 +166,7 @@ jobs:
assert manifest['protocol'] == 1
assert 'PTP-P2' in manifest['capabilities']
assert 'SMPSYNCH-AUTO' in manifest['capabilities']
assert 'PROFILE-BINARY-V1' in manifest['capabilities']
assert actual == manifest['sha256']
print(f"Firmware package: PASS · {manifest['image']} · PTP-P2 · pre-v3 · SHA-256 {actual}")
PY
Expand Down
38 changes: 37 additions & 1 deletion embedded/esp32p4_smv_injector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,40 @@ Ethernet RX

In a switched lab, the analyzer still requires the relevant frames to reach its physical port. Use a TAP or managed-switch mirror/SPAN configuration that preserves the traffic and VLAN tags.

Use an isolated bench network. Do not connect this development image to an operational process bus.
Use an isolated bench network. Do not connect this development image to an operational process bus.

## P3-A1 binary profile staging (engineering console, V1)

The optional capability is explicitly advertised as `PROFILE-BINARY-V1` in
IDENTIFY and the firmware manifest; older firmware without the token must
continue using the legacy textual PROFILE route.

The shared host binary envelope is accepted from SCL engineering intent only after
the existing Class-A compiler has resolved a supported device profile. Firmware
uses that same `SvDeviceProfileBinaryCodec` (magic/version/length/CRC/leaf
descriptors); it does not parse SCL or maintain a second active profile model.

For a locally compiled binary V1 envelope, send the following STOPPED-only
commands to the firmware console (decimal transaction and byte offsets; chunk
hex without separators; max 48 raw bytes per command):

```text
PROFILE BINBEGIN 1 <total-binary-bytes>
PROFILE BINCHUNK 1 0 <up-to-48-bytes-as-hex>
PROFILE BINCHUNK 1 48 <next-bytes-as-hex>
PROFILE BINCOMMIT 1
```

A complete transaction is decoded and mapped into the existing
`RuntimePublisherProfile` only if the current device supports it (one ASDU,
SmpPerSec, ordered 4I+4V INT32+Quality leaves, 64-byte payload, supported
SmvOpts). The existing atomic `runtime_profile_commit()` owns generation,
while START owns packet-template preparation and the realtime timing lifecycle.

Missing, duplicate or out-of-order chunks, replayed transaction numbers, wrong
CRC/schema, unsupported wire semantics, and RUNNING state are rejected without
changing the active stream. `PROFILE BINABORT <transaction>` cancels staging,
and START discards all unfinished profile staging. The old textual PROFILE path
remains available until Studio binary transport/ACK equivalence is independently
verified. This is a software integration gate, not physical interoperability,
PTP-lock or wire-jitter proof.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ idf_component_register(
"${ARSTACK_ROOT}/src/mms/utc_time.cpp"
"${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/time_sync/ptp.cpp"
INCLUDE_DIRS
"${ARSTACK_ROOT}/include")
Expand Down
101 changes: 101 additions & 0 deletions embedded/esp32p4_smv_injector/main/binary_profile_staging.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include "ariec61850/sampled_values/compiled_device_profile.hpp"

#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <span>

namespace ar::esp32p4::smv {

// Transfer-only storage, never an alternative active SV profile authority.
// Fixed bounds fit the legacy 192-byte ASCII console using <=48-byte chunks.
class BinaryProfileStaging final {
public:
static constexpr std::size_t max_record_bytes =
ar::iec61850::sampled_values::SvDeviceProfileBinaryCodec::header_bytes +
ar::iec61850::sampled_values::SvDeviceProfileBinaryCodec::fixed_payload_bytes +
ar::iec61850::sampled_values::SvDeviceProfileBinaryCodec::leaf_descriptor_bytes *
ar::iec61850::sampled_values::compiled_sv_device_profile_max_leaves +
ar::iec61850::sampled_values::compiled_sv_device_profile_max_sv_id_bytes +
ar::iec61850::sampled_values::compiled_sv_device_profile_max_dataset_bytes;
static constexpr std::size_t max_chunk_bytes = 48U;
static constexpr std::size_t min_record_bytes =
ar::iec61850::sampled_values::SvDeviceProfileBinaryCodec::header_bytes +
ar::iec61850::sampled_values::SvDeviceProfileBinaryCodec::fixed_payload_bytes +
ar::iec61850::sampled_values::SvDeviceProfileBinaryCodec::leaf_descriptor_bytes;

// BINCHUNK worst-case ASCII length, including decimal token/offset.
static_assert(17U + 10U + 1U + 10U + 1U + 2U * max_chunk_bytes < 192U);

[[nodiscard]] bool begin(const std::uint32_t transaction,
const std::size_t total_bytes) noexcept {
if (active_ || transaction == 0U || transaction <= last_transaction_ ||
total_bytes < min_record_bytes || total_bytes > max_record_bytes) {
return false;
}
data_.fill(std::uint8_t{0U});
expected_ = total_bytes;
received_ = 0U;
transaction_ = transaction;
last_transaction_ = transaction;
active_ = true;
return true;
}

[[nodiscard]] bool owns(const std::uint32_t transaction) const noexcept {
return active_ && transaction != 0U && transaction == transaction_;
}

[[nodiscard]] bool append(const std::uint32_t transaction,
const std::size_t offset,
const std::span<const std::uint8_t> bytes) noexcept {
if (!owns(transaction)) return false;
if (offset != received_ || bytes.empty() ||
bytes.size() > max_chunk_bytes ||
bytes.size() > expected_ - received_) {
abort();
return false;
}
std::copy(bytes.begin(), bytes.end(), data_.begin() + received_);
received_ += bytes.size();
return true;
}

[[nodiscard]] bool complete(const std::uint32_t transaction) const noexcept {
return owns(transaction) && received_ == expected_;
}

[[nodiscard]] std::span<const std::uint8_t> complete_record(
const std::uint32_t transaction) const noexcept {
return complete(transaction)
? std::span<const std::uint8_t>{data_}.first(expected_)
: std::span<const std::uint8_t>{};
}

[[nodiscard]] std::size_t received() const noexcept { return received_; }
[[nodiscard]] std::size_t expected() const noexcept { return expected_; }
[[nodiscard]] bool active() const noexcept { return active_; }

void abort() noexcept {
data_.fill(std::uint8_t{0U});
expected_ = 0U;
received_ = 0U;
transaction_ = 0U;
active_ = false;
// Keep last_transaction_ to reject replay inside this console session.
}

private:
std::array<std::uint8_t, max_record_bytes> data_{};
std::size_t expected_{};
std::size_t received_{};
std::uint32_t transaction_{};
std::uint32_t last_transaction_{};
bool active_{};
};

} // namespace ar::esp32p4::smv
81 changes: 81 additions & 0 deletions embedded/esp32p4_smv_injector/main/compiled_profile_adapter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include "runtime_profile.hpp"
#include "ariec61850/sampled_values/compiled_device_profile.hpp"

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <span>

namespace ar::esp32p4::smv {

enum class BinaryRuntimeProfileStatus : std::uint8_t {
ok,
invalid_envelope,
unsupported_device_capability,
};

// Device capability is narrower than host SCL validity. Do not reinterpret
// unsupported wire fields or invent an alternative active profile model.
[[nodiscard]] inline BinaryRuntimeProfileStatus decode_binary_runtime_profile(
const std::span<const std::uint8_t> bytes,
RuntimePublisherProfile& output) noexcept {
using namespace ar::iec61850::sampled_values;
CompiledSvDeviceProfile decoded{};
if (!SvDeviceProfileBinaryCodec::decode(bytes, decoded).success()) {
return BinaryRuntimeProfileStatus::invalid_envelope;
}
if ((decoded.profile_family != SvProfileFamily::iec61850_9_2 &&
decoded.profile_family != SvProfileFamily::legacy_9_2le) ||
decoded.sampling_basis != SvSampleMode::samples_per_second ||
decoded.no_asdu != 1U ||
decoded.frame_rate_hz != decoded.configured_sample_rate ||
decoded.frame_rate_hz == 0U || decoded.frame_rate_hz > 65535U ||
decoded.leaf_count != 16U || decoded.payload_size_bytes != 64U ||
!decoded.asdu_options.sample_synchronized ||
decoded.asdu_options.refresh_time ||
decoded.asdu_options.security ||
decoded.asdu_options.synch_source_id) {
return BinaryRuntimeProfileStatus::unsupported_device_capability;
}

// The current fixed 4I+4V TX template writes eight ordered INT32+Quality
// pairs. Refuse every other valid IEC layout instead of altering its wire.
for (std::size_t i = 0U; i < 8U; ++i) {
const auto& value = decoded.leaves[2U * i];
const auto& quality = decoded.leaves[2U * i + 1U];
if (value.wire_type != SvDeviceWireType::int32 ||
value.wire_width_bytes != 4U || value.quality || value.timestamp ||
quality.wire_type != SvDeviceWireType::quality ||
quality.wire_width_bytes != 4U || !quality.quality ||
quality.timestamp) {
return BinaryRuntimeProfileStatus::unsupported_device_capability;
}
}

RuntimePublisherProfile candidate{};
candidate.schema_version = 1U;
candidate.destination_mac = decoded.destination_mac;
candidate.app_id = decoded.app_id;
candidate.vlan_present = decoded.vlan_present;
candidate.vlan_id = decoded.vlan_id;
candidate.vlan_priority = decoded.vlan_priority;
candidate.configuration_revision = decoded.configuration_revision;
candidate.publisher_rate_hz = decoded.frame_rate_hz;
candidate.sample_counter_modulus = decoded.sample_counter_modulus;
candidate.no_asdu = decoded.no_asdu;
candidate.include_data_set = decoded.asdu_options.data_set;
candidate.include_sample_rate = decoded.asdu_options.sample_rate;
std::copy_n(decoded.sv_id.begin(), decoded.sv_id_length,
candidate.sv_id.begin());
std::copy_n(decoded.data_set_reference.begin(),
decoded.data_set_reference_length,
candidate.data_set_reference.begin());
// Generation is assigned exclusively by runtime_profile_commit().
output = candidate;
return BinaryRuntimeProfileStatus::ok;
}

} // namespace ar::esp32p4::smv
9 changes: 5 additions & 4 deletions embedded/esp32p4_smv_injector/main/live_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,12 @@ void print_identity() noexcept {
const char* version = app != nullptr ? app->version : "unknown";
#if CONFIG_AR_PTP_LAB_TX
const char* capabilities = g_control_lease_timer != nullptr
? "SMV-4I4V,PROFILE,LIVE-SETPOINTS,SESSION-LEASE,PTP-P2,SMPSYNCH-AUTO"
: "SMV-4I4V,PROFILE,LIVE-SETPOINTS,PTP-P2,SMPSYNCH-AUTO";
? "SMV-4I4V,PROFILE,LIVE-SETPOINTS,SESSION-LEASE,PTP-P2,SMPSYNCH-AUTO,PROFILE-BINARY-V1"
: "SMV-4I4V,PROFILE,LIVE-SETPOINTS,PTP-P2,SMPSYNCH-AUTO,PROFILE-BINARY-V1";
#else
const char* capabilities = g_control_lease_timer != nullptr
? "SMV-4I4V,PROFILE,LIVE-SETPOINTS,SESSION-LEASE"
: "SMV-4I4V,PROFILE,LIVE-SETPOINTS";
? "SMV-4I4V,PROFILE,LIVE-SETPOINTS,SESSION-LEASE,PROFILE-BINARY-V1"
: "SMV-4I4V,PROFILE,LIVE-SETPOINTS,PROFILE-BINARY-V1";
#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 @@ -432,6 +432,7 @@ void handle_line(char* line) noexcept {
ESP_LOGE(kTag, "START rejected: Studio control-session lease is unavailable");
return;
}
profile_control_abort_staging();
g_start_request.store(true, std::memory_order_release);
g_running.store(true, std::memory_order_release);
wake_publisher();
Expand Down
Loading
Loading