Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7fbd46e
feat(m3b): pure bounded, explicit boot PTP SOURCE admission policy
masarray Oct 23, 2026
2b012d2
test(m3b): boot-source gating, manual override, retries, and monotoni…
masarray Oct 23, 2026
c3e040d
feat(m3b): declare manual PTP precedence over boot timebase startup
masarray Oct 23, 2026
65b19ea
feat(m3b): atomic operator-override latch prevents background PTP SOU…
masarray Oct 23, 2026
bb09c8f
feat(m3b): manual PTP START/STOP/CONFIG dominates boot supervisor
masarray Oct 23, 2026
e37493d
feat(m3b): guarded opt-in isolated bench boot PTP source policy
masarray Oct 23, 2026
e55d4d4
feat(m3b): enable explicitly lab-scoped boot clock supervisor in inje…
masarray Oct 23, 2026
2c43249
feat(m3b): boot SOURCE supervisor with PHY readiness, bounded retries…
masarray Oct 23, 2026
c543c99
fix(m3b): explicit PTP START cannot reset an already-ready boot EMAC …
masarray Oct 23, 2026
fab7f6a
fix(m3b): Studio shutdown stops SV only, preserves ESP32 always-on PT…
masarray Oct 23, 2026
c2fda00
test(m3b): Studio exit must stop SV without issuing PTP STOP
masarray Oct 23, 2026
32b6cf3
refactor(m3b): keep low-rate boot clock observer after operator role …
masarray Oct 23, 2026
3da865a
ci(m3b): matched-source firmware preview for isolated Draft M3.B
masarray Oct 23, 2026
4075592
ci(m3b): run exact-head GCC/Clang boot-timebase regressions on Draft
masarray Oct 23, 2026
3d766e9
ci(policy): verify exact M3.B Draft heavy-build allowlist without wid…
masarray Oct 23, 2026
da83457
feat(m3b): explicit link-loss recovery gate, never SV STOP driven
masarray Oct 23, 2026
567c8ff
feat(m3b): bounded physical link recovery, preserve authority on SV STOP
masarray Oct 23, 2026
697eda8
test(m3b): PHY recovery cannot confuse operator SV STOP with GM restart
masarray Oct 23, 2026
8e31a73
fix(m3b): expire link-loss restart permission when original PTP SOURC…
masarray Oct 23, 2026
f3b6fdf
ci(m3b): fail paired firmware packaging if boot SOURCE autostart was …
masarray Oct 23, 2026
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
5 changes: 4 additions & 1 deletion .github/workflows/esp32p4-smv-injector.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
# On draft consolidation, do not run ordinary expensive standalone builds.
# The Studio Native Preview explicitly requests a paired firmware via
# workflow_call phase_sync_preview=true; allow only that purposeful build.
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft || (inputs.phase_sync_preview == true && (github.event.pull_request.head.ref == 'feat/ptp-source-policy-fsm-20261022' || github.event.pull_request.head.ref == 'fix/autolive-from-pcap-proven-7c95-20261023' || github.event.pull_request.head.ref == 'feat/m3a-persistent-sv-grid-20261023')) || github.event.pull_request.head.ref == 'feat/m3a-persistent-sv-grid-20261023' }}
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft || (inputs.phase_sync_preview == true && (github.event.pull_request.head.ref == 'feat/ptp-source-policy-fsm-20261022' || github.event.pull_request.head.ref == 'fix/autolive-from-pcap-proven-7c95-20261023' || github.event.pull_request.head.ref == 'feat/m3a-persistent-sv-grid-20261023' || github.event.pull_request.head.ref == 'feat/m3b-boot-timebase-supervisor-20261023')) || github.event.pull_request.head.ref == 'feat/m3a-persistent-sv-grid-20261023' || github.event.pull_request.head.ref == 'feat/m3b-boot-timebase-supervisor-20261023' }}
runs-on: ubuntu-latest
timeout-minutes: 25
env:
Expand Down Expand Up @@ -155,6 +155,9 @@ jobs:
run: |
set -euo pipefail
grep -qx 'CONFIG_AR_SMV_PTP_PHASE_SERVO_EXPERIMENTAL=y' sdkconfig
# M3.B must really be linked into the exact shipped ESP32-P4 binary.
# A disabled dependency would otherwise silently negate sdkconfig.defaults.
grep -qx 'CONFIG_AR_PTP_BOOT_SOURCE_AUTOSTART=y' sdkconfig
grep -qx 'CONFIG_AR_SMV_HW_TX_EVIDENCE_EXPERIMENTAL=y' sdkconfig
grep -qx '# CONFIG_AR_SMV_BROADCAST_MIRROR is not set' sdkconfig
grep -qx 'CONFIG_AR_PTP_OPERATING_ROLE=0' sdkconfig
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ptp-core-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ concurrency:
jobs:
portable-ptp:
# Exactly this isolated Draft M3.A runs GCC/Clang phase regression.
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft || github.event.pull_request.head.ref == 'feat/m3a-persistent-sv-grid-20261023' }}
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft || (github.event.pull_request.head.ref == 'feat/m3a-persistent-sv-grid-20261023' || github.event.pull_request.head.ref == 'feat/m3b-boot-timebase-supervisor-20261023') }}
strategy:
fail-fast: false
matrix:
Expand Down
6 changes: 5 additions & 1 deletion apps/arstack_studio/src/DeviceController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,11 @@ bool DeviceController::shutdown() {
shuttingDown_ = true;
pendingAutoDetect_ = false;
pendingConnectPort_.clear();
const bool requestPtpStop = needsPtpStopBeforeShutdown();
// M3.B: Studio owns the SV output lease, never the board's persistent
// time authority. On USB/Studio shutdown request only SV STOP; stopping
// PTP here would destroy clock phase across a simple GUI disconnect.
// Explicit operator PTP STOP remains a separate verified command.
constexpr bool requestPtpStop = false;
resetPtpStartup();
resetScenarioStart();

Expand Down
6 changes: 6 additions & 0 deletions apps/arstack_studio/tests/check_operator_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ def main() -> None:
"running 4I4V edits must auto-apply once after commit without STOP/START, "
"not on each keystroke, and queue the newest revision while ACK pending")
controller = (STUDIO / "src/DeviceController.cpp").read_text(encoding="utf-8")
# M3.B: the ESP owns PTP across USB/Studio exit; the host may withdraw
# its 4I4V output lease but must never silently send PTP STOP.
require("constexpr bool requestPtpStop = false;" in controller
and "const bool requestSvStop = running_;" in controller
and "ioWorker_->shutdown(requestSvStop, requestPtpStop);" in controller,
"Studio session retirement must stop SV without killing always-on PTP")
require("logUpdateTimer_.setInterval(100)" in controller
and "if (!logUpdateTimer_.isActive()) logUpdateTimer_.start();" in controller,
"serial log QML notification must be coalesced to avoid GUI stutter")
Expand Down
14 changes: 14 additions & 0 deletions embedded/esp32p4_smv_injector/main/Kconfig.projbuild
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ config AR_SMV_HW_TX_EVIDENCE_EXPERIMENTAL
unavailable, never substituted with esp_timer time or claimed as
proof that a relay has synchronized. Defaults OFF.

