#include "App.h"
#include "Config.h"
#include "Config_Text.h"
#include "Log.h"
#include <WiFi.h>
#include <esp_mac.h>
#include <esp_sleep.h>
#include <esp_system.h>
#include <esp32-hal-cpu.h>
#include <driver/gpio.h>
#include <Wire.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>

namespace {
constexpr uint8_t MENU_OPTICAL_CALIBRATION_ITEM = 7;

const char *uiFailName(FailReason reason);

const char *appStateName(AppState state) {
  static const char *names[] = {"IDLE", "MENU", "BOARD_TEST", "SOLO_MEASURE", "SOLO_DRIVER", "MASTER_DISCOVER",
    "MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START",
    "SLAVE_MEASURE", "SLAVE_WAIT_ACK", "FINISHED"};
  const uint8_t index = static_cast<uint8_t>(state);
  return index < sizeof(names) / sizeof(names[0]) ? names[index] : "UNKNOWN";
}

const char *buttonEventName(ButtonEvent event) {
  static const char *names[] = {"NONE", "SHORT", "LONG", "REPEAT"};
  const uint8_t index = static_cast<uint8_t>(event);
  return index < sizeof(names) / sizeof(names[0]) ? names[index] : "UNKNOWN";
}

uint32_t pulseFromDuty(float hz, float dutyPct) {
  return hz > 0.0f ? static_cast<uint32_t>(lroundf(dutyPct * 10000000.0f / hz)) : 0U;
}

float dutyFromPulse(uint32_t hz, uint32_t pulseNs) {
  return static_cast<float>(static_cast<double>(hz) * pulseNs / 10000000.0);
}

bool configuredTxPulseLightOn(const Settings &settings) {
  return static_cast<TestKind>(settings.testKind) == TestKind::DRIVER
    ? txActiveLightOn(settings) : true;
}

const char *configuredLevelName(const Settings &settings) {
  return static_cast<TestKind>(settings.testKind) == TestKind::DRIVER
    ? lightCodeName(static_cast<LightCode>(settings.lightCode)) : "AUTO";
}

void formatTarget(uint32_t hz, uint32_t pulseNs, char *out, size_t size) {
  char frequency[16], pulse[12];
  Display::formatPwmFrequency(hz, frequency, sizeof(frequency));
  Display::formatPulse(pulseNs, pulse, sizeof(pulse));
  snprintf(out, size, UiText::TEST_FORMAT, frequency, pulse);
}

void formatMeasured(float hz, uint32_t pulseNs, char *out, size_t size) {
  char frequency[12], pulse[12];
  Display::formatFrequency(hz, frequency, sizeof(frequency));
  Display::formatPulse(pulseNs, pulse, sizeof(pulse), true);
  snprintf(out, size, "F:%s, P:%s", frequency, pulse);
}

void formatTestTarget(uint32_t hz, uint32_t pulseNs, char *out, size_t size) {
  char target[32];
  formatTarget(hz, pulseNs, target, sizeof(target));
  snprintf(out, size, UiText::TEST_TARGET_FORMAT, target);
}

void formatFailure(FailReason reason, uint32_t hz, uint32_t pulseNs,
                   char *out, size_t size) {
  (void)reason;
  char target[32];
  formatTarget(hz, pulseNs, target, sizeof(target));
  snprintf(out, size, UiText::FAIL_TARGET_FORMAT, target);
}

void formatElapsedNs(uint64_t ns, char *out, size_t size) {
  // Compact form keeps error timing within 21 OLED columns.
  // Exact nanoseconds remain available in the Serial diagnostic.
  if (ns < 1000ULL) snprintf(out, size, "%llun", ns);
  else if (ns < 1000000ULL) snprintf(out, size, "%.1fu", ns / 1000.0);
  else if (ns < 1000000000ULL) snprintf(out, size, "%.0fm", ns / 1000000.0);
  else snprintf(out, size, "%.2fs", ns / 1000000000.0);
}

size_t utf8CharacterCount(const char *text) {
  size_t count = 0;
  while (text && *text) {
    const uint8_t byte = static_cast<uint8_t>(*text++);
    // Continuation bytes (10xxxxxx) belong to the preceding UTF-8 character.
    if ((byte & 0xC0U) != 0x80U) ++count;
  }
  return count;
}

const char *uiRoleName(Role role) {
  const uint8_t index = static_cast<uint8_t>(role);
  return index < sizeof(UiText::ROLE_NAMES) / sizeof(UiText::ROLE_NAMES[0])
    ? UiText::ROLE_NAMES[index] : "?";
}

const char *uiFailName(FailReason reason) {
  const uint8_t index = static_cast<uint8_t>(reason);
  return index < sizeof(UiText::FAIL_NAMES) / sizeof(UiText::FAIL_NAMES[0])
    ? UiText::FAIL_NAMES[index] : "UNKNOWN";
}

const char *roleCorner(Role role) {
  static const char *markers[] = {"O", "M", "S"};
  const uint8_t index = static_cast<uint8_t>(role);
  return index < sizeof(markers) / sizeof(markers[0]) ? markers[index] : "?";
}

void formatMenuLine(const char *label, const char *value, char *out, size_t size) {
  constexpr size_t OLED_TEXT_COLUMNS = 21;
  const size_t labelLength = utf8CharacterCount(label);
  const size_t valueLength = utf8CharacterCount(value);
  const size_t usedColumns = labelLength + valueLength;
  const int padding = static_cast<int>(
    usedColumns < OLED_TEXT_COLUMNS ? OLED_TEXT_COLUMNS - usedColumns : 0U);
  snprintf(out, size, "%s%*s%s", label, padding, "", value);
}

uint32_t overallProgress(uint32_t stageIndex, uint8_t step,
                         uint8_t stepsPerStage = MEASUREMENT_PROGRESS_STEPS) {
  if (step > stepsPerStage) step = stepsPerStage;
  return stageIndex * stepsPerStage + step;
}

uint32_t overallProgressTotal(
    uint32_t stageCount,
    uint8_t stepsPerStage = MEASUREMENT_PROGRESS_STEPS) {
  return stageCount * stepsPerStage;
}

uint32_t stageWallTimeMs(uint32_t testTimeMs, uint32_t frequencyHz) {
  return static_cast<uint32_t>((nominalStageUs(frequencyHz, testTimeMs, PWM_SETTLE_CYCLES) + 999ULL) / 1000ULL);
}

const char *uiTestName(TestKind kind) {
  const uint8_t index = static_cast<uint8_t>(kind);
  return index < sizeof(UiText::TEST_NAMES) / sizeof(UiText::TEST_NAMES[0])
    ? UiText::TEST_NAMES[index] : "?";
}

const char *uiTestGroupName(TestGroup group) {
  const uint8_t index = static_cast<uint8_t>(group);
  return index < sizeof(UiText::TEST_GROUP_NAMES) / sizeof(UiText::TEST_GROUP_NAMES[0])
    ? UiText::TEST_GROUP_NAMES[index] : "?";
}

const char *uiBoardTestName(BoardTest test) {
  const uint8_t index = static_cast<uint8_t>(test);
  return index < sizeof(UiText::BOARD_TEST_NAMES) / sizeof(UiText::BOARD_TEST_NAMES[0])
    ? UiText::BOARD_TEST_NAMES[index] : "?";
}

bool boardTestAvailable(BoardTest test) {
  if (test == BoardTest::PWM_OUTPUT) return true;
  if (test == BoardTest::ADC) return BOARD_RX_USES_ADC && BOARD_ADC_AVAILABLE;
  if (test == BoardTest::RX_INPUT) return !BOARD_RX_USES_ADC;
  return false;
}

BoardTest defaultBoardTest() {
  return BOARD_RX_USES_ADC && BOARD_ADC_AVAILABLE
    ? BoardTest::ADC : BoardTest::RX_INPUT;
}

uint8_t lastValidMaxPulseIndex(uint32_t frequencyHz) {
  uint8_t last = static_cast<uint8_t>(countOf(MAX_PULSE_OPTIONS_NS) - 1U);
  while (last && static_cast<uint64_t>(MAX_PULSE_OPTIONS_NS[last]) * frequencyHz >= 1000000000ULL)
    --last;
  return last;
}

uint8_t firstMaxPulseIndexAtLeast(uint32_t pulseNs, uint8_t last) {
  for (uint8_t i = 0; i <= last; ++i)
    if (MAX_PULSE_OPTIONS_NS[i] >= pulseNs) return i;
  return last;
}

uint8_t lastMinPulseIndexAtMost(uint32_t pulseNs) {
  for (size_t i = countOf(MIN_PULSE_OPTIONS_NS); i > 0; --i)
    if (MIN_PULSE_OPTIONS_NS[i - 1U] <= pulseNs) return static_cast<uint8_t>(i - 1U);
  return 0;
}

uint32_t plannedPulseCaptureHz(uint32_t frequencyHz, float dutyPct) {
  if (!frequencyHz || dutyPct <= 0.0f || dutyPct >= 100.0f) return 0;
  return MCPWM_CAPTURE_RESOLUTION_HZ;
}

uint32_t plannedCaptureHz(uint32_t frequencyHz, float dutyPct) {
  // One 32-bit S3 MCPWM capture timer measures period and pulse at 80 MHz.
  return plannedPulseCaptureHz(frequencyHz, dutyPct);
}

bool pulsePointHasResolution(uint32_t hz, uint32_t pulseNs, float accuracyPct) {
  uint32_t actualHz = 0, actualPulseNs = 0;
  uint8_t bits = 0;
  if (TARGET_IS_C3) {
    IntegerPwmConfig config = {};
    if (!choosePwmConfig(hz, pulseNs, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS, config)) return false;
    actualHz = config.actualHz;
    actualPulseNs = config.actualPulseNs;
    bits = config.bits;
  } else {
    if (!hz || MCPWM_RESOLUTION_HZ % hz) return false;
    const uint32_t periodTicks = MCPWM_RESOLUTION_HZ / hz;
    if (periodTicks < 2U || periodTicks > MCPWM_MAX_PERIOD_TICKS) return false;
    uint32_t activeTicks = static_cast<uint32_t>(
      (static_cast<uint64_t>(pulseNs) * MCPWM_RESOLUTION_HZ + 500000000ULL) / 1000000000ULL);
    if (!activeTicks || activeTicks >= periodTicks) return false;
    actualHz = hz;
    actualPulseNs = static_cast<uint32_t>(
      (static_cast<uint64_t>(activeTicks) * 1000000000ULL + MCPWM_RESOLUTION_HZ / 2U) /
      MCPWM_RESOLUTION_HZ);
    bits = 1;
    for (uint32_t ticks = periodTicks; ticks > 1U; ticks >>= 1U) ++bits;
  }
  if (!periodWithin(actualHz, hz, accuracyPct) ||
      !periodWithin(actualPulseNs, pulseNs, accuracyPct)) return false;
  const float dutyPct = dutyFromPulse(actualHz, actualPulseNs);
  const uint32_t captureHz = plannedCaptureHz(actualHz, dutyPct);
  const uint32_t pulseCaptureHz = plannedPulseCaptureHz(actualHz, dutyPct);
  return captureHz && pulseCaptureHz && validateResolution(actualHz, dutyPct, accuracyPct,
    captureHz, pulseCaptureHz, bits, MEASUREMENT_AVERAGING_PERIODS) == FailReason::NONE;
}

uint32_t minimumPulseForAccuracy(uint32_t frequencyHz, float accuracyPct) {
  for (uint32_t pulseNs : TEST_PULSE_WIDTHS_NS)
    if (pulsePointHasResolution(frequencyHz, pulseNs, accuracyPct)) return pulseNs;
  return UINT32_MAX;
}

uint8_t firstMinPulseIndexAtLeast(uint32_t pulseNs, uint8_t last) {
  for (uint8_t i = 0; i <= last; ++i)
    if (MIN_PULSE_OPTIONS_NS[i] >= pulseNs) return i;
  return last;
}

uint8_t cycleIndex(uint8_t value, uint8_t first, uint8_t last, int direction) {
  if (first >= last) return first;
  if (direction > 0) return value >= last ? first : static_cast<uint8_t>(value + 1U);
  return value <= first ? last : static_cast<uint8_t>(value - 1U);
}

uint8_t nextMenuItem(uint8_t current, TestGroup group) {
  if (group == TestGroup::BOARD)
    return current == 0U ? MENU_OPTICAL_CALIBRATION_ITEM : 0U;
  return current >= MENU_OPTICAL_CALIBRATION_ITEM ? 0U
    : static_cast<uint8_t>(current + 1U);
}

bool parseUnsigned(const char *text, uint32_t &value) {
  if (!text || !*text || *text == '-') return false;
  char *end = nullptr;
  const unsigned long parsed = strtoul(text, &end, 10);
  if (!end || *end) return false;
  value = static_cast<uint32_t>(parsed);
  return true;
}

template <size_t N>
int optionIndex(const uint32_t (&options)[N], uint32_t value) {
  for (size_t i = 0; i < N; ++i)
    if (options[i] == value) return static_cast<int>(i);
  return -1;
}

int accuracyOptionIndex(const char *text) {
  if (!text || !*text) return -1;
  char *end = nullptr;
  const float value = strtof(text, &end);
  if (!end || *end) return -1;
  for (size_t i = 0; i < countOf(ACCURACY_OPTIONS_PCT); ++i)
    if (fabsf(ACCURACY_OPTIONS_PCT[i] - value) < 0.001f) return static_cast<int>(i);
  return -1;
}

void lowerAscii(char *text) {
  for (; text && *text; ++text)
    if (*text >= 'A' && *text <= 'Z') *text = static_cast<char>(*text - 'A' + 'a');
}
}

