#include "SignalProbe.h" #include "Config.h" #include namespace { constexpr uint32_t DISPLAY_INTERVAL_MS = 250; constexpr uint8_t PERIOD_TOLERANCE_PCT = 10; bool pwmPointAvailable(uint32_t frequencyHz, uint32_t pulseNs) { return static_cast(frequencyHz) * pulseNs < 1000000000ULL; } void formatWidth(uint64_t nanoseconds, char *out, size_t size) { if (nanoseconds < 1000ULL) snprintf(out, size, "%lluns", nanoseconds); else if (nanoseconds < 1000000ULL) snprintf(out, size, "%.2fus", nanoseconds / 1000.0); else snprintf(out, size, "%.2fms", nanoseconds / 1000000.0); } void formatPulseCount(uint64_t count, char *out, size_t size) { if (count < 1000000ULL) snprintf(out, size, "%llu", count); else if (count < 1000000000ULL) snprintf(out, size, "%lluM", count / 1000000ULL); else snprintf(out, size, "%lluG", count / 1000000000ULL); } } SignalProbe::SignalProbe() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE) {} void SignalProbe::begin() { Serial.begin(SERIAL_BAUD); #if ARDUINO_USB_CDC_ON_BOOT Serial.setTxTimeoutMs(SERIAL_TX_TIMEOUT_MS); #endif setCpuFrequencyMhz(160); startButton_.begin(); modeButton_.begin(); pwm_.begin(); pwm_.configureActiveLight(true); display_.begin(); receiverInitialized_ = receiver_.begin(); rxReady_ = receiverInitialized_ && captureStart(); applyPwm(); Serial.printf("Signal probe: MODE short=frequency, MODE long=pulse, START short=reset RX minimum, START long=PWM on/off, hold both=RX active level; RX=%s\n", rxActiveHigh_ ? "HIGH" : "LOW"); show(); } bool SignalProbe::captureStart() { if (!receiver_.startRaw()) { Serial.println("RX capture start failed"); return false; } havePulseStart_ = false; havePulseEnd_ = false; extraEdgeInPeriod_ = false; return true; } void SignalProbe::resetMinimum() { if (rxReady_) receiver_.stop(); rxReady_ = receiverInitialized_ && captureStart(); minTicks_ = UINT64_MAX; pulseCount_ = 0; edgeCount_ = 0; rejectedPeriods_ = 0; displayDirty_ = true; Serial.println("RX minimum reset"); } void SignalProbe::updateCapture() { if (!rxReady_) return; CaptureEvent edges[64]; for (uint8_t batch = 0; batch < 8; ++batch) { const size_t count = receiver_.readRawEdges(edges, countOf(edges)); if (!count) break; uint64_t batchMinimum = minTicks_; uint64_t batchPulses = 0; uint64_t batchRejected = 0; const bool activeRising = rxActiveHigh_; const uint32_t expectedHz = PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_]; const uint64_t expectedPeriod = receiver_.tickHz() / expectedHz; for (size_t i = 0; i < count; ++i) { if (edges[i].rising == activeRising) { if (havePulseStart_) { const uint64_t period = edges[i].tick - pulseStartTick_; const uint64_t width = havePulseEnd_ ? pulseEndTick_ - pulseStartTick_ : 0; const uint64_t error = period > expectedPeriod ? period - expectedPeriod : expectedPeriod - period; if (havePulseEnd_ && !extraEdgeInPeriod_ && period && width && width < period && error * 100U <= expectedPeriod * PERIOD_TOLERANCE_PCT) { if (width < batchMinimum) batchMinimum = width; ++batchPulses; } else { ++batchRejected; } } pulseStartTick_ = edges[i].tick; havePulseStart_ = true; havePulseEnd_ = false; extraEdgeInPeriod_ = false; } else if (havePulseStart_ && !havePulseEnd_ && edges[i].tick > pulseStartTick_) { pulseEndTick_ = edges[i].tick; havePulseEnd_ = true; } else if (havePulseStart_) { extraEdgeInPeriod_ = true; } } const uint32_t dropped = receiver_.takeDroppedItems(); if (dropped) { Serial.printf("RX capture overflow: %lu edges lost; restarting capture\n", dropped); receiver_.stop(); rxReady_ = captureStart(); displayDirty_ = true; return; } edgeCount_ += count; pulseCount_ += batchPulses; rejectedPeriods_ += batchRejected; displayDirty_ = true; if (batchMinimum < minTicks_) { minTicks_ = batchMinimum; displayDirty_ = true; } } } void SignalProbe::applyPwm() { pwm_.stop(); if (!pwmEnabled_) { Serial.println("PWM off"); displayDirty_ = true; return; } const uint32_t frequency = PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_]; const uint32_t pulse = MAX_PULSE_OPTIONS_NS[pulseIndex_]; if (!pwm_.start(frequency, pulse, actual_)) { Serial.printf("PWM start failed: %lu Hz, %lu ns\n", frequency, pulse); pwmEnabled_ = false; } else { Serial.printf("PWM GPIO=%u requested=%luHz/%luns actual=%luHz/%luns\n", GPIO_PWM, frequency, pulse, actual_.actualHz, actual_.actualPulseNs); } displayDirty_ = true; } void SignalProbe::advanceFrequency() { const uint8_t count = static_cast(countOf(PWM_FREQUENCY_OPTIONS_HZ)); for (uint8_t step = 0; step < count; ++step) { frequencyIndex_ = static_cast((frequencyIndex_ + 1U) % count); if (pwmPointAvailable(PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_], MAX_PULSE_OPTIONS_NS[pulseIndex_])) break; } applyPwm(); resetMinimum(); } void SignalProbe::advancePulse() { const uint8_t count = static_cast(countOf(MAX_PULSE_OPTIONS_NS)); for (uint8_t step = 0; step < count; ++step) { pulseIndex_ = static_cast((pulseIndex_ + 1U) % count); if (pwmPointAvailable(PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_], MAX_PULSE_OPTIONS_NS[pulseIndex_])) break; } applyPwm(); resetMinimum(); } void SignalProbe::show() { char frequency[16], pulse[16], first[64], second[64]; const uint32_t shownFrequency = pwm_.running() ? actual_.actualHz : PWM_FREQUENCY_OPTIONS_HZ[frequencyIndex_]; const uint32_t shownPulse = pwm_.running() ? actual_.actualPulseNs : MAX_PULSE_OPTIONS_NS[pulseIndex_]; Display::formatPwmFrequency(shownFrequency, frequency, sizeof(frequency)); Display::formatPulse(shownPulse, pulse, sizeof(pulse)); snprintf(first, sizeof(first), "PWM%s %s %s", pwmEnabled_ ? "" : " OFF", frequency, pulse); if (!rxReady_) { snprintf(second, sizeof(second), "RX: ERROR"); } else if (minTicks_ == UINT64_MAX) { snprintf(second, sizeof(second), "RX E:%llu BAD:%llu", edgeCount_, rejectedPeriods_); } else { char width[24], count[12]; const uint64_t nanoseconds = (minTicks_ * 1000000000ULL + receiver_.tickHz() / 2U) / receiver_.tickHz(); formatWidth(nanoseconds, width, sizeof(width)); formatPulseCount(pulseCount_, count, sizeof(count)); snprintf(second, sizeof(second), "MIN:%s N:%s", width, count); Serial.printf("RX minimum=%lluns, periods=%llu, rejected=%llu, edges=%llu\n", nanoseconds, pulseCount_, rejectedPeriods_, edgeCount_); } display_.show(first, second, 0, 0, rxActiveHigh_ ? "H" : "L"); lastDisplayMs_ = millis(); displayDirty_ = false; } void SignalProbe::update() { const uint32_t now = millis(); const ButtonEvent start = startButton_.update(now); const ButtonEvent mode = modeButton_.update(now); if (startButton_.pressed() && modeButton_.pressed()) { if (!bothHeld_) { bothHeld_ = true; bothHeldSinceMs_ = now; startButton_.suppressUntilRelease(); modeButton_.suppressUntilRelease(); } if (!bothHeldHandled_ && now - bothHeldSinceMs_ >= BUTTON_LONG_PRESS_MS) { rxActiveHigh_ = !rxActiveHigh_; bothHeldHandled_ = true; resetMinimum(); Serial.printf("RX active level=%s\n", rxActiveHigh_ ? "HIGH" : "LOW"); } } else if (bothHeld_) { if (!startButton_.pressed() && !modeButton_.pressed()) { bothHeld_ = false; bothHeldHandled_ = false; } } else { if (mode == ButtonEvent::SHORT) advanceFrequency(); else if (mode == ButtonEvent::LONG) advancePulse(); if (start == ButtonEvent::SHORT) resetMinimum(); else if (start == ButtonEvent::LONG) { pwmEnabled_ = !pwmEnabled_; applyPwm(); } } updateCapture(); if (displayDirty_ && now - lastDisplayMs_ >= DISPLAY_INTERVAL_MS) show(); }