Repository navigation
M2A: bounded continuous PTP–SMV phase authority and guarded ESP32-P4 observer #1013
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: ESP32-P4 SMV Injector | |
| on: | |
| workflow_call: | |
| workflow_dispatch: | |
| push: | |
| branches: [main] | |
| 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: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| 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: | |
| if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} | |
| 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=1000' 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 |