App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE),
  measurement_(receiver_), driverTest_(receiver_) {}

void App::begin() {
  Serial.begin(SERIAL_BAUD);
#if ARDUINO_USB_CDC_ON_BOOT
  Serial.setTxTimeoutMs(SERIAL_TX_TIMEOUT_MS);
#endif
  Log::printf("BOOT", "firmware start, Serial=%lu baud", SERIAL_BAUD);
  startButton_.begin(); modeButton_.begin(); pwm_.begin();
  bootCheckStartedMs_ = millis();
  bootResetCandidate_ = startButton_.pressed() && modeButton_.pressed();
  Log::printf("BOOT", "buttons initialized, factory-reset candidate=%s", bootResetCandidate_ ? "YES" : "NO");
  if (!bootResetCandidate_) finishInitialization(false);
}

void App::finishInitialization(bool factoryReset) {
  if (initialized_) return;
  Log::printf("BOOT", "initialization continues, factory-reset=%s", factoryReset ? "YES" : "NO");
  if (factoryReset) {
    store_.defaults(settings_); store_.save(settings_); Log::event("BOOT", "FACTORY DEFAULTS RESTORED");
  } else if (!store_.load(settings_)) {
    store_.save(settings_); Log::event("BOOT", "NVS invalid/missing: defaults loaded");
  }
  sanitizeRange();
  params_ = store_.params(settings_);
  pwm_.configureActiveLight(configuredTxPulseLightOn(settings_));
  if (!display_.begin()) Log::event("BOOT", "OLED unavailable; Serial UI remains fully operational");
  initialized_ = true;
  if (!receiver_.begin()) { Log::event("BOOT", "FATAL: capture peripheral init failed"); finish(false, FailReason::UNSUPPORTED); return; }
  Log::printf("BOOT", "capture initialized: %s", receiver_.highRateBackend() ? "MCPWM 80MHz" : "GPIO cycle counter");
  lastUserActivityMs_ = millis();
  setActivePerformance(false);
  printConfiguration();
  if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::OPTICS &&
      static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
  else showIdle();
}

void App::update() {
  serviceSerialConsole();
  serviceIdlePowerSave();
  const uint32_t now = millis();
  const ButtonEvent startEvent = startButton_.update(now);
  const ButtonEvent modeEvent = modeButton_.update(now);
  if (startEvent != ButtonEvent::NONE)
    Log::printf("INPUT", "START %s state=%s", buttonEventName(startEvent), appStateName(state_));
  if (modeEvent != ButtonEvent::NONE)
    Log::printf("INPUT", "MODE %s state=%s", buttonEventName(modeEvent), appStateName(state_));
  if (startEvent != ButtonEvent::NONE || modeEvent != ButtonEvent::NONE) {
    lastUserActivityMs_ = now;
    leaveIdlePowerSave();
  }
  if (!initialized_) {
    if (!startButton_.pressed() || !modeButton_.pressed()) finishInitialization(false);
    else if (now - bootCheckStartedMs_ >= FACTORY_RESET_HOLD_MS) finishInitialization(true);
    return;
  }
  if (state_ == AppState::BOARD_TEST) {
    if (startEvent == ButtonEvent::LONG) stopBoardTest();
    else {
      if (modeEvent != ButtonEvent::NONE)
        Log::event("ACTION", "MODE ignored while board test is active");
      updateBoardTest(now);
    }
    return;
  }
  if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED &&
      startEvent == ButtonEvent::LONG) { abortTest(); return; }
  if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED &&
      modeEvent != ButtonEvent::NONE) Log::event("ACTION", "MODE ignored while test is active");

  if (state_ == AppState::IDLE || state_ == AppState::FINISHED) {
    if (modeEvent == ButtonEvent::SHORT) {
      cycleRunMode(); sanitizeRange(); const bool saved = store_.save(settings_);
      params_ = store_.params(settings_);
      if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::OPTICS &&
          static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
      else showIdle();
      if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD)
        Log::printf("ACTION", "board test changed to %s, NVS=%s",
          boardTestName(static_cast<BoardTest>(settings_.boardTest)), saved ? "OK" : "FAILED");
      else
        Log::printf("ACTION", "mode changed to %s/%s, NVS=%s",
          roleName(static_cast<Role>(settings_.role)),
          testKindName(static_cast<TestKind>(settings_.testKind)), saved ? "OK" : "FAILED");
    } else if (modeEvent == ButtonEvent::LONG) {
      state_ = AppState::MENU; menuItem_ = 0; Log::event("ACTION", "settings menu entered"); showMenu();
    } else if (startEvent == ButtonEvent::SHORT) { Log::event("ACTION", "test start requested"); startTest(); }
    else if (state_ == AppState::FINISHED && static_cast<Role>(settings_.role) == Role::SLAVE &&
             now >= slaveRearmAtMs_) armSlave(pendingReason_ != FailReason::NONE);
    return;
  }
  if (state_ == AppState::SLAVE_READY && modeEvent != ButtonEvent::NONE) {
    radio_.end(); havePeer_ = false;
    if (modeEvent == ButtonEvent::SHORT) {
      settings_.role = static_cast<uint8_t>(Role::SOLO);
      settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
      const bool saved = store_.save(settings_);
      params_ = store_.params(settings_); state_ = AppState::IDLE; showIdle();
      Log::printf("ACTION", "role changed to SOLO, NVS=%s", saved ? "OK" : "FAILED");
    } else if (modeEvent == ButtonEvent::LONG) {
      state_ = AppState::MENU; menuItem_ = 0; showMenu();
    }
    return;
  }
  if (state_ == AppState::MENU) {
    if (modeEvent == ButtonEvent::SHORT) {
      leaveOpticalCalibration();
      menuItem_ = nextMenuItem(menuItem_,
        static_cast<TestGroup>(settings_.testGroup));
      Log::printf("ACTION", "menu item selected index=%u", menuItem_);
      showMenu();
    }
    else if (modeEvent == ButtonEvent::LONG) {
      leaveOpticalCalibration();
      sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_);
      Log::printf("ACTION", "settings menu saved and closed, NVS=%s", saved ? "OK" : "FAILED");
      state_ = AppState::IDLE; printConfiguration();
      if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::OPTICS &&
          static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
      else showIdle();
    } else if (menuItem_ != MENU_OPTICAL_CALIBRATION_ITEM &&
               startEvent == ButtonEvent::SHORT) changeMenu(+1);
    else if (menuItem_ != MENU_OPTICAL_CALIBRATION_ITEM &&
             (startEvent == ButtonEvent::LONG || startEvent == ButtonEvent::REPEAT)) changeMenu(-1);
    updateOpticalCalibration(now);
    return;
  }
  if (state_ == AppState::SOLO_MEASURE) {
    if (static_cast<int32_t>(now - localMeasurementDeadlineMs_) >= 0) {
      Log::event("MEASURE", "local stage watchdog expired");
      measurement_.forceFail(FailReason::LOST_EDGE);
    }
    const MeasureState ms = measurement_.update();
    if (ms == MeasureState::FAIL) {
      pwm_.stop();
      printStageStats(measurement_.stats(), actual_.actualHz);
      showStageResult(measurement_.stats());
      finish(false, measurement_.reason(), true);
    }
    else if (ms == MeasureState::PASS) {
      pwm_.stop();
      printStageStats(measurement_.stats(), actual_.actualHz);
      showStageResult(measurement_.stats());
      stagePassed();
    } else if (measurement_.takeProgressUpdate()) {
      StageStats live = {};
      if (measurement_.statsSnapshot(live)) showStageResult(live);
    }
  } else if (state_ == AppState::SOLO_DRIVER) {
    if (static_cast<int32_t>(now - localMeasurementDeadlineMs_) >= 0)
      driverTest_.forceFail(FailReason::LOST_EDGE);
    const DriverState ds = driverTest_.update();
    if (ds == DriverState::SUBSAMPLE_DONE) {
      // Capture is already stopped. Update the OLED only in this quiet gap,
      // then restart the same PWM point and arm the next tenth of the sample.
      pwm_.stop();
      driverTest_.takeProgressUpdate();
      showDriverResult(driverTest_.stats());
      ActualPwm resumed = {};
      if (!pwm_.start(requestedHz_, requestedPulseNs_, resumed)) {
        driverTest_.forceFail(FailReason::RESOLUTION);
      } else {
        actual_ = resumed;
        if (!driverTest_.resumeSubsample()) {
          pwm_.stop();
          driverTest_.forceFail(FailReason::DATA_LOSS);
        }
      }
    } else if (ds == DriverState::FAIL) {
      pwm_.stop(); receiver_.stop();
      driverTest_.printSummary();
      driverTest_.printTrace();
      showDriverResult(driverTest_.stats());
      finish(false, driverTest_.stats().reason, true);
    } else if (ds == DriverState::PASS) {
      pwm_.stop();
      driverTest_.printSummary();
      const bool finalPoint = stageIndex_ + 1U >= stageCount_;
      showDriverResult(driverTest_.stats(), finalPoint);
      stagePassed();
    }
  } else if (state_ == AppState::MASTER_DISCOVER || state_ == AppState::MASTER_WAIT_READY ||
             state_ == AppState::MASTER_WAIT_RESULT || state_ == AppState::MASTER_FINALIZE) {
    handleRadio(); updateMaster();
  } else {
    handleRadio(); updateSlave();
  }
}

void App::showIdle() {
  setActivePerformance(false);
  setStandbyOpticalOutput();
  lastUserActivityMs_ = millis();
  char one[64];
  if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD) {
    snprintf(one, sizeof(one), "%s: %s", uiTestGroupName(TestGroup::BOARD),
      uiBoardTestName(static_cast<BoardTest>(settings_.boardTest)));
    display_.show(one, UiText::BOARD_READY);
  } else {
    snprintf(one, sizeof(one), "%s: %s",
      uiRoleName(static_cast<Role>(settings_.role)),
      uiTestName(static_cast<TestKind>(settings_.testKind)));
    display_.show(one, UiText::START_RUN);
  }
}