config AR_PTP_BOOT_SOURCE_AUTOSTART
bool "LAB ONLY: start local PTP SOURCE and SV dry sample grid on boot"
default n
depends on AR_PTP_LAB_TX && AR_SMV_PTP_PHASE_SERVO_EXPERIMENTAL
help
Explicit isolated-laboratory feature: after PHY link is verified,
start the configured PTP LAB SOURCE automatically once at boot.
Existing inactive-SV preflight then aligns its dry GPTimer without
requiring Studio START or a USB serial session. The boot supervisor
never starts SOURCE when role is RECEIVER/MONITOR, never restarts
after operator PTP START/STOP/CONFIG, and limits failed retries.
SV Ethernet transmission remains OFF until operator SV START.
Never enable while connected to an energized utility process bus.

config AR_PTP_OPERATING_ROLE
int "PTP operating role (0=lab source, 1=time receiver, 2=monitor)"
range 0 2
Expand Down
101 changes: 101 additions & 0 deletions embedded/esp32p4_smv_injector/main/app_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "ariec61850/sampled_values/frame_codec.hpp"
#include "ariec61850/sampled_values/live_signal_state.hpp"
#include "ariec61850/time_sync/sv_ptp_epoch.hpp"
#include "ariec61850/time_sync/sv_boot_clock_policy.hpp"
#include "ariec61850/time_sync/sv_ptp_clock_lease.hpp"
#include "ariec61850/time_sync/ptp_runtime.hpp"

