Skip to content

Merge pull request #151 from masarray/hardening/smv-p3a1-studio-binar… #938

Merge pull request #151 from masarray/hardening/smv-p3a1-studio-binar…

Merge pull request #151 from masarray/hardening/smv-p3a1-studio-binar… #938

name: ESP32-P4 SMV Injector
on:
workflow_call:
push:
paths:
- 'embedded/esp32p4_smv_injector/**'
- 'include/ariec61850/asn1/**'
- 'include/ariec61850/ethernet/**'
- 'include/ariec61850/mms/utc_time.hpp'
- 'include/ariec61850/time_sync/**'
- 'include/ariec61850/sampled_values/**'
- 'src/asn1/**'
- 'src/ethernet/**'
- 'src/mms/utc_time.cpp'
- 'src/time_sync/**'
- 'src/sampled_values/**'
- '.github/workflows/esp32p4-smv-injector.yml'
pull_request:
paths:
- 'embedded/esp32p4_smv_injector/**'
- 'include/ariec61850/asn1/**'
- 'include/ariec61850/ethernet/**'
- 'include/ariec61850/mms/utc_time.hpp'
- 'include/ariec61850/time_sync/**'
- 'include/ariec61850/sampled_values/**'
- 'src/asn1/**'
- 'src/ethernet/**'
- 'src/mms/utc_time.cpp'
- 'src/time_sync/**'
- 'src/sampled_values/**'
- '.github/workflows/esp32p4-smv-injector.yml'
permissions:
contents: read
concurrency:
group: esp32p4-smv-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
ARSTACK_FIRMWARE_VERSION: '0.1.1'
ARSTACK_PROTOCOL_VERSION: '1'
jobs:
build-esp32p4:
runs-on: ubuntu-latest
timeout-minutes: 25
container:
image: espressif/idf:v5.5.4
defaults:
run:
shell: bash
working-directory: embedded/esp32p4_smv_injector
steps:
- uses: actions/checkout@v4
- name: Select ESP32-P4 target
run: source /opt/esp/idf/export.sh && idf.py set-target esp32p4
- name: Verify P0 pre-v3 silicon policy
run: |
set -euo pipefail
grep -qx 'CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y' sdkconfig
echo 'ESP32-P4 pre-v3 build policy: PASS'
- name: Verify production PTP-P2 profile
run: |
set -euo pipefail
grep -qx 'CONFIG_ETH_TRANSMIT_MUTEX=y' sdkconfig
grep -qx 'CONFIG_AR_PTP_LAB_TX=y' sdkconfig
grep -qx 'CONFIG_AR_PTP_OPERATING_ROLE=0' sdkconfig
grep -qx 'CONFIG_AR_PTP_DOMAIN=0' sdkconfig
grep -qx 'CONFIG_AR_PTP_TRANSPORT_SPECIFIC=0' sdkconfig
grep -qx 'CONFIG_AR_PTP_RESPOND_PDELAY=y' sdkconfig
grep -qx 'CONFIG_AR_PTP_SYNC_INTERVAL_MS=250' sdkconfig
grep -qx 'CONFIG_AR_PTP_ANNOUNCE_INTERVAL_MS=1000' sdkconfig
echo 'Production timing profile: PASS · serialized shared EMAC TX · PTP-P2 LAB_SOURCE · HW timestamp adapter compiled'
- name: Verify realtime START, transport health and telemetry boundary policy
run: |
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
source = Path('main/app_main.cpp').read_text(encoding='utf-8')
cmake = Path('main/CMakeLists.txt').read_text(encoding='utf-8')
smp_synch = Path('main/smp_synch_lab.cpp').read_text(encoding='utf-8')
assert 'find_fixed_field' not in source
assert 'AR_SMP_SYNCH_INTERCEPT_TX' not in cmake
assert 'encode_into_with_layout' in source
assert 'sample_synchronization_offset' in source
assert 'smp_synch_lab_decision()' in source
assert 'wire_field_offsets.hpp' not in smp_synch
assert 'esp_eth_transmit' not in smp_synch
assert 'observe_canonical_tx_result' not in smp_synch
start_begin = source.index('if (take_start_request())')
start_end = source.index('if (!live_tx_running())', start_begin)
start_block = source[start_begin:start_end]
clock_start = start_block.rindex(
'sample_clock = start_sample_clock(task_handle, active_profile.publisher_rate_hz);'
)
assert start_block.rindex('ESP_LOGI') < clock_start
assert 'ESP_LOG' not in start_block[clock_start:]
assert 'static_assert(std::atomic<std::uint32_t>::is_always_lock_free);' in source
assert 'g_sample_tick_total.fetch_add(1U, std::memory_order_relaxed);' in source
assert 'elapsed_ticks) * 1000000ULL' in source
assert 'rate_baseline_valid = true;' in source
assert 'kTransportFailureStopThreshold = 400U' in source
assert 'live_control_force_stop();' in source
assert 'SV transport fault:' in source
print('Realtime boundary policy: PASS - encoder-owned WirePlan + direct smpSynch patch + one canonical TX failure authority')
PY
- name: Build firmware
run: source /opt/esp/idf/export.sh && idf.py build
- name: Create single-image recovery firmware
run: |
set -euo pipefail
source /opt/esp/idf/export.sh
image="arstack-esp32p4-smv-${ARSTACK_FIRMWARE_VERSION}.bin"
# idf.py runs esptool's merge_bin from inside the build directory.
# Passing build/<name> therefore resolves to build/build/<name> and fails.
idf.py merge-bin -o "${image}" -f raw
test -s "build/${image}"
- name: Create release manifest and SHA-256 contract
run: |
set -euo pipefail
image="arstack-esp32p4-smv-${ARSTACK_FIRMWARE_VERSION}.bin"
sha="$(sha256sum "build/${image}" | awk '{print $1}')"
python3 - "$image" "$sha" "$GITHUB_SHA" <<'PY'
import json
import os
import sys
image, sha256, source_commit = sys.argv[1:]
manifest = {
"schema": "arstack.studio.firmware.v1",
"product": "ARStack ESP32-P4 SMV Injector",
"chip": "esp32p4",
"chipRevisionPolicy": "pre-v3",
"version": os.environ["ARSTACK_FIRMWARE_VERSION"],
"protocol": int(os.environ["ARSTACK_PROTOCOL_VERSION"]),
"capabilities": ["SMV-4I4V", "PROFILE", "PROFILE-BINARY-V1", "LIVE-SETPOINTS", "SESSION-LEASE", "PTP-P2", "SMPSYNCH-AUTO"],
"flashOffset": 0,
"image": image,
"sha256": sha256,
"espIdf": "5.5.4",
"sourceCommit": source_commit,
}
with open("build/firmware-manifest.json", "w", encoding="utf-8", newline="\n") as handle:
json.dump(manifest, handle, indent=2, sort_keys=True)
handle.write("\n")
PY
python3 - <<'PY'
import hashlib, json, pathlib
root = pathlib.Path('build')
manifest = json.loads((root / 'firmware-manifest.json').read_text(encoding='utf-8'))
payload = (root / manifest['image']).read_bytes()
actual = hashlib.sha256(payload).hexdigest()
assert manifest['schema'] == 'arstack.studio.firmware.v1'
assert manifest['chip'] == 'esp32p4'
assert manifest['chipRevisionPolicy'] == 'pre-v3'
assert manifest['flashOffset'] == 0
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
- name: Stage Studio firmware package
run: |
set -euo pipefail
mkdir -p build/studio-firmware
cp "build/arstack-esp32p4-smv-${ARSTACK_FIRMWARE_VERSION}.bin" build/studio-firmware/
cp build/firmware-manifest.json build/studio-firmware/
sha256sum build/studio-firmware/* > build/studio-firmware/SHA256SUMS.txt
- name: Upload ESP32-P4 firmware package
uses: actions/upload-artifact@v4
with:
name: arstack-esp32p4-smv-firmware
path: embedded/esp32p4_smv_injector/build/studio-firmware/
if-no-files-found: error
retention-days: 14