void App::serviceSerialConsole() {
  while (Serial.available() > 0) {
    const int raw = Serial.read();
    if (raw < 0) break;
    const char c = static_cast<char>(raw);
    lastUserActivityMs_ = millis();
    leaveIdlePowerSave();

    if (c == '\r') continue;
    if (c == '\n') {
      if (serialLineOverflow_) Serial.println("ERR command too long");
      else if (serialLineLength_) {
        serialLine_[serialLineLength_] = '\0';
        handleSerialCommand(serialLine_);
      }
      serialLineLength_ = 0;
      serialLineOverflow_ = false;
      continue;
    }
    if (c < ' ' || c > '~') continue;
    if (serialLineLength_ + 1U < sizeof(serialLine_))
      serialLine_[serialLineLength_++] = c;
    else serialLineOverflow_ = true;
  }
}

void App::printSerialHelp() {
  Serial.println("COMMANDS (send with newline):");
  Serial.println("  help | status | start | stop | defaults");
  Serial.println("  set group optics|board");
  Serial.println("  set boardtest adc|pwm|rx");
  Serial.println("  set role solo|master|slave");
  Serial.println("  set test optical|driver");
  Serial.println("  set frequency 500|1000|2000|5000|10000|25000");
  Serial.println("  set max 2000|5000|10000|20000|50000|100000|200000|500000");
  Serial.println("  set min 250|500|1000|2000|5000|10000|50000");
  Serial.println("  set accuracy 1|2|5|10");
  Serial.println("  set time 100|250|500|1000|2000|5000   (ms)");
  Serial.println("  set light HH|HL|LH|LL   (DRIVER only)");
}

void App::printSerialStatus() {
  if (!initialized_) {
    Serial.println("STATUS initializing");
    return;
  }
  params_ = store_.params(settings_);
  Serial.printf("STATUS state=%s group=%s boardtest=%s role=%s test=%s frequency=%luHz max=%luns min=%luns accuracy=%.2f%% time=%lums light=%s usb=%s\n",
    appStateName(state_), testGroupName(static_cast<TestGroup>(settings_.testGroup)),
    boardTestName(static_cast<BoardTest>(settings_.boardTest)), roleName(static_cast<Role>(settings_.role)),
    testKindName(static_cast<TestKind>(settings_.testKind)), params_.frequencyHz,
    params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs,
    configuredLevelName(settings_),
    usbHostPresent() ? "connected" : "disconnected");
}

bool App::serialSettingsMutable() const {
  return initialized_ && (state_ == AppState::IDLE || state_ == AppState::FINISHED ||
    state_ == AppState::MENU || state_ == AppState::SLAVE_READY);
}

void App::finishSerialSettingsChange() {
  if (state_ == AppState::SLAVE_READY) radio_.end();
  leaveOpticalCalibration();
  state_ = AppState::IDLE;
  sanitizeRange();
  params_ = store_.params(settings_);
  pwm_.configureActiveLight(configuredTxPulseLightOn(settings_));
  const bool saved = store_.save(settings_);
  Serial.printf("OK settings saved=%s\n", saved ? "yes" : "no");
  if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::OPTICS &&
      static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
  else showIdle();
  printSerialStatus();
}

void App::handleSerialCommand(char *line) {
  lowerAscii(line);
  char *save = nullptr;
  char *command = strtok_r(line, " \t", &save);
  char *name = strtok_r(nullptr, " \t", &save);
  char *value = strtok_r(nullptr, " \t", &save);
  char *extra = strtok_r(nullptr, " \t", &save);
  if (!command) return;

  if ((!strcmp(command, "help") || !strcmp(command, "?")) && !name) {
    printSerialHelp();
    return;
  }
  if ((!strcmp(command, "status") || !strcmp(command, "get")) && !name) {
    printSerialStatus();
    return;
  }
  if (!strcmp(command, "start") && !name) {
    if (!initialized_) Serial.println("ERR still initializing");
    else if (state_ == AppState::IDLE || state_ == AppState::FINISHED) {
      Serial.println("OK test start requested");
      startTest();
    } else if (state_ == AppState::SLAVE_READY) Serial.println("OK slave already armed");
    else Serial.printf("ERR busy state=%s\n", appStateName(state_));
    return;
  }
  if ((!strcmp(command, "stop") || !strcmp(command, "abort")) && !name) {
    if (!initialized_) Serial.println("ERR still initializing");
    else if (state_ == AppState::IDLE || state_ == AppState::FINISHED) Serial.println("OK already stopped");
    else if (state_ == AppState::BOARD_TEST) {
      Serial.println("OK board test stopped");
      stopBoardTest();
    } else if (state_ == AppState::MENU) {
      leaveOpticalCalibration();
      state_ = AppState::IDLE; showIdle(); Serial.println("OK menu closed");
    } else if (state_ == AppState::SLAVE_READY) Serial.println("OK slave is armed; no test is running");
    else {
      Serial.println("OK abort requested");
      abortTest();
    }
    return;
  }
  if (!strcmp(command, "defaults") && !name) {
    if (!serialSettingsMutable()) {
      Serial.printf("ERR settings locked state=%s\n", appStateName(state_));
      return;
    }
    store_.defaults(settings_);
    finishSerialSettingsChange();
    return;
  }
  if (strcmp(command, "set") || !name || !value || extra) {
    Serial.println("ERR unknown command; send 'help'");
    return;
  }
  if (!serialSettingsMutable()) {
    Serial.printf("ERR settings locked state=%s; stop the test first\n", appStateName(state_));
    return;
  }

  bool accepted = false;
  uint32_t numeric = 0;
  if (!strcmp(name, "group")) {
    if (!strcmp(value, "optics") || !strcmp(value, "optical")) {
      settings_.testGroup = static_cast<uint8_t>(TestGroup::OPTICS); accepted = true;
    } else if (!strcmp(value, "board")) {
      settings_.testGroup = static_cast<uint8_t>(TestGroup::BOARD); accepted = true;
    }
  } else if (!strcmp(name, "boardtest") || !strcmp(name, "board")) {
    BoardTest selected = static_cast<BoardTest>(UINT8_MAX);
    if (!strcmp(value, "adc")) selected = BoardTest::ADC;
    else if (!strcmp(value, "pwm")) selected = BoardTest::PWM_OUTPUT;
    else if (!strcmp(value, "rx")) selected = BoardTest::RX_INPUT;
    if (boardTestAvailable(selected)) {
      settings_.boardTest = static_cast<uint8_t>(selected);
      accepted = true;
    }
  } else if (!strcmp(name, "role")) {
    if (!strcmp(value, "solo")) { settings_.role = static_cast<uint8_t>(Role::SOLO); accepted = true; }
    else if (!strcmp(value, "master")) { settings_.role = static_cast<uint8_t>(Role::MASTER); accepted = true; }
    else if (!strcmp(value, "slave")) { settings_.role = static_cast<uint8_t>(Role::SLAVE); accepted = true; }
  } else if (!strcmp(name, "test")) {
    if (!strcmp(value, "optical")) { settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL); accepted = true; }
    else if (!strcmp(value, "driver") && !TARGET_IS_C3 &&
             static_cast<Role>(settings_.role) == Role::SOLO) {
      settings_.testKind = static_cast<uint8_t>(TestKind::DRIVER); accepted = true;
    }
  } else if ((!strcmp(name, "frequency") || !strcmp(name, "freq")) && parseUnsigned(value, numeric)) {
    const int index = optionIndex(PWM_FREQUENCY_OPTIONS_HZ, numeric);
    if (index >= 0) { settings_.frequencyIndex = static_cast<uint8_t>(index); accepted = true; }
  } else if ((!strcmp(name, "max") || !strcmp(name, "maxpulse")) && parseUnsigned(value, numeric)) {
    const int index = optionIndex(MAX_PULSE_OPTIONS_NS, numeric);
    if (index >= 0) { settings_.maxPulseIndex = static_cast<uint8_t>(index); accepted = true; }
  } else if ((!strcmp(name, "min") || !strcmp(name, "minpulse")) && parseUnsigned(value, numeric)) {
    const int index = optionIndex(MIN_PULSE_OPTIONS_NS, numeric);
    if (index >= 0) { settings_.minPulseIndex = static_cast<uint8_t>(index); accepted = true; }
  } else if (!strcmp(name, "accuracy")) {
    const int index = accuracyOptionIndex(value);
    if (index >= 0) { settings_.accuracyIndex = static_cast<uint8_t>(index); accepted = true; }
  } else if ((!strcmp(name, "time") || !strcmp(name, "duration")) && parseUnsigned(value, numeric)) {
    const int index = optionIndex(TEST_TIME_OPTIONS_MS, numeric);
    if (index >= 0) { settings_.timeIndex = static_cast<uint8_t>(index); accepted = true; }
  } else if (!strcmp(name, "light")) {
    if (static_cast<TestKind>(settings_.testKind) != TestKind::DRIVER) {
      Serial.println("ERR level setting is available only in DRIVER test");
      return;
    }
    if (!strcmp(value, "hh")) { settings_.lightCode = static_cast<uint8_t>(LightCode::HH); accepted = true; }
    else if (!strcmp(value, "hl")) { settings_.lightCode = static_cast<uint8_t>(LightCode::HL); accepted = true; }
    else if (!strcmp(value, "lh")) { settings_.lightCode = static_cast<uint8_t>(LightCode::LH); accepted = true; }
    else if (!strcmp(value, "ll")) { settings_.lightCode = static_cast<uint8_t>(LightCode::LL); accepted = true; }
  }

  if (!accepted) {
    Serial.println("ERR invalid setting or value; send 'help'");
    return;
  }
  finishSerialSettingsChange();
}

void App::cycleRunMode() {
  if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD) {
    do {
      settings_.boardTest = (settings_.boardTest + 1U) %
        (static_cast<uint8_t>(BoardTest::RX_INPUT) + 1U);
    } while (!boardTestAvailable(static_cast<BoardTest>(settings_.boardTest)));
    return;
  }
  const Role role = static_cast<Role>(settings_.role);
  const TestKind kind = static_cast<TestKind>(settings_.testKind);
  if (role == Role::SOLO && kind == TestKind::OPTICAL && !TARGET_IS_C3) {
    settings_.testKind = static_cast<uint8_t>(TestKind::DRIVER);
  } else if (role == Role::SOLO) {
    settings_.role = static_cast<uint8_t>(Role::MASTER);
    settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
  } else if (role == Role::MASTER) {
    settings_.role = static_cast<uint8_t>(Role::SLAVE);
    settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
  } else {
    settings_.role = static_cast<uint8_t>(Role::SOLO);
    settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
  }
}