Expand Down Expand Up @@ -784,6 +785,98 @@ void stop_sample_clock(gptimer_handle_t& timer) noexcept {
#endif
}

#if AR_SV_PTP_PHASE_SUPPORTED && defined(CONFIG_AR_PTP_BOOT_SOURCE_AUTOSTART) && CONFIG_AR_PTP_BOOT_SOURCE_AUTOSTART
// CPU0 control-plane only. PTP SOURCE and the dry sample grid are brought
// online from boot without waiting for a USB host or a first SV START.
// Publisher ISR/smpCnt/BER remain unchanged. Never issue an SV START here.
void sv_boot_clock_supervisor_task(void*) {
using Boot = ar::iec61850::time_sync::SvBootClockPolicy;
std::uint32_t attempts = 0U;
std::uint32_t retry_after_ms = 0U;
bool ready_once = false;
bool observed_link_loss = false;
bool reported_grid = false;
bool reported_operator_override = false;
// Let the control and publisher tasks bind before background admission.
vTaskDelay(pdMS_TO_TICKS(1000));
ESP_LOGI(kTag, "M3B boot timebase: enabled for isolated SOURCE profile; SV remains OFF");
while (true) {
const bool operator_overrode =
!ar_ptp_boot_supervisor_autostart_allowed();
ar_ptp_lab_config_t config{};
const bool source_selected =
ar_ptp_lab_get_config(&config) &&
config.role == AR_PTP_ROLE_LAB_SOURCE;
const bool link_ready =
(xEventGroupGetBits(g_link_events) & kLinkUpBit) != 0U;
const bool runtime_owned =
ar_ptp_source_runtime_active() || ar_ptp_lab_is_running();
const auto now_ms =
static_cast<std::uint32_t>(esp_timer_get_time() / 1000LL);
if (ready_once && !link_ready) observed_link_loss = true;
// If the original runtime survived the PHY cycle, it remains the
// sole time authority. Do not retain a stale link-loss permission
// that could restart it after an unrelated future task failure.
if (observed_link_loss && link_ready && runtime_owned)
observed_link_loss = false;
if (Boot::may_recover_after_link_cycle(
ready_once, observed_link_loss, link_ready, runtime_owned,
operator_overrode, source_selected)) {
// Only a real Ethernet link cycle may re-arm a lost boot SOURCE.
// SOURCE generation changes and SV phase claims requalify rather
// than inheriting stale global/LOCAL evidence.
ready_once = false;
observed_link_loss = false;
attempts = 0U;
retry_after_ms = now_ms + 2000U;
ESP_LOGW(kTag,
"M3B PHY recovered with no PTP runtime; retry SOURCE after guard");
}
if (Boot::may_start(
!ready_once, operator_overrode, source_selected,
link_ready, runtime_owned, attempts, now_ms, retry_after_ms)) {
++attempts;
// Blocking verified SOURCE start is allowed only in this
// low-priority worker. It is never called from ISR/publisher.
const bool started = ar_esp32p4_ptp_start();
if (started) {
ready_once = true; // never auto-reseed after a later STOP
ESP_LOGI(kTag,
"M3B SOURCE READY from boot: attempt=%lu; waiting for dry SV grid",
static_cast<unsigned long>(attempts));
} else {
retry_after_ms = now_ms + Boot::retry_delay_ms(attempts);
ESP_LOGW(kTag,
"M3B boot SOURCE not ready: attempt=%lu/%lu; bounded retry",
static_cast<unsigned long>(attempts),
static_cast<unsigned long>(Boot::kMaxAttempts));
}
}
const bool dry_phase_observed =
g_preflight_phase_clock_active.load(std::memory_order_acquire) &&
g_sample_clock_context.epoch_generation.load(
std::memory_order_acquire) != 0U &&
g_sample_clock_context.last_phase_observed_epoch.load(
std::memory_order_acquire) != 0U;
if (dry_phase_observed && !reported_grid) {
ESP_LOGI(kTag,
"M3B dry PTP/SV sample grid tracking before injection; SV TX OFF");
reported_grid = true;
} else if (!dry_phase_observed) {
reported_grid = false; // SOURCE/GM fault, operator STOP or relock
}
if (operator_overrode && !reported_operator_override) {
ESP_LOGI(kTag,
"M3B operator owns PTP; boot SOURCE autostart disabled until reboot");
reported_operator_override = true;
}
// Monitoring continues quietly even after manual PTP role control.
vTaskDelay(pdMS_TO_TICKS(
operator_overrode || ready_once ? 1000U : Boot::kPollPeriodMs));
}
}
#endif