void App::sanitizeRange() {
  if (settings_.testGroup > static_cast<uint8_t>(TestGroup::BOARD))
    settings_.testGroup = static_cast<uint8_t>(TestGroup::OPTICS);
  if (settings_.boardTest > static_cast<uint8_t>(BoardTest::RX_INPUT) ||
      !boardTestAvailable(static_cast<BoardTest>(settings_.boardTest)))
    settings_.boardTest = static_cast<uint8_t>(defaultBoardTest());
  if (settings_.role > static_cast<uint8_t>(Role::SLAVE))
    settings_.role = static_cast<uint8_t>(Role::SOLO);
  if (settings_.testKind > static_cast<uint8_t>(TestKind::DRIVER))
    settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
  if (settings_.lightCode > static_cast<uint8_t>(LightCode::LL))
    settings_.lightCode = static_cast<uint8_t>(LightCode::HH);
  if (settings_.role != static_cast<uint8_t>(Role::SOLO) ||
      (TARGET_IS_C3 && settings_.testKind == static_cast<uint8_t>(TestKind::DRIVER)))
    settings_.testKind = static_cast<uint8_t>(TestKind::OPTICAL);
  settings_.frequencyIndex %= countOf(PWM_FREQUENCY_OPTIONS_HZ);
  settings_.maxPulseIndex %= countOf(MAX_PULSE_OPTIONS_NS);
  settings_.minPulseIndex %= countOf(MIN_PULSE_OPTIONS_NS);
  settings_.accuracyIndex %= countOf(ACCURACY_OPTIONS_PCT);
  settings_.timeIndex %= countOf(TEST_TIME_OPTIONS_MS);
  const uint32_t hz = PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex];
  const uint8_t lastValid = lastValidMaxPulseIndex(hz);
  if (settings_.maxPulseIndex > lastValid) settings_.maxPulseIndex = lastValid;
  const uint8_t lastMin = lastMinPulseIndexAtMost(MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
  if (settings_.minPulseIndex > lastMin) settings_.minPulseIndex = lastMin;
  if (settings_.testKind == static_cast<uint8_t>(TestKind::DRIVER)) {
    const uint8_t driverLastMin = lastMinPulseIndexAtMost(
      MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
    const uint8_t firstDriverMin = firstMinPulseIndexAtLeast(
      DRIVER_MIN_INPUT_PULSE_NS, driverLastMin);
    if (settings_.minPulseIndex < firstDriverMin)
      settings_.minPulseIndex = firstDriverMin;
  }
}

void App::changeMenu(int d) {
  sanitizeRange();
  if (menuItem_ == 0) {
    settings_.testGroup = cycleIndex(settings_.testGroup, 0,
      static_cast<uint8_t>(TestGroup::BOARD), d);
  } else if (menuItem_ == 2) {
    const uint8_t last = lastValidMaxPulseIndex(
      PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex]);
    const uint8_t first = firstMaxPulseIndexAtLeast(
      MIN_PULSE_OPTIONS_NS[settings_.minPulseIndex], last);
    settings_.maxPulseIndex = cycleIndex(settings_.maxPulseIndex,
      first, last, d);
  } else if (menuItem_ == 3) {
    const uint8_t last = lastMinPulseIndexAtMost(
      MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
    const uint8_t first = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER
      ? firstMinPulseIndexAtLeast(DRIVER_MIN_INPUT_PULSE_NS, last) : 0U;
    settings_.minPulseIndex = cycleIndex(settings_.minPulseIndex, first, last, d);
  } else {
    uint8_t *value = nullptr; size_t count = 0;
    switch (menuItem_) {
      case 1: value = &settings_.frequencyIndex; count = countOf(PWM_FREQUENCY_OPTIONS_HZ); break;
      case 4: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break;
      case 5: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
      case 6:
        if (static_cast<TestKind>(settings_.testKind) != TestKind::DRIVER) {
          showMenu();
          return;
        }
        value = &settings_.lightCode; count = 4; break;
      default: return;
    }
    *value = cycleIndex(*value, 0, static_cast<uint8_t>(count - 1U), d);
  }
  sanitizeRange(); params_ = store_.params(settings_);
  pwm_.configureActiveLight(configuredTxPulseLightOn(settings_));
  Log::printf("ACTION", "menu item=%u changed direction=%+d frequency=%u max-pulse=%u min-pulse=%u accuracy=%u time=%u light=%s",
    menuItem_, d, settings_.frequencyIndex, settings_.maxPulseIndex,
    settings_.minPulseIndex, settings_.accuracyIndex, settings_.timeIndex,
    configuredLevelName(settings_));
  showMenu();
}

void App::showMenu() {
  char one[64], value[24], total[64], all[12];
  const char *label = nullptr;
  Display::formatDuration(actualNominalTotalUs(), all, sizeof(all));
  switch (menuItem_) {
    case 0:
      snprintf(value, sizeof(value), "%s",
        uiTestGroupName(static_cast<TestGroup>(settings_.testGroup)));
      label = UiText::MENU_TEST_GROUP;
      break;
    case 1:
      Display::formatPwmFrequency(params_.frequencyHz, value, sizeof(value));
      label = UiText::MENU_FREQUENCY;
      break;
    case 2:
      Display::formatPulse(params_.maxPulseNs, value, sizeof(value));
      label = UiText::MENU_MAX_PULSE;
      break;
    case 3:
      Display::formatPulse(params_.minPulseNs, value, sizeof(value));
      label = UiText::MENU_MIN_PULSE;
      break;
    case 4:
      snprintf(value, sizeof(value), "+/-%g%%", params_.accuracyPct);
      label = UiText::MENU_ACCURACY;
      break;
    case 5:
      snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f);
      label = UiText::MENU_TEST_TIME;
      break;
    case 6: {
      if (static_cast<TestKind>(settings_.testKind) != TestKind::DRIVER) {
        snprintf(value, sizeof(value), "%s", UiText::LIGHT_AUTO);
        label = UiText::MENU_LIGHT_CODE;
        formatMenuLine(label, value, one, sizeof(one));
        display_.show(one, UiText::LIGHT_AUTO_FORMAT);
        return;
      }
      const char *code = lightCodeName(static_cast<LightCode>(settings_.lightCode));
      snprintf(value, sizeof(value), "%s", code);
      label = UiText::MENU_LIGHT_CODE;
      formatMenuLine(label, value, one, sizeof(one));
      snprintf(total, sizeof(total), UiText::LIGHT_CODE_FORMAT, code[0], code[1]);
      display_.show(one, total);
      return;
    }
    case MENU_OPTICAL_CALIBRATION_ITEM:
      enterOpticalCalibration(millis());
      return;
    default: return;
  }
  formatMenuLine(label, value, one, sizeof(one));
  if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD)
    snprintf(total, sizeof(total), "%s", UiText::BOARD_READY);
  else
    formatMenuLine(UiText::MENU_TOTAL_TIME, all, total, sizeof(total));
  display_.show(one, total);
}

void App::enterOpticalCalibration(uint32_t now) {
  if (opticalCalibrationActive_) return;
  optical_current_begin();
  pwm_.lightOn();
  opticalCalibrationActive_ = true;
  opticalCalibrationStartedMs_ = now;
  opticalCalibrationUpdatedMs_ = 0;
  display_.show("Idiode: --.- mA", "Vcc: --.-- V");
  Log::printf("CALIB", "optical LED ON; sense GPIO=%u, VCC GPIO=%u, settling %lums",
    GPIO_OPTICAL_CURRENT, GPIO_OPTICAL_VCC, OPTICAL_CURRENT_SETTLE_MS);
}

void App::updateOpticalCalibration(uint32_t now, bool force) {
  if (!opticalCalibrationActive_ ||
      now - opticalCalibrationStartedMs_ < OPTICAL_CURRENT_SETTLE_MS) return;
  (void)force;
  OpticalCurrentMeasurement reading = {};
  if (!optical_current_poll(reading)) return;
  opticalCalibrationUpdatedMs_ = now;
  char current[32], vcc[24];
  snprintf(current, sizeof(current), "Idiode: %.1f mA", reading.currentMa);
  snprintf(vcc, sizeof(vcc), "Vcc: %.2f V", reading.vccVoltage);
  display_.show(current, vcc);
  Log::printf("CALIB",
    "raw sense=%u vcc=%u | ADC sense=%.3fV vcc=%.3fV | Usense=%.3fV Vcc=%.3fV | Idiode=%.1fmA",
    reading.senseRaw, reading.vccRaw, reading.senseAdcVoltage,
    reading.vccAdcVoltage, reading.senseVoltage, reading.vccVoltage,
    reading.currentMa);
}

void App::leaveOpticalCalibration() {
  if (!opticalCalibrationActive_) return;
  pwm_.stop();
  opticalCalibrationActive_ = false;
  Log::event("CALIB", "optical LED OFF; calibration menu left");
}

void App::startTest() {
  leaveIdlePowerSave();
  pwm_.stop();
  setActivePerformance(true);
  sanitizeRange();
  if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD) {
    startBoardTest();
    return;
  }
  params_ = store_.params(settings_); stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
  stageIndex_ = 0; requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; pendingReason_ = FailReason::NONE;
  havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0;
  if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; }
  Log::printf("TEST", "starting role=%s test=%s light=%s stages=%lu",
    roleName(static_cast<Role>(settings_.role)),
    testKindName(static_cast<TestKind>(settings_.testKind)),
    configuredLevelName(settings_), stageCount_);
  if (SERIAL_MINIMAL_LOG) {
    Log::printf("CONFIG", "mode=%s/%s frequency=%luHz pulse=%lu..%luns accuracy=%.2f%% time=%lums LIGHT=%s stages=%lu",
      roleName(static_cast<Role>(settings_.role)),
      testKindName(static_cast<TestKind>(settings_.testKind)), params_.frequencyHz,
      params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs,
      configuredLevelName(settings_),
      stageCount_);
  }
  printConfiguration();
  const Role role = static_cast<Role>(settings_.role);
  if (role == Role::SOLO) {
    if (!prepareStage()) return;
    state_ = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER
      ? AppState::SOLO_DRIVER : AppState::SOLO_MEASURE;
  } else if (!radio_.begin()) finish(false, FailReason::LINK_LOST);
  else if (role == Role::MASTER) startMasterDiscovery();
  else { state_ = AppState::SLAVE_READY; Log::event("TEST", "Slave armed and waiting for Master"); display_.show(UiText::SLAVE_READY, UiText::WAIT_MASTER); }
}

void App::startBoardTest() {
  const BoardTest test = static_cast<BoardTest>(settings_.boardTest);
  state_ = AppState::BOARD_TEST;
  boardTestUpdatedMs_ = boardDisplayUpdatedMs_ = 0;
  boardPwmLightOn_ = false;
  rxPinStateKnown_ = false;
  Log::printf("BOARD", "starting test=%s", boardTestName(test));

  // ADC and RX checks need the optical transmitter continuously illuminated.
  // The PWM-output check drives the same pin itself and therefore replaces
  // the constant active level with its test waveform.
  if (test != BoardTest::PWM_OUTPUT) {
    pwm_.lightOn();
    Log::printf("BOARD", "optical output active GPIO=%u level=%s", GPIO_PWM,
      TX_LIGHT_ON_GPIO_LEVEL == HIGH ? "HIGH" : "LOW");
  }

  if (test == BoardTest::ADC) {
    if (!BOARD_ADC_AVAILABLE) {
      Serial.println("BOARD ADC: unavailable in MAKETKA profile");
      display_.show(UiText::ADC_UNAVAILABLE, UiText::BOARD_STOP);
      return;
    }
    pinMode(GPIO_ANALOG_RX, INPUT);
    pinMode(GPIO_VBAT, INPUT);
    display_.show(UiText::BOARD_TEST_NAMES[static_cast<uint8_t>(BoardTest::ADC)],
      UiText::BOARD_STOP);
  } else if (test == BoardTest::PWM_OUTPUT) {
    boardPwmLightOn_ = true;
    boardTestUpdatedMs_ = millis();
    pwm_.lightOn();
    Serial.printf("BOARD PWM: GPIO=%u frequency=%luHz duty=50%% light=ON\n",
      GPIO_PWM, BOARD_PWM_TEST_FREQUENCY_HZ);
    display_.show(UiText::BOARD_TEST_NAMES[static_cast<uint8_t>(test)], UiText::BOARD_STOP);
  } else {
    pinMode(GPIO_RX, INPUT);
    display_.show(UiText::BOARD_TEST_NAMES[static_cast<uint8_t>(test)], UiText::BOARD_STOP);
  }
}

void App::updateBoardTest(uint32_t now) {
  const BoardTest test = static_cast<BoardTest>(settings_.boardTest);
  if (test == BoardTest::ADC) {
    if (!BOARD_ADC_AVAILABLE || now - boardTestUpdatedMs_ < BOARD_ADC_PRINT_INTERVAL_MS) return;
    boardTestUpdatedMs_ = now;
    const uint16_t analogRx = analogRead(GPIO_ANALOG_RX);
    const uint16_t vbat = analogRead(GPIO_VBAT);
    Serial.printf("ADC analog_rx=%u vbat=%u\n", analogRx, vbat);
    if (now - boardDisplayUpdatedMs_ >= BOARD_TEST_DISPLAY_INTERVAL_MS) {
      boardDisplayUpdatedMs_ = now;
      char one[32], two[32];
      snprintf(one, sizeof(one), "ADC RX: %u", analogRx);
      snprintf(two, sizeof(two), "ADC VBAT: %u", vbat);
      display_.show(one, two);
    }
    return;
  }

  if (test == BoardTest::PWM_OUTPUT) {
    if (now - boardTestUpdatedMs_ < BOARD_PWM_TEST_HALF_PERIOD_MS) return;
    boardTestUpdatedMs_ = now;
    boardPwmLightOn_ = !boardPwmLightOn_;
    if (boardPwmLightOn_) pwm_.lightOn();
    else pwm_.stop();
    Serial.printf("BOARD PWM: light=%s\n", boardPwmLightOn_ ? "ON" : "OFF");
    char two[32];
    snprintf(two, sizeof(two), "%luHz 50%%: %s", BOARD_PWM_TEST_FREQUENCY_HZ,
      boardPwmLightOn_ ? "ON" : "OFF");
    display_.show(UiText::BOARD_TEST_NAMES[static_cast<uint8_t>(test)], two);
    return;
  }

  const bool level = digitalRead(GPIO_RX) == HIGH;
  if (!rxPinStateKnown_ || level != rxPinState_) {
    rxPinState_ = level;
    rxPinStateKnown_ = true;
    Serial.printf("RX GPIO=%u state=%s (%u)\n", GPIO_RX,
      level ? "HIGH" : "LOW", level ? 1U : 0U);
    char two[32];
    snprintf(two, sizeof(two), "GPIO %u: %s", GPIO_RX, level ? "HIGH" : "LOW");
    display_.show(UiText::BOARD_TEST_NAMES[static_cast<uint8_t>(test)], two);
  }
}

void App::stopBoardTest() {
  Log::printf("BOARD", "stopping test=%s", boardTestName(static_cast<BoardTest>(settings_.boardTest)));
  pwm_.stop();
  state_ = AppState::IDLE;
  setActivePerformance(false);
  showIdle();
}

bool App::armSlave(bool preserveDisplay) {
  setActivePerformance(false);
  pwm_.stop();
  lastUserActivityMs_ = millis();
  params_ = store_.params(settings_);
  stageIndex_ = 0; stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
  requestedHz_ = params_.frequencyHz; requestedPulseNs_ = 0; session_ = 0; sequence_ = 0; havePeer_ = false;
  lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; retries_ = 0; slaveRearmAtMs_ = 0;
  if (!radio_.begin()) {
    state_ = AppState::FINISHED; pendingReason_ = FailReason::LINK_LOST;
    slaveRearmAtMs_ = millis() + LINK_HEARTBEAT_TIMEOUT_MS;
    display_.show(UiText::LINK_FAILED, UiText::RADIO_ERROR,
      0, 0, roleCorner(Role::SLAVE));
    return false;
  }
  radio_.setWindowedReceive(true);
  radio_.flush(); state_ = AppState::SLAVE_READY;
  Log::event("TEST", "Slave automatically armed and waiting for Master");
  if (!preserveDisplay) display_.show(UiText::SLAVE_READY, UiText::WAIT_MASTER);
  return true;
}

bool App::prepareStage(bool showProgress) {
  requestedHz_ = params_.frequencyHz;
  requestedPulseNs_ = pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, stageIndex_);
  actual_ = {};
  const uint32_t maxHz = TARGET_IS_C3 ? C3_STRICT_MAX_HZ :
    (receiver_.highRateBackend() ? S3_STRICT_MAX_HZ : C3_STRICT_MAX_HZ);
  if (requestedHz_ > maxHz) { finish(false, FailReason::UNSUPPORTED); return false; }
  Log::printf("PWM", "starting GPIO=%u requested=%luHz pulse=%luns", GPIO_PWM, requestedHz_, requestedPulseNs_);
  if (!pwm_.start(requestedHz_, requestedPulseNs_, actual_)) {
    Log::printf("PWM", "START FAILED GPIO=%u requested=%luHz pulse=%luns; PWM setup failed",
      GPIO_PWM, requestedHz_, requestedPulseNs_);
    finish(false, FailReason::RESOLUTION); return false;
  }
  if (!periodWithin(actual_.actualHz, requestedHz_, params_.accuracyPct) ||
      !periodWithin(actual_.actualPulseNs, requestedPulseNs_, params_.accuracyPct)) {
    Log::printf("PWM", "requested point cannot be generated within tolerance: requested=%luHz/%luns actual=%luHz/%luns tolerance=%.2f%%",
      requestedHz_, requestedPulseNs_, actual_.actualHz, actual_.actualPulseNs,
      params_.accuracyPct);
    finish(false, FailReason::RESOLUTION); return false;
  }
  const bool driverMode = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER;
  if (!driverMode) {
    const uint32_t plannedRxHz = receiver_.plannedTickHz(actual_.actualHz, actual_.actualDutyPct);
    const uint32_t plannedPulseRxHz = receiver_.plannedPulseTickHz(
      actual_.actualHz, actual_.actualDutyPct);
    const FailReason resolution = validateResolution(actual_.actualHz, actual_.actualDutyPct,
      params_.accuracyPct, plannedRxHz, plannedPulseRxHz, actual_.bits,
      MEASUREMENT_AVERAGING_PERIODS);
    if (resolution != FailReason::NONE) {
      Log::printf("PWM", "resolution rejected: actual=%luHz duty=%.3f%% bits=%u period-capture=%luHz pulse-capture=%luHz tolerance=%.3f%%",
        actual_.actualHz, actual_.actualDutyPct, actual_.bits, plannedRxHz,
        plannedPulseRxHz, effectiveTolerancePct(params_.accuracyPct));
      finish(false, resolution); return false;
    }
  } else if (!receiver_.highRateBackend()) {
    finish(false, FailReason::UNSUPPORTED); return false;
  }
  Log::printf("PWM", "stage=%lu/%lu requested=%luHz/%luns actual=%luHz/%luns duty=%.3f%% bits=%u STARTED",
    stageIndex_ + 1, stageCount_, requestedHz_, requestedPulseNs_, actual_.actualHz,
    actual_.actualPulseNs, actual_.actualDutyPct, actual_.bits);
  if (showProgress) showStageProgress();
  if (static_cast<Role>(settings_.role) == Role::SOLO) {
    const bool started = driverMode ? startDriverMeasurement() :
      startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct);
    if (!started) { finish(false, FailReason::UNSUPPORTED); return false; }
  }
  return true;
}

bool App::startLocalMeasurement(float hz, float duty) {
  Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% period-capture=%luHz pulse-capture=%luHz settle=%u cycles window=%lums; every pulse validated",
    hz, duty, effectiveTolerancePct(params_.accuracyPct), receiver_.plannedTickHz(static_cast<uint32_t>(hz + 0.5f), duty),
    receiver_.plannedPulseTickHz(static_cast<uint32_t>(hz + 0.5f), duty),
    PWM_SETTLE_CYCLES, params_.testTimeMs);
  const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs,
    MEASUREMENT_AVERAGING_PERIODS, PWM_SETTLE_CYCLES, true);
  const uint32_t nominalMs = stageWallTimeMs(params_.testTimeMs,
    static_cast<uint32_t>(hz + 0.5f));
  const uint64_t watchdogMs = static_cast<uint64_t>(nominalMs) * 2ULL + 2000ULL;
  localMeasurementDeadlineMs_ = millis() + static_cast<uint32_t>(
    watchdogMs > UINT32_MAX ? UINT32_MAX : watchdogMs);
  Log::printf("MEASURE", "receiver start %s, continuous edge capture", ok ? "OK" : "FAILED");
  return ok;
}

bool App::startDriverMeasurement() {
  const bool ok = driverTest_.start(actual_.actualHz, actual_.actualPulseNs,
    params_.accuracyPct, params_.testTimeMs, PWM_SETTLE_CYCLES,
    txActiveLightOn(settings_), rxActiveLightOn(settings_));
  const uint32_t nominalMs = stageWallTimeMs(params_.testTimeMs, actual_.actualHz);
  const uint64_t watchdogMs = static_cast<uint64_t>(nominalMs) * 2ULL + 2000ULL;
  localMeasurementDeadlineMs_ = millis() + static_cast<uint32_t>(
    watchdogMs > UINT32_MAX ? UINT32_MAX : watchdogMs);
  if (!ok) Log::event("DRIVER", "response test start FAILED");
  return ok;
}

void App::stagePassed() {
  Log::printf("TEST", "stage %lu/%lu PASS; PWM stopping", stageIndex_ + 1, stageCount_);
  pwm_.stop();
  // With PWM already quiet it is safe to stop capture before clearing its
  // queue for the next pulse width. Never reset a FreeRTOS queue concurrently
  // with the capture ISR.
  if (static_cast<Role>(settings_.role) == Role::SOLO) receiver_.stop();
  if (++stageIndex_ >= stageCount_) {
    const bool preserveDriverMeasurements =
      static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER;
    finish(true, FailReason::NONE, preserveDriverMeasurements);
    return;
  }
  if (static_cast<Role>(settings_.role) == Role::SOLO) {
    if (prepareStage()) state_ = static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER
      ? AppState::SOLO_DRIVER : AppState::SOLO_MEASURE;
  }
  else if (static_cast<Role>(settings_.role) == Role::MASTER) {
    requestedHz_ = params_.frequencyHz;
    requestedPulseNs_ = pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, stageIndex_);
    actual_ = {};
    stageStartConfirmed_ = false;
    pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE);
    state_ = AppState::MASTER_WAIT_READY; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS;
  }
}

void App::startMasterDiscovery() {
  session_ = esp_random(); if (!session_) session_ = 1;
  sequence_ = 1; stageIndex_ = 0; requestedHz_ = params_.frequencyHz;
  requestedPulseNs_ = 0; havePeer_ = false; radio_.flush();
  opticalWakeActive_ = true;
  lastOpticalWakeToggleMs_ = millis();
  pwm_.lightOn();
  pendingPacket_ = makePacket(MessageType::DISCOVER); radio_.sendBroadcast(pendingPacket_);
  lastSendMs_ = millis(); retries_ = 0;
  state_ = AppState::MASTER_DISCOVER; Log::printf("ESP-NOW", "discovery started session=%08lX", session_);
  display_.show(UiText::MASTER_SEARCH, UiText::HOLD_START_STOP);
}

ProtocolPacket App::makePacket(MessageType type) const {
  ProtocolPacket p = {};
  p.type = static_cast<uint8_t>(type); p.session = session_; p.stage = stageIndex_;
  p.stageCount = static_cast<uint16_t>(stageCount_); p.sequence = sequence_;
  p.requestedHz = requestedHz_; p.requestedPulseNs = requestedPulseNs_;
  p.actualHz = actual_.actualHz; p.actualPulseNs = actual_.actualPulseNs;
  p.testTimeMs = params_.testTimeMs;
  p.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f);
  p.lightCode = settings_.lightCode;
  return p;
}

void App::sendCurrent(MessageType type) {
  ++sequence_; pendingPacket_ = makePacket(type);
  const bool ok = sendLinked(pendingPacket_); lastSendMs_ = millis();
  if (!ok) Log::printf("ESP-NOW", "sendCurrent %s FAILED", messageName(type));
}