void publisher_task(void* argument) {
#if CONFIG_AR_SMV_LIVE_TX && CONFIG_AR_SMV_P2_REALTIME_50HZ
const auto eth_handle = static_cast<esp_eth_handle_t>(argument);
Expand Down Expand Up @@ -1905,6 +1998,14 @@ extern "C" void app_main(void) {
ESP_LOGE(kTag, "Failed to create telemetry task on CPU0");
return;
}
#if AR_SV_PTP_PHASE_SUPPORTED && defined(CONFIG_AR_PTP_BOOT_SOURCE_AUTOSTART) && CONFIG_AR_PTP_BOOT_SOURCE_AUTOSTART
if (xTaskCreatePinnedToCore(
&sv_boot_clock_supervisor_task, "ar_smv_boot_clock", 4096,
nullptr, 1, nullptr, 0) != pdPASS) {
// The verified legacy manual PTP START remains available.
ESP_LOGE(kTag, "M3B boot clock supervisor creation failed; manual PTP available");
}
#endif
#else
ESP_LOGW(kTag, "Live transmission disabled; Ethernet bring-up only");
#endif
Expand Down
8 changes: 8 additions & 0 deletions embedded/esp32p4_smv_injector/main/ethernet_port.c
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,14 @@ bool ar_esp32p4_ptp_start(void)
return false;
}