bool App::sendLinked(ProtocolPacket packet) {
  return havePeer_ ? radio_.sendTo(peer_, packet) : radio_.sendBroadcast(packet);
}

void App::updateHeartbeat() {
  if (!havePeer_ || state_ == AppState::FINISHED) return;
  const uint32_t now = millis();
  const uint32_t radioRxMs = radio_.lastReceiveMs();
  if (radioRxMs && now - radioRxMs < now - lastPeerSeenMs_) lastPeerSeenMs_ = radioRxMs;
  if (now - lastPeerSeenMs_ >= LINK_HEARTBEAT_TIMEOUT_MS) {
    Log::event("ESP-NOW", "peer heartbeat timeout");
    finish(false, FailReason::LINK_LOST);
    return;
  }
  if (static_cast<Role>(settings_.role) == Role::MASTER &&
      now - lastHeartbeatMs_ >= LINK_HEARTBEAT_INTERVAL_MS) {
    ProtocolPacket heartbeat = makePacket(MessageType::HEARTBEAT);
    heartbeat.sequence = sequence_;
    sendLinked(heartbeat);
    lastHeartbeatMs_ = now;
  }
}

bool App::packetForCurrent(const ProtocolPacket &p) const {
  return p.session == session_ && p.stage == stageIndex_;
}

void App::handleRadio() {
  ReceivedPacket r;
  while (radio_.receive(r)) {
    const MessageType type = static_cast<MessageType>(r.packet.type);
    if (type != MessageType::HEARTBEAT && type != MessageType::HEARTBEAT_ACK && type != MessageType::PROGRESS &&
        state_ != AppState::SLAVE_MEASURE)
      Log::printf("ESP-NOW", "RX %s session=%08lX stage=%u seq=%u",
        messageName(type), r.packet.session, r.packet.stage, r.packet.sequence);
    if ((state_ == AppState::SLAVE_READY || state_ == AppState::SLAVE_WAIT_START) &&
        type == MessageType::DISCOVER && (!havePeer_ || !memcmp(peer_, r.mac, 6))) {
      leaveIdlePowerSave();
      pwm_.stop();
      setActivePerformance(true);
      radio_.setWindowedReceive(false);
      memcpy(peer_, r.mac, 6); havePeer_ = true; session_ = r.packet.session; stageIndex_ = 0; sequence_ = r.packet.sequence;
      lastPeerSeenMs_ = millis();
      ProtocolPacket ack = makePacket(MessageType::DISCOVER_ACK); ack.sequence = r.packet.sequence; sendLinked(ack);
      state_ = AppState::SLAVE_WAIT_START; display_.show(UiText::MASTER_SEEN, UiText::ACK_SENT); continue;
    }
    if (state_ == AppState::MASTER_DISCOVER && type == MessageType::DISCOVER_ACK && r.packet.session == session_) {
      opticalWakeActive_ = false;
      pwm_.stop();
      memcpy(peer_, r.mac, 6); havePeer_ = true; lastPeerSeenMs_ = lastHeartbeatMs_ = millis();
      requestedHz_ = params_.frequencyHz;
      requestedPulseNs_ = pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, stageIndex_);
      sendCurrent(MessageType::PREPARE); state_ = AppState::MASTER_WAIT_READY; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS;
      char mac[20]; Radio::macText(peer_, mac, sizeof(mac)); Log::printf("ESP-NOW", "Slave selected %s", mac); continue;
    }
    if (havePeer_ && !memcmp(peer_, r.mac, 6) && r.packet.session == session_) lastPeerSeenMs_ = millis();
    if (havePeer_ && !memcmp(peer_, r.mac, 6) && r.packet.session == session_ &&
        type == MessageType::HEARTBEAT) {
      continue; // Radio's priority heartbeat task has already sent the ACK.
    }
    if (type == MessageType::HEARTBEAT_ACK) continue;
    if (havePeer_ && !memcmp(peer_, r.mac, 6) && type == MessageType::RESULT &&
        r.packet.session == session_ && r.packet.stage < stageIndex_) {
      ProtocolPacket ack = {}; ack.type = static_cast<uint8_t>(MessageType::ACK);
      ack.session = session_; ack.stage = r.packet.stage; ack.sequence = r.packet.sequence;
      sendLinked(ack); continue; // idempotent ACK for a retried old result
    }
    const bool matchingPeer = havePeer_ && !memcmp(peer_, r.mac, 6) && r.packet.session == session_;
    if (matchingPeer && type == MessageType::PREPARE) {
      const bool expected = state_ == AppState::SLAVE_WAIT_START && r.packet.stage == stageIndex_;
      const bool implicitAck = state_ == AppState::SLAVE_WAIT_ACK && pendingPacket_.passed &&
        r.packet.stage == static_cast<uint16_t>(pendingPacket_.stage + 1U);
      if (expected || implicitAck) {
        stageIndex_ = r.packet.stage;
        sequence_ = r.packet.sequence;
        state_ = AppState::SLAVE_WAIT_START;
        params_.testTimeMs = r.packet.testTimeMs;
        params_.accuracyPct = r.packet.accuracyX100 / 100.0f;
        if (r.packet.lightCode <= static_cast<uint8_t>(LightCode::LL))
          settings_.lightCode = r.packet.lightCode;
        requestedHz_ = r.packet.requestedHz; requestedPulseNs_ = r.packet.requestedPulseNs;
        stageCount_ = r.packet.stageCount;
        actual_ = {};
        ProtocolPacket ready = makePacket(MessageType::READY);
        ready.sequence = r.packet.sequence; sendLinked(ready);
      }
      continue;
    }
    if (!havePeer_ || memcmp(peer_, r.mac, 6) || !packetForCurrent(r.packet)) continue;
    if (type == MessageType::ABORT) {
      const FailReason reason = r.packet.reason > static_cast<uint8_t>(FailReason::NONE) &&
        r.packet.reason <= static_cast<uint8_t>(FailReason::ABORTED)
        ? static_cast<FailReason>(r.packet.reason) : FailReason::ABORTED;
      if (r.packet.requestedHz) requestedHz_ = r.packet.requestedHz;
      if (r.packet.requestedPulseNs) requestedPulseNs_ = r.packet.requestedPulseNs;
      actual_.actualHz = r.packet.actualHz ? r.packet.actualHz : requestedHz_;
      actual_.actualPulseNs = r.packet.actualPulseNs ? r.packet.actualPulseNs : requestedPulseNs_;
      actual_.actualDutyPct = dutyFromPulse(actual_.actualHz, actual_.actualPulseNs);
      measurement_.abort(); finish(false, reason); continue;
    }
    if (state_ == AppState::MASTER_WAIT_READY && type == MessageType::READY) {
      if (!prepareStage(false)) continue;
      sendCurrent(MessageType::START_STAGE); state_ = AppState::MASTER_WAIT_RESULT;
      stageStartConfirmed_ = false; retries_ = 0;
      deadlineMs_ = millis() + LINK_RETRY_INTERVAL_MS;
    } else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::READY &&
               r.packet.sequence == pendingPacket_.sequence) {
      if (!stageStartConfirmed_) showStageProgress();
      stageStartConfirmed_ = true;
      deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) +
        LINK_REPLY_TIMEOUT_MS + 20;
    } else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::PROGRESS) {
      stageStartConfirmed_ = true;
      deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) +
        LINK_REPLY_TIMEOUT_MS;
      showRemoteResult(r.packet);
    } else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::RESULT) {
      ProtocolPacket ack = makePacket(MessageType::ACK); ack.sequence = r.packet.sequence;
      ack.passed = r.packet.passed && stageIndex_ + 1U >= stageCount_;
      sendLinked(ack); pwm_.stop();
      showRemoteResult(r.packet);
      if (!r.packet.passed) {
        finish(false, static_cast<FailReason>(r.packet.reason), true);
      } else if (ack.passed) {
        pendingPacket_ = ack;
        state_ = AppState::MASTER_FINALIZE; retries_ = 0;
        deadlineMs_ = millis() + FINAL_ACK_RETRY_INTERVAL_MS;
      } else stagePassed();
    } else if (state_ == AppState::MASTER_FINALIZE && type == MessageType::RESULT) {
      // The Slave did not receive the final ACK and repeated RESULT.
      sendLinked(pendingPacket_);
    } else if (state_ == AppState::SLAVE_WAIT_START && type == MessageType::START_STAGE) {
      sequence_ = r.packet.sequence;
      actual_.actualHz = r.packet.actualHz; actual_.actualPulseNs = r.packet.actualPulseNs;
      actual_.actualDutyPct = dutyFromPulse(actual_.actualHz, actual_.actualPulseNs);
      if (!startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { finish(false, FailReason::UNSUPPORTED); continue; }
      showStageProgress();
      state_ = AppState::SLAVE_MEASURE;
      deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) + LINK_REPLY_TIMEOUT_MS;
      ProtocolPacket started = makePacket(MessageType::READY);
      started.sequence = r.packet.sequence; sendLinked(started);
    } else if (state_ == AppState::SLAVE_MEASURE && type == MessageType::START_STAGE) {
      // START_STAGE or its acknowledgement was lost. Do not restart the
      // measurement; only confirm the already running stage again.
      ProtocolPacket started = makePacket(MessageType::READY);
      started.sequence = r.packet.sequence; sendLinked(started);
    } else if (state_ == AppState::SLAVE_WAIT_ACK && type == MessageType::ACK && r.packet.sequence == pendingPacket_.sequence) {
      if (pendingPacket_.passed) {
        // Master sends ACK only after stopping its PWM. Disable capture for
        // every completed stage, so the next start can clear its queue without
        // racing the ISR (not only after the final stage).
        receiver_.stop();
        if (r.packet.passed) {
          radio_.end(); pendingReason_ = FailReason::NONE;
          if (armSlave(true)) display_.show(UiText::PASS_WORD, UiText::WAIT_MASTER);
        } else {
          stageIndex_ = static_cast<uint32_t>(r.packet.stage) + 1U;
          state_ = AppState::SLAVE_WAIT_START;
        }
      }
      else finish(false, static_cast<FailReason>(pendingPacket_.reason), true);
    }
  }
}

void App::updateMaster() {
  const uint32_t now = millis();
  if (state_ == AppState::MASTER_DISCOVER) {
    if (now - lastOpticalWakeToggleMs_ >= OPTICAL_WAKE_HALF_PERIOD_MS) {
      opticalWakeActive_ = !opticalWakeActive_;
      if (opticalWakeActive_) pwm_.lightOn();
      else pwm_.stop();
      lastOpticalWakeToggleMs_ = now;
    }
    if (now - lastSendMs_ >= DISCOVERY_RETRY_INTERVAL_MS) {
      if (++retries_ % 50U == 0U) Log::event("ESP-NOW", "DISCOVER burst continues");
      radio_.sendBroadcast(pendingPacket_);
      lastSendMs_ = now;
    }
    return;
  }
  if (state_ == AppState::MASTER_FINALIZE) {
    if (now < deadlineMs_) return;
    if (retries_++ < FINAL_ACK_RETRIES) {
      sendLinked(pendingPacket_);
      deadlineMs_ = now + FINAL_ACK_RETRY_INTERVAL_MS;
    } else finish(true, FailReason::NONE);
    return;
  }
  updateHeartbeat();
  if (state_ == AppState::FINISHED) return;
  if (now < deadlineMs_) return;
  if (retries_ >= LINK_PACKET_RETRIES) { finish(false, FailReason::LINK_LOST); return; }
  Log::printf("ESP-NOW", "%s retry=%u", messageName(static_cast<MessageType>(pendingPacket_.type)), retries_ + 1);
  sendLinked(pendingPacket_); ++retries_;
  deadlineMs_ = now + (state_ == AppState::MASTER_WAIT_RESULT ?
    (stageStartConfirmed_ ? stageWallTimeMs(params_.testTimeMs, actual_.actualHz) + LINK_REPLY_TIMEOUT_MS : LINK_RETRY_INTERVAL_MS) :
    LINK_REPLY_TIMEOUT_MS);
}