// M3.B: Studio may send its ordinary explicit PTP START after boot
// already verified the SOURCE. This command is idempotent, and must
// never tear down/reseed an active shared EMAC timebase.
if (ar_ptp_lab_is_running()) return true;
if (ar_ptp_source_runtime_active()) {
s_ptp_preflight_failure = "PTP_RUNTIME_ALREADY_ACTIVE";
return false; // a worker is starting/stopping; do not roll it back
}
// SOURCE start is accepted only after verified Announce + Sync + Follow_Up
// transmission. A failed start must not leave the shared EMAC in IEEE1588
// timestamp mode because SMV uses the same MAC immediately afterwards.
Expand Down
3 changes: 3 additions & 0 deletions embedded/esp32p4_smv_injector/main/live_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ void handle_ptp_command(char* save) noexcept {
ESP_LOGE(kTag, "Usage: PTP START");
return;
}
ar_ptp_boot_supervisor_operator_override();
if (!ar_esp32p4_ptp_start()) {
ar_ptp_lab_config_t active{};
const bool is_source = ar_ptp_lab_get_config(&active) &&
Expand All @@ -433,11 +434,13 @@ void handle_ptp_command(char* save) noexcept {
ESP_LOGE(kTag, "Usage: PTP STOP");
return;
}
ar_ptp_boot_supervisor_operator_override();
ar_ptp_lab_stop();
ESP_LOGI(kTag, "PTP stop requested");
return;
}
if (std::strcmp(subcommand, "CONFIG") == 0) {
ar_ptp_boot_supervisor_operator_override();
std::array<std::uint32_t, 8> values{};
for (auto& value : values) {
if (!parse_u32(strtok_r(nullptr, kTokenDelimiters.data(), &save), value)) {
Expand Down
8 changes: 8 additions & 0 deletions embedded/esp32p4_smv_injector/main/ptp_lab_task.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ bool ar_ptp_lab_get_config(ar_ptp_lab_config_t* config);
*/
bool ar_ptp_lab_try_start(esp_eth_handle_t eth_handle);

// M3.B boot-only supervisor. Operator PTP START/STOP/CONFIG permanently
// disables background SOURCE-start attempts until the next device boot.
// It never disables the actual timebase or SV sample grid on SV STOP.
void ar_ptp_boot_supervisor_operator_override(void);
bool ar_ptp_boot_supervisor_autostart_allowed(void);
// Runtime ownership is not equivalent to verified SOURCE READY.
bool ar_ptp_source_runtime_active(void);

/**
* Legacy start entry point retained for P1.5 source ABI compatibility.
* New user-facing control paths should call ar_ptp_lab_try_start().
Expand Down
12 changes: 12 additions & 0 deletions embedded/esp32p4_smv_injector/main/ptp_mode_facade.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ portMUX_TYPE g_ptp_facade_mux = portMUX_INITIALIZER_UNLOCKED;
ar_ptp_lab_config_t g_facade_override{};
bool g_facade_override_valid = false;
std::atomic_bool g_receiver_stop_pending{false};
// Cancel only automatic boot attempts; operator PTP actions are authoritative.
std::atomic_bool g_boot_source_autostart_allowed{true};
ar::iec61850::time_sync::PtpControlCancellationGate g_role_control;

void stop_active_ptp_roles() noexcept;
Expand Down Expand Up @@ -236,6 +238,16 @@ void log_p2_status(const ar_ptp_lab_status_t& status) noexcept {
} // namespace
} // namespace ar::esp32p4::smv

extern "C" void ar_ptp_boot_supervisor_operator_override(void) {
ar::esp32p4::smv::g_boot_source_autostart_allowed.store(
false, std::memory_order_release);
}

extern "C" bool ar_ptp_boot_supervisor_autostart_allowed(void) {
return ar::esp32p4::smv::g_boot_source_autostart_allowed.load(
std::memory_order_acquire);
}

extern "C" void ar_ptp_lab_get_default_config(ar_ptp_lab_config_t* config) {
if (config == nullptr) return;
*config = ar::esp32p4::smv::default_config();
Expand Down
3 changes: 3 additions & 0 deletions embedded/esp32p4_smv_injector/sdkconfig.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ CONFIG_AR_SMV_VOLTAGE_RMS_MV=57740
# v0.1.1 production timing companion. LAB_SOURCE preserves the existing
# ARIEC61850/ARSVIN lab behavior while using ESP32-P4 IEEE1588 HW timestamps.
CONFIG_AR_PTP_LAB_TX=y
# M3.B isolated bench: PTP SOURCE starts after PHY link, irrespective of USB
# Studio connection; SV remains OFF and the 4-kHz grid prequalifies silently.
CONFIG_AR_PTP_BOOT_SOURCE_AUTOSTART=y
CONFIG_AR_PTP_OPERATING_ROLE=0
CONFIG_AR_PTP_DOMAIN=0
CONFIG_AR_PTP_TRANSPORT_SPECIFIC=0
Expand Down
44 changes: 44 additions & 0 deletions include/ariec61850/time_sync/sv_boot_clock_policy.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include <cstdint>

namespace ar::iec61850::time_sync {

// Control-plane-only gate for opt-in isolated-LAB SOURCE startup. No task,
// allocation, ISR, UART, clock read or network operation is hidden here.
// Never start another GM when RECEIVER/MONITOR or an active runtime is present.
struct SvBootClockPolicy final {
static constexpr std::uint32_t kPollPeriodMs = 250U;
static constexpr std::uint32_t kMaxAttempts = 4U;

[[nodiscard]] static constexpr bool may_start(
const bool opted_in, const bool operator_overrode,
const bool selected_source, const bool eth_tx_ready,
const bool ptp_runtime_owned, const std::uint32_t attempts,
const std::uint32_t now_ms, const std::uint32_t retry_after_ms) noexcept {
return opted_in && !operator_overrode && selected_source &&
eth_tx_ready && !ptp_runtime_owned && attempts < kMaxAttempts &&
static_cast<std::int32_t>(now_ms - retry_after_ms) >= 0;
}

// Re-arm automatic startup ONLY after a genuine PHY link-down/up cycle.
// A plain operator SV STOP, USB loss, SOURCE task fault or GUI action
// never permits silent grandmaster promotion/re-seeding.
[[nodiscard]] static constexpr bool may_recover_after_link_cycle(
const bool once_ready, const bool observed_link_loss,
const bool link_now_ready, const bool runtime_owned,
const bool operator_overrode, const bool selected_source) noexcept {
return once_ready && observed_link_loss && link_now_ready &&
!runtime_owned && !operator_overrode && selected_source;
}

// Bounded exponential retry only after genuine startup failure. Never
// keep re-seeding the shared EMAC timebase indefinitely.
[[nodiscard]] static constexpr std::uint32_t retry_delay_ms(
const std::uint32_t failed_attempts) noexcept {
const auto capped = failed_attempts > 4U ? 4U : failed_attempts;
return 1000U << capped; // 2, 4, 8, 16 seconds
}
};
} // namespace ar::iec61850::time_sync
20 changes: 18 additions & 2 deletions scripts/check_ci_trigger_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,22 @@ def main():
"github.event.pull_request.head.ref == "
"'feat/m3a-persistent-sv-grid-20261023' }}"
)
# This Draft M3.B is stacked on M3.A; only these two exact
# head branches may run expensive portable/firmware validation.
m3b_ptp_gate = m3a_ptp_gate.replace(
"github.event.pull_request.head.ref == 'feat/m3a-persistent-sv-grid-20261023'",
"(github.event.pull_request.head.ref == 'feat/m3a-persistent-sv-grid-20261023' || "
"github.event.pull_request.head.ref == 'feat/m3b-boot-timebase-supervisor-20261023')"
)
m3b_firmware_gate = m3a_firmware_gate.replace(
"'feat/m3a-persistent-sv-grid-20261023')) ||",
"'feat/m3a-persistent-sv-grid-20261023' || github.event.pull_request.head.ref == "
"'feat/m3b-boot-timebase-supervisor-20261023')) ||"
).replace(
"'feat/m3a-persistent-sv-grid-20261023' }}",
"'feat/m3a-persistent-sv-grid-20261023' || github.event.pull_request.head.ref == "
"'feat/m3b-boot-timebase-supervisor-20261023' }}"
)
for index, boundary in enumerate(boundaries):
job_name = boundary.group(1)
block_end = (
Expand All @@ -140,12 +156,12 @@ def main():
if if_lines == [gate]:
continue
if name == "esp32p4-smv-injector.yml" and job_name == "build-esp32p4":
assert if_lines == [m3a_firmware_gate], (
assert if_lines == [m3b_firmware_gate], (
name, job_name, "unexpected Draft firmware bypass"
)
continue
if name == "ptp-core-ci.yml" and job_name == "portable-ptp":
assert if_lines == [m3a_ptp_gate], (
assert if_lines == [m3b_ptp_gate], (
name, job_name, "unexpected Draft PTP regression bypass"
)
continue
Expand Down
Loading
Loading