void App::updateSlave() {
  updateHeartbeat();
  if (state_ == AppState::FINISHED) return;
  if (state_ == AppState::SLAVE_MEASURE) {
    if (static_cast<int32_t>(millis() - localMeasurementDeadlineMs_) >= 0) {
      Log::event("MEASURE", "Slave local stage watchdog expired");
      measurement_.forceFail(FailReason::LOST_EDGE);
    }
    const MeasureState ms = measurement_.update();
    if (measurement_.takeProgressUpdate()) {
      StageStats live = {};
      if (measurement_.statsSnapshot(live)) {
        ProtocolPacket progress = makePacket(MessageType::PROGRESS);
        progress.progressStep = measurement_.progressStep();
        fillMeasuredResult(progress, live);
        progress.sequence = sequence_; sendLinked(progress);
        showStageResult(live);
      }
      return;
    }
    if (ms != MeasureState::PASS && ms != MeasureState::FAIL) {
      return;
    }
    printStageStats(measurement_.stats(), actual_.actualHz);
    showStageResult(measurement_.stats());
    pendingPacket_ = makePacket(MessageType::RESULT);
    pendingPacket_.progressStep = ms == MeasureState::PASS ?
      MEASUREMENT_PROGRESS_STEPS : measurement_.progressStep();
    pendingPacket_.passed = ms == MeasureState::PASS && measurement_.reason() == FailReason::NONE;
    pendingPacket_.reason = static_cast<uint8_t>(measurement_.reason()); pendingPacket_.periods = measurement_.stats().periods;
    fillMeasuredResult(pendingPacket_, measurement_.stats());
    pendingPacket_.minPeriodTicks = measurement_.stats().minPeriod; pendingPacket_.maxPeriodTicks = measurement_.stats().maxPeriod;
    pendingPacket_.sequence = ++sequence_; sendLinked(pendingPacket_);
    Log::printf("TEST", "Slave result prepared: %s reason=%s periods=%lu",
      pendingPacket_.passed ? "PASS" : "FAIL", failName(static_cast<FailReason>(pendingPacket_.reason)), pendingPacket_.periods);
    state_ = AppState::SLAVE_WAIT_ACK; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS;
  } else if (state_ == AppState::SLAVE_WAIT_ACK && millis() >= deadlineMs_) {
    if (retries_++ >= LINK_PACKET_RETRIES) finish(false, FailReason::LINK_LOST);
    else { Log::printf("ESP-NOW", "RESULT retry=%u", retries_); sendLinked(pendingPacket_); deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS; }
  }
}

void App::sendAbort(FailReason reason) {
  if (!havePeer_) return;
  ++sequence_;
  ProtocolPacket packet = makePacket(MessageType::ABORT);
  packet.reason = static_cast<uint8_t>(reason);
  if (!packet.actualPulseNs) packet.actualPulseNs = requestedPulseNs_;
  sendLinked(packet);
}

void App::abortTest() {
  Log::event("ACTION", "abort requested: sending ABORT, stopping receiver and PWM");
  sendAbort(FailReason::ABORTED);
  measurement_.abort();
  driverTest_.abort();
  finish(false, FailReason::ABORTED);
}

void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
  Log::printf("TEST", "finishing result=%s reason=%s", pass ? "PASS" : "FAIL", failName(reason));
  const AppState failedState = state_;
  const bool masterLinkLost = reason == FailReason::LINK_LOST &&
    (failedState == AppState::MASTER_DISCOVER || failedState == AppState::MASTER_WAIT_READY ||
     failedState == AppState::MASTER_WAIT_RESULT || failedState == AppState::MASTER_FINALIZE);
  const bool masterActive = failedState == AppState::MASTER_DISCOVER ||
    failedState == AppState::MASTER_WAIT_READY || failedState == AppState::MASTER_WAIT_RESULT ||
    failedState == AppState::MASTER_FINALIZE;
  const bool slaveLinkLost = reason == FailReason::LINK_LOST &&
    (failedState == AppState::SLAVE_READY || failedState == AppState::SLAVE_WAIT_START ||
     failedState == AppState::SLAVE_MEASURE || failedState == AppState::SLAVE_WAIT_ACK);
  pwm_.stop(); receiver_.stop();
  if (masterLinkLost) {
    Log::event("ESP-NOW", "link lost; returning to continuous discovery");
    startMasterDiscovery();
    return;
  }
  if (!pass && masterActive && havePeer_ && reason != FailReason::ABORTED) sendAbort(reason);
  if (state_ != AppState::IDLE && state_ != AppState::MENU) radio_.end();
  state_ = AppState::FINISHED; pendingReason_ = reason;
  setStandbyOpticalOutput();
  setActivePerformance(false);
  lastUserActivityMs_ = millis();
  if (slaveLinkLost) {
    char target[32], one[64];
    formatTarget(requestedHz_, requestedPulseNs_, target, sizeof(target));
    snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target);
    display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_,
      roleCorner(Role::SLAVE));
    armSlave(true);
    return;
  }
  // The result has already been acknowledged before a normal measurement
  // failure reaches here.  Re-arm ESP-NOW immediately so a quick retry from
  // Master is not hidden behind the former two-second delay; preserve the
  // failure screen while listening.
  if (static_cast<Role>(settings_.role) == Role::SLAVE) {
    armSlave(true);
    return;
  }
  if (preserveDisplay) return;
  char one[64];
  if (pass) {
    const Role role = static_cast<Role>(settings_.role);
    snprintf(one, sizeof(one), "%s %s", uiRoleName(role), UiText::PASS_WORD);
    display_.show(one, role == Role::SLAVE ? UiText::WAIT_MASTER : UiText::START_AGAIN);
  }
  else if (requestedHz_) {
    char target[32];
    formatTarget(requestedHz_, requestedPulseNs_, target, sizeof(target));
    snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target);
    display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_,
      roleCorner(static_cast<Role>(settings_.role)));
  } else {
    display_.show(UiText::TEST_FAILED, uiFailName(reason), 0, 0,
      roleCorner(static_cast<Role>(settings_.role)));
  }
}

bool App::idlePowerSaveAllowed() const {
  // Never enter blocking light sleep while the settings screen is open. A
  // wake-up press is deliberately consumed by the button state machine, which
  // is useful in IDLE but makes menu navigation appear frozen.
  return !usbHostPresent() && initialized_ && (state_ == AppState::IDLE ||
    state_ == AppState::FINISHED || state_ == AppState::SLAVE_READY);
}

bool App::usbHostPresent() const {
#if ARDUINO_USB_MODE && ARDUINO_USB_CDC_ON_BOOT && SOC_USB_SERIAL_JTAG_SUPPORTED
  // This is driven by USB SOF packets, not by CDC traffic: an enumerated host
  // keeps the board awake even if COM is closed and no bytes are exchanged.
  // Retain the state across short SOF/driver glitches.
  const uint32_t now = millis();
  if (Serial.isPlugged()) {
    lastUsbHostSeenMs_ = now ? now : 1U;
    return true;
  }
  return lastUsbHostSeenMs_ &&
    now - lastUsbHostSeenMs_ <= USB_HOST_DISCONNECT_GRACE_MS;
#else
  return false;
#endif
}

void App::setStandbyOpticalOutput() {
  // Calibration and tests may hold the transmitter active. Idle/result
  // screens must always detach PWM and restore the physical light-OFF level.
  pwm_.stop();
}

void App::setActivePerformance(bool active) {
  const bool driverMode = initialized_ &&
    static_cast<TestGroup>(settings_.testGroup) == TestGroup::OPTICS &&
    static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER;
  const uint32_t targetMhz = active ? (driverMode ? 240U : 160U) : 80U;
  if (getCpuFrequencyMhz() != targetMhz && !setCpuFrequencyMhz(targetMhz))
    Log::printf("POWER", "CPU frequency change to %luMHz FAILED", targetMhz);
}

void App::leaveIdlePowerSave(bool wakeDisplay) {
  if (!idlePowerSave_) {
    if (wakeDisplay) display_.setPower(true);
    return;
  }
  idlePowerSave_ = false;
  lastUserActivityMs_ = millis();
  setStandbyOpticalOutput();
  if (idleSleepRadioStopped_) {
    idleSleepRadioStopped_ = false;
    if (radio_.begin()) {
      radio_.setWindowedReceive(true);
      radio_.flush();
      Log::event("POWER", "Slave ESP-NOW restored after external wake");
    } else Log::event("POWER", "Slave ESP-NOW restore FAILED after external wake");
  }
  if (wakeDisplay) display_.setPower(true);
  Log::event("POWER", "idle light sleep ended");
}

void App::serviceIdlePowerSave() {
  if (!idlePowerSaveAllowed()) {
    leaveIdlePowerSave(false);
    return;
  }

  const uint32_t now = millis();
  if (!idlePowerSave_) {
    if (now - lastUserActivityMs_ < IDLE_POWER_SAVE_TIMEOUT_MS) {
      delay(1); // allow the FreeRTOS idle task to halt the CPU between UI polls
      return;
    }
    idlePowerSave_ = true;
    pwm_.stop();
    if (state_ == AppState::SLAVE_READY) {
      radio_.end();
      idleSleepRadioStopped_ = true;
    }
    display_.setPower(false);
    Log::event("POWER", "idle timeout; preparing light sleep");
    Serial.flush();
    delay(2);
  }

  gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_BUTTON_START),
    BUTTON_ACTIVE_LEVEL == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
  gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_BUTTON_MODE),
    BUTTON_ACTIVE_LEVEL == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
  if (static_cast<Role>(settings_.role) == Role::SLAVE) {
    const bool currentRxHigh = gpio_get_level(static_cast<gpio_num_t>(GPIO_RX)) != 0;
    gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_RX),
      currentRxHigh ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
  } else gpio_wakeup_disable(static_cast<gpio_num_t>(GPIO_RX));
  esp_sleep_enable_gpio_wakeup();
  const esp_err_t result = esp_light_sleep_start();
  if (result != ESP_OK) {
    leaveIdlePowerSave(true);
    delay(1);
    return;
  }

  const esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
  const bool buttonWake = digitalRead(GPIO_BUTTON_START) == BUTTON_ACTIVE_LEVEL ||
    digitalRead(GPIO_BUTTON_MODE) == BUTTON_ACTIVE_LEVEL;
  if (buttonWake) {
    startButton_.suppressUntilRelease();
    modeButton_.suppressUntilRelease();
  }

  // GPIO wake worked, so disarm all level sources before peripherals and the
  // button state machines are brought back up.
  gpio_wakeup_disable(static_cast<gpio_num_t>(GPIO_BUTTON_START));
  gpio_wakeup_disable(static_cast<gpio_num_t>(GPIO_BUTTON_MODE));
  gpio_wakeup_disable(static_cast<gpio_num_t>(GPIO_RX));

  // Native USB and I2C can retain stale driver state across light sleep even
  // though their clocks have stopped. A full end/begin cycle prevents the
  // several-second button stalls and restores Serial output after wake.
  setActivePerformance(false);
  Serial.end();
  delay(2);
  Serial.begin(SERIAL_BAUD);
#if ARDUINO_USB_CDC_ON_BOOT
  Serial.setTxTimeoutMs(SERIAL_TX_TIMEOUT_MS);
#endif
  Wire.end();
  Wire.begin(GPIO_SDA, GPIO_SCL);
  Wire.setClock(400000);
  Wire.setTimeOut(30);

  // This also restores ESP-NOW when Slave stopped it before sleeping.
  leaveIdlePowerSave(true);
  Log::printf("POWER", "light sleep wake cause=%u button=%s; peripherals restored",
    static_cast<unsigned>(cause), buttonWake ? "YES" : "NO");
  if (buttonWake)
    Log::event("POWER", "wake button consumed; next press will perform the action");
}

void App::printConfiguration() {
  if (SERIAL_MINIMAL_LOG) return;
  const char *board = TARGET_IS_C3 ? "ESP32-C3" : "ESP32-S3";
  uint8_t mac[6] = {}; esp_read_mac(mac, ESP_MAC_WIFI_STA);
  if (static_cast<TestGroup>(settings_.testGroup) == TestGroup::BOARD) {
    Serial.printf("\nOptical Channel Tester | %s | group=BOARD | test=%s\n", board,
      boardTestName(static_cast<BoardTest>(settings_.boardTest)));
    Serial.printf("GPIO PWM=%u RX=%u ANALOG_RX=%u VBAT=%u START=%u MODE=%u SDA=%u SCL=%u\n",
      GPIO_PWM, GPIO_RX, GPIO_ANALOG_RX, GPIO_VBAT, GPIO_BUTTON_START,
      GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL);
    return;
  }
  Serial.printf("\nOptical Channel Tester | %s | group=OPTICS | mode=%s/%s | light=%s\n", board,
    roleName(static_cast<Role>(settings_.role)),
    testKindName(static_cast<TestKind>(settings_.testKind)),
    configuredLevelName(settings_));
  Serial.printf("MAC=%02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  Serial.printf("GPIO PWM=%u RX=%u START=%u MODE=%u SDA=%u SCL=%u\n", GPIO_PWM, GPIO_RX,
    GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL);
  if (static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER) {
    const char *code = lightCodeName(static_cast<LightCode>(settings_.lightCode));
    Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, TX light=%c RX active light=%c\n",
      params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs,
      params_.accuracyPct, params_.testTimeMs, code[0], code[1]);
  } else {
    Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, optical polarity=AUTO\n",
      params_.frequencyHz, params_.maxPulseNs, params_.minPulseNs,
      params_.accuracyPct, params_.testTimeMs);
  }
  stageCount_ = pulseWidthPointCount(params_.maxPulseNs, params_.minPulseNs);
  Serial.printf("Pulse widths descending (%lu): ", stageCount_);
  for (uint32_t i = 0; i < stageCount_; ++i)
    Serial.printf("%lu%s", pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, i),
      i + 1 == stageCount_ ? " ns\n" : ",");
  Serial.printf("ALL nominal: %llu us | RX=%s\n", actualNominalTotalUs(),
    receiver_.highRateBackend() ? "MCPWM 80MHz" : "GPIO cycle counter");
}

uint64_t App::actualNominalTotalUs() {
  // ALL is only an estimate. Do not attach/detach LEDC for every frequency:
  // large sweeps can perform hundreds of unnecessary driver reconfigurations
  // immediately before the real test and leave no observable PWM on failure.
  return nominalTotalUs(params_, PWM_SETTLE_CYCLES);
}

void App::printStageStats(const StageStats &s, uint32_t hz) {
  if (!s.periods) return;
  const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum;
  const uint32_t measuredPulseNs = static_cast<uint32_t>(lround(
    static_cast<double>(s.activeSum) * 1000000000.0 /
      (static_cast<uint64_t>(receiver_.pulseTickHz()) * s.periods)));
  char requestedText[12], measuredText[12];
  Display::formatFrequency(hz, requestedText, sizeof(requestedText));
  Display::formatFrequency(measuredHz, measuredText, sizeof(measuredText));
  const char *status = s.reason == FailReason::NONE ? "PASS" : "FAIL";
  Log::printf("RESULT", "%s/%luns %s periods=%lu measured=%s/%luns skipped=%lu%s%s",
    requestedText, requestedPulseNs_, status, s.periods, measuredText, measuredPulseNs, s.droppedItems,
    s.reason == FailReason::NONE ? "" : " reason=", s.reason == FailReason::NONE ? "" : failName(s.reason));
}

void App::showStageResult(const StageStats &s) {
  char one[64], two[64];
  formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
  if (s.reason != FailReason::NONE) {
    formatFailure(s.reason, requestedHz_, requestedPulseNs_, one, sizeof(one));
    if (s.reason == FailReason::PERIOD_OUT && s.badFrequency > 0.0f) {
      char frequency[12];
      Display::formatFrequency(s.badFrequency, frequency, sizeof(frequency));
      snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, frequency);
    } else if (s.reason == FailReason::DUTY_OUT && s.badFrequency > 0.0f) {
      char pulse[12];
      Display::formatPulse(pulseFromDuty(s.badFrequency, s.badDuty), pulse,
        sizeof(pulse), true);
      snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, pulse);
    } else {
      snprintf(two, sizeof(two), "%s", uiFailName(s.reason));
    }
    display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()),
      overallProgressTotal(stageCount_), roleCorner(static_cast<Role>(settings_.role)));
    return;
  }
  if (!s.periods || !s.periodSum) {
    display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, measurement_.progressStep()),
      overallProgressTotal(stageCount_));
    return;
  }
  const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum;
  const uint32_t measuredPulseNs = static_cast<uint32_t>(lround(
    static_cast<double>(s.activeSum) * 1000000000.0 /
      (static_cast<uint64_t>(receiver_.pulseTickHz()) * s.periods)));
  formatMeasured(measuredHz, measuredPulseNs, two, sizeof(two));
  display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()),
    overallProgressTotal(stageCount_));
}

void App::showDriverResult(const DriverStats &s, bool testPassed) {
  char one[64], two[64];
  const bool haveResponse = s.responses || s.lastResponseTicks;
  const uint64_t delaySumTicks =
    s.turnOn.delaySumTicks + s.turnOff.delaySumTicks;
  const uint64_t responseSumTicks =
    s.turnOn.responseSumTicks + s.turnOff.responseSumTicks;
  const uint64_t displayedDelayTicks = s.responses ?
    delaySumTicks / s.responses : s.lastDelayTicks;
  const uint64_t displayedResponseTicks = s.responses ?
    responseSumTicks / s.responses : s.lastResponseTicks;
  const uint32_t delayNs = static_cast<uint32_t>(
    (displayedDelayTicks * 1000000000ULL +
      driverTest_.tickHz() / 2U) / driverTest_.tickHz());
  const uint32_t responseNs = static_cast<uint32_t>(
    (displayedResponseTicks * 1000000000ULL +
      driverTest_.tickHz() / 2U) / driverTest_.tickHz());
  char delay[12] = "---", response[12] = "---";
  if (haveResponse) {
    Display::formatPulse(delayNs, delay, sizeof(delay));
    Display::formatPulse(responseNs, response, sizeof(response));
  }
  if (testPassed) {
    snprintf(one, sizeof(one), "%s", UiText::PASS_WORD);
    snprintf(two, sizeof(two), UiText::DRIVER_MEASUREMENT_FORMAT,
      delay, response);
  } else if (s.reason != FailReason::NONE) {
    snprintf(one, sizeof(one), "%s", uiFailName(s.reason));
    char elapsed[12] = "---", errorPulse[12] = "---";
    if (s.errorTriggerValid) {
      const uint64_t elapsedNs =
        (static_cast<uint64_t>(s.errorTriggerTicks) * 1000000000ULL +
          driverTest_.tickHz() / 2U) /
        driverTest_.tickHz();
      formatElapsedNs(elapsedNs, elapsed, sizeof(elapsed));
    }
    if (s.errorPulseValid) {
      const uint64_t errorPulseNs =
        (static_cast<uint64_t>(s.errorPulseTicks) * 1000000000ULL +
          driverTest_.tickHz() / 2U) / driverTest_.tickHz();
      formatElapsedNs(errorPulseNs, errorPulse, sizeof(errorPulse));
    }
    if (s.errorPulseValid)
      snprintf(two, sizeof(two), "T:%s P:%s", elapsed, errorPulse);
    else snprintf(two, sizeof(two), "T:%s", elapsed);
  } else {
    formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
    snprintf(two, sizeof(two), UiText::DRIVER_MEASUREMENT_FORMAT,
      delay, response);
  }
  const bool finished = testPassed || s.reason != FailReason::NONE;
  const uint8_t progressSteps = driverTest_.progressSteps();
  display_.show(one, two,
    finished ? 0U : overallProgress(
      stageIndex_, driverTest_.progressStep(), progressSteps),
    finished ? 0U : overallProgressTotal(stageCount_, progressSteps),
    s.reason == FailReason::NONE ? nullptr : roleCorner(Role::SOLO));
}

void App::showRemoteResult(const ProtocolPacket &packet) {
  const FailReason reason = packet.reason <= static_cast<uint8_t>(FailReason::ABORTED)
    ? static_cast<FailReason>(packet.reason) : FailReason::UNSUPPORTED;
  char one[64], two[64];
  formatTestTarget(packet.requestedHz, packet.requestedPulseNs, one, sizeof(one));
  if (reason == FailReason::NONE) {
    if (packet.measuredHzX10) {
      formatMeasured(packet.measuredHzX10 / 10.0f, packet.measuredPulseNs, two, sizeof(two));
    } else snprintf(two, sizeof(two), "%s", UiText::NO_MEASUREMENT);
  } else if (reason == FailReason::PERIOD_OUT && packet.measuredHzX10) {
    char frequency[12];
    Display::formatFrequency(packet.measuredHzX10 / 10.0f, frequency, sizeof(frequency));
    snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, frequency);
  } else if (reason == FailReason::DUTY_OUT && packet.measuredPulseNs) {
    char pulse[12];
    Display::formatPulse(packet.measuredPulseNs, pulse, sizeof(pulse), true);
    snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, pulse);
  } else {
    snprintf(two, sizeof(two), "%s", uiFailName(reason));
  }
  if (reason != FailReason::NONE)
    formatFailure(reason, packet.requestedHz, packet.requestedPulseNs, one, sizeof(one));
  display_.show(one, two, overallProgress(stageIndex_, packet.progressStep),
    overallProgressTotal(stageCount_), reason == FailReason::NONE ? nullptr :
      roleCorner(static_cast<Role>(settings_.role)));
}

void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const {
  packet.reason = static_cast<uint8_t>(stats.reason);
  packet.periods = stats.periods;
  if (!stats.periods || !stats.periodSum) return;
  const bool badPeriod = (stats.reason == FailReason::PERIOD_OUT || stats.reason == FailReason::DUTY_OUT) &&
    stats.badFrequency > 0.0f;
  const float measuredHz = badPeriod ? stats.badFrequency :
    static_cast<float>(receiver_.tickHz()) * stats.periods / stats.periodSum;
  packet.measuredHzX10 = static_cast<uint32_t>(lroundf(measuredHz * 10.0f));
  packet.measuredPulseNs = badPeriod ? pulseFromDuty(measuredHz, stats.badDuty) :
    static_cast<uint32_t>(lround(static_cast<double>(stats.activeSum) * 1000000000.0 /
      (static_cast<uint64_t>(receiver_.pulseTickHz()) * stats.periods)));
}

void App::showStageProgress() {
  if (static_cast<TestKind>(settings_.testKind) == TestKind::DRIVER) {
    char one[64], two[64];
    formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
    snprintf(two, sizeof(two), UiText::DRIVER_MEASUREMENT_FORMAT,
      "---", "---");
    display_.show(one, two, overallProgress(stageIndex_, 0),
      overallProgressTotal(stageCount_));
    return;
  }
  char one[64];
  formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
  display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, 0),
    overallProgressTotal(stageCount_));
}
