Глобальная переделка. тест сделан по длине импульса и заданной частоте шим, а не меандру
This commit is contained in:
@@ -8,10 +8,13 @@
|
||||
#include <esp_system.h>
|
||||
#include <esp32-hal-cpu.h>
|
||||
#include <driver/gpio.h>
|
||||
#include <Wire.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
const char *uiFailName(FailReason reason);
|
||||
|
||||
const char *appStateName(AppState state) {
|
||||
static const char *names[] = {"IDLE", "MENU", "SOLO_MEASURE", "MASTER_DISCOVER",
|
||||
"MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START",
|
||||
@@ -26,9 +29,40 @@ const char *buttonEventName(ButtonEvent event) {
|
||||
return index < sizeof(names) / sizeof(names[0]) ? names[index] : "UNKNOWN";
|
||||
}
|
||||
|
||||
void formatErrorDuty(float duty, char *out, size_t size) {
|
||||
if (fabsf(duty - roundf(duty)) < 0.05f) snprintf(out, size, "%.0f%%", duty);
|
||||
else snprintf(out, size, "%.1f%%", duty);
|
||||
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);
|
||||
}
|
||||
|
||||
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, "TEST: %s", 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, "FAIL AT %s", target);
|
||||
}
|
||||
|
||||
size_t utf8CharacterCount(const char *text) {
|
||||
@@ -81,12 +115,94 @@ uint32_t overallProgressTotal(uint32_t stageCount) {
|
||||
uint32_t stageWallTimeMs(uint32_t testTimeMs, uint32_t frequencyHz) {
|
||||
return static_cast<uint32_t>((nominalStageUs(frequencyHz, testTimeMs, PWM_SETTLE_CYCLES) + 999ULL) / 1000ULL);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE), measurement_(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();
|
||||
@@ -103,11 +219,12 @@ void App::finishInitialization(bool factoryReset) {
|
||||
} else if (!store_.load(settings_)) {
|
||||
store_.save(settings_); Log::event("BOOT", "NVS invalid/missing: defaults loaded");
|
||||
}
|
||||
sanitizeRange();
|
||||
params_ = store_.params(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() ? "RMT DMA" : "RMT ping-pong");
|
||||
Log::printf("BOOT", "capture initialized: %s", receiver_.highRateBackend() ? "MCPWM 80MHz" : "GPIO cycle counter");
|
||||
lastUserActivityMs_ = millis();
|
||||
setActivePerformance(false);
|
||||
printConfiguration();
|
||||
@@ -166,7 +283,7 @@ void App::update() {
|
||||
}
|
||||
if (state_ == AppState::MENU) {
|
||||
if (modeEvent == ButtonEvent::SHORT) {
|
||||
menuItem_ = (menuItem_ + 1U) % 4U; Log::printf("ACTION", "menu item selected index=%u", menuItem_); showMenu();
|
||||
menuItem_ = (menuItem_ + 1U) % 5U; Log::printf("ACTION", "menu item selected index=%u", menuItem_); showMenu();
|
||||
}
|
||||
else if (modeEvent == ButtonEvent::LONG) {
|
||||
sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_);
|
||||
@@ -179,20 +296,25 @@ void App::update() {
|
||||
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 (ms == MeasureState::STEP_READY) {
|
||||
} else if (measurement_.takeProgressUpdate()) {
|
||||
StageStats live = {};
|
||||
if (measurement_.statsSnapshot(live)) showStageResult(live);
|
||||
measurement_.continueAfterDisplay();
|
||||
}
|
||||
} else if (state_ == AppState::MASTER_DISCOVER || state_ == AppState::MASTER_WAIT_READY ||
|
||||
state_ == AppState::MASTER_WAIT_RESULT || state_ == AppState::MASTER_FINALIZE) {
|
||||
@@ -212,8 +334,21 @@ void App::showIdle() {
|
||||
}
|
||||
|
||||
void App::sanitizeRange() {
|
||||
settings_.startIndex %= countOf(START_FREQ_OPTIONS_HZ);
|
||||
settings_.endIndex %= countOf(END_FREQ_OPTIONS_HZ);
|
||||
if (settings_.role > static_cast<uint8_t>(Role::SLAVE))
|
||||
settings_.role = static_cast<uint8_t>(Role::SOLO);
|
||||
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;
|
||||
const uint8_t firstMin = firstMinPulseIndexAtLeast(
|
||||
minimumPulseForAccuracy(hz, ACCURACY_OPTIONS_PCT[settings_.accuracyIndex]), lastMin);
|
||||
if (settings_.minPulseIndex < firstMin) settings_.minPulseIndex = firstMin;
|
||||
}
|
||||
|
||||
void App::serviceRxPinStateLog() {
|
||||
@@ -231,17 +366,36 @@ void App::serviceRxPinStateLog() {
|
||||
|
||||
void App::changeMenu(int d) {
|
||||
sanitizeRange();
|
||||
uint8_t *value = nullptr; size_t count = 0;
|
||||
switch (menuItem_) {
|
||||
case 0: value = &settings_.startIndex; count = countOf(START_FREQ_OPTIONS_HZ); break;
|
||||
case 1: value = &settings_.endIndex; count = countOf(END_FREQ_OPTIONS_HZ); break;
|
||||
case 2: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break;
|
||||
case 3: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
|
||||
default: return;
|
||||
if (menuItem_ == 1) {
|
||||
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_ == 2) {
|
||||
const uint8_t last = lastMinPulseIndexAtMost(
|
||||
MAX_PULSE_OPTIONS_NS[settings_.maxPulseIndex]);
|
||||
const uint8_t first = firstMinPulseIndexAtLeast(
|
||||
minimumPulseForAccuracy(PWM_FREQUENCY_OPTIONS_HZ[settings_.frequencyIndex],
|
||||
ACCURACY_OPTIONS_PCT[settings_.accuracyIndex]), last);
|
||||
settings_.minPulseIndex = cycleIndex(settings_.minPulseIndex,
|
||||
first, last, d);
|
||||
} else {
|
||||
uint8_t *value = nullptr; size_t count = 0;
|
||||
switch (menuItem_) {
|
||||
case 0: value = &settings_.frequencyIndex; count = countOf(PWM_FREQUENCY_OPTIONS_HZ); break;
|
||||
case 3: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break;
|
||||
case 4: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
|
||||
default: return;
|
||||
}
|
||||
*value = cycleIndex(*value, 0, static_cast<uint8_t>(count - 1U), d);
|
||||
}
|
||||
*value = static_cast<uint8_t>((*value + count + d) % count);
|
||||
Log::printf("ACTION", "menu item=%u changed direction=%+d new-index=%u", menuItem_, d, *value);
|
||||
sanitizeRange(); params_ = store_.params(settings_); showMenu();
|
||||
sanitizeRange(); params_ = store_.params(settings_);
|
||||
Log::printf("ACTION", "menu item=%u changed direction=%+d frequency=%u max-pulse=%u min-pulse=%u accuracy=%u time=%u",
|
||||
menuItem_, d, settings_.frequencyIndex, settings_.maxPulseIndex,
|
||||
settings_.minPulseIndex, settings_.accuracyIndex, settings_.timeIndex);
|
||||
showMenu();
|
||||
}
|
||||
|
||||
void App::showMenu() {
|
||||
@@ -250,23 +404,26 @@ void App::showMenu() {
|
||||
Display::formatDuration(actualNominalTotalUs(), all, sizeof(all));
|
||||
switch (menuItem_) {
|
||||
case 0:
|
||||
Display::formatTestFrequency(params_.startHz, value, sizeof(value));
|
||||
strncat(value, UiText::FREQUENCY_UNIT, sizeof(value) - strlen(value) - 1U);
|
||||
label = UiText::MENU_START_FREQUENCY;
|
||||
Display::formatPwmFrequency(params_.frequencyHz, value, sizeof(value));
|
||||
label = UiText::MENU_FREQUENCY;
|
||||
break;
|
||||
case 1:
|
||||
Display::formatTestFrequency(params_.endHz, value, sizeof(value));
|
||||
strncat(value, UiText::FREQUENCY_UNIT, sizeof(value) - strlen(value) - 1U);
|
||||
label = UiText::MENU_END_FREQUENCY;
|
||||
Display::formatPulse(params_.maxPulseNs, value, sizeof(value));
|
||||
label = UiText::MENU_MAX_PULSE;
|
||||
break;
|
||||
case 2:
|
||||
Display::formatPulse(params_.minPulseNs, value, sizeof(value));
|
||||
label = UiText::MENU_MIN_PULSE;
|
||||
break;
|
||||
case 3:
|
||||
snprintf(value, sizeof(value), "+/-%g%%", params_.accuracyPct);
|
||||
label = UiText::MENU_ACCURACY;
|
||||
break;
|
||||
default:
|
||||
case 4:
|
||||
snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f);
|
||||
label = UiText::MENU_TEST_TIME;
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
formatMenuLine(label, value, one, sizeof(one));
|
||||
formatMenuLine(UiText::MENU_TOTAL_TIME, all, total, sizeof(total));
|
||||
@@ -277,18 +434,17 @@ void App::startTest() {
|
||||
leaveIdlePowerSave();
|
||||
pwm_.stop();
|
||||
setActivePerformance(true);
|
||||
params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz);
|
||||
stageIndex_ = 0; requestedHz_ = 0; pendingReason_ = FailReason::NONE;
|
||||
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 stages=%lu", roleName(static_cast<Role>(settings_.role)), stageCount_);
|
||||
if (SERIAL_MINIMAL_LOG) {
|
||||
char startText[12], endText[12];
|
||||
Display::formatFrequency(params_.startHz, startText, sizeof(startText));
|
||||
Display::formatFrequency(params_.endHz, endText, sizeof(endText));
|
||||
Log::printf("CONFIG", "mode=%s range=%s..%s adjacent accuracy=%.2f%% time=%lums duty=%u%% stages=%lu",
|
||||
roleName(static_cast<Role>(settings_.role)), startText, endText,
|
||||
params_.accuracyPct, params_.testTimeMs, params_.dutyPct, stageCount_);
|
||||
Log::printf("CONFIG", "mode=%s frequency=%luHz pulse=%lu..%luns accuracy=%.2f%% time=%lums TX=%s RX=AUTO stages=%lu",
|
||||
roleName(static_cast<Role>(settings_.role)), params_.frequencyHz,
|
||||
params_.maxPulseNs, params_.minPulseNs, params_.accuracyPct, params_.testTimeMs,
|
||||
PWM_ACTIVE_LEVEL == HIGH ? "HIGH" : "LOW",
|
||||
stageCount_);
|
||||
}
|
||||
printConfiguration();
|
||||
const Role role = static_cast<Role>(settings_.role);
|
||||
@@ -305,8 +461,8 @@ bool App::armSlave(bool preserveDisplay) {
|
||||
pwm_.stop();
|
||||
lastUserActivityMs_ = millis();
|
||||
params_ = store_.params(settings_);
|
||||
stageIndex_ = 0; stageCount_ = frequencyPointCount(params_.startHz, params_.endHz);
|
||||
requestedHz_ = 0; session_ = 0; sequence_ = 0; havePeer_ = false;
|
||||
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;
|
||||
@@ -323,29 +479,40 @@ bool App::armSlave(bool preserveDisplay) {
|
||||
}
|
||||
|
||||
bool App::prepareStage(bool showProgress) {
|
||||
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_);
|
||||
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 duty=%u%%", GPIO_PWM, requestedHz_, params_.dutyPct);
|
||||
if (!pwm_.start(requestedHz_, params_.dutyPct, actual_)) {
|
||||
Log::printf("PWM", "START FAILED GPIO=%u requested=%luHz; LEDC attach/write/read failed",
|
||||
GPIO_PWM, requestedHz_);
|
||||
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 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, actual_.bits,
|
||||
plannedRxHz, plannedPulseRxHz, actual_.bits,
|
||||
MEASUREMENT_AVERAGING_PERIODS);
|
||||
if (resolution != FailReason::NONE) {
|
||||
Log::printf("PWM", "resolution rejected: actual=%luHz duty=%.3f%% bits=%u RXclock=%luHz tolerance=%.3f%%",
|
||||
actual_.actualHz, actual_.actualDutyPct, actual_.bits, plannedRxHz,
|
||||
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;
|
||||
}
|
||||
Log::printf("PWM", "stage=%lu/%lu requested=%luHz actual=%luHz duty=%.2f%% bits=%u STARTED",
|
||||
stageIndex_ + 1, stageCount_, requestedHz_, actual_.actualHz, actual_.actualDutyPct, actual_.bits);
|
||||
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 && !startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) {
|
||||
finish(false, FailReason::UNSUPPORTED); return false;
|
||||
@@ -354,23 +521,33 @@ bool App::prepareStage(bool showProgress) {
|
||||
}
|
||||
|
||||
bool App::startLocalMeasurement(float hz, float duty) {
|
||||
Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% RX=%luHz settle=%u cycles window=%lums average=%u periods; per-pulse logging suspended",
|
||||
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),
|
||||
PWM_SETTLE_CYCLES, params_.testTimeMs, MEASUREMENT_AVERAGING_PERIODS);
|
||||
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);
|
||||
Log::printf("MEASURE", "receiver start %s, RMT chunk=%u symbols", ok ? "OK" : "FAILED",
|
||||
receiver_.receiveChunkSymbols());
|
||||
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;
|
||||
}
|
||||
|
||||
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_) { finish(true, FailReason::NONE); return; }
|
||||
if (static_cast<Role>(settings_.role) == Role::SOLO) { if (prepareStage()) state_ = AppState::SOLO_MEASURE; }
|
||||
else if (static_cast<Role>(settings_.role) == Role::MASTER) {
|
||||
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_);
|
||||
requestedHz_ = params_.frequencyHz;
|
||||
requestedPulseNs_ = pulseWidthAt(params_.maxPulseNs, params_.minPulseNs, stageIndex_);
|
||||
actual_ = {};
|
||||
stageStartConfirmed_ = false;
|
||||
pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE);
|
||||
@@ -380,7 +557,8 @@ void App::stagePassed() {
|
||||
|
||||
void App::startMasterDiscovery() {
|
||||
session_ = esp_random(); if (!session_) session_ = 1;
|
||||
sequence_ = 1; stageIndex_ = 0; requestedHz_ = 0; havePeer_ = false; radio_.flush();
|
||||
sequence_ = 1; stageIndex_ = 0; requestedHz_ = params_.frequencyHz;
|
||||
requestedPulseNs_ = 0; havePeer_ = false; radio_.flush();
|
||||
opticalWakeActive_ = true;
|
||||
lastOpticalWakeToggleMs_ = millis();
|
||||
pwm_.active();
|
||||
@@ -394,9 +572,8 @@ 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.actualHz = actual_.actualHz;
|
||||
const float packetDuty = actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct;
|
||||
p.actualDutyX100 = static_cast<uint16_t>(packetDuty * 100.0f + 0.5f);
|
||||
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.settleCycles = PWM_SETTLE_CYCLES;
|
||||
return p;
|
||||
@@ -458,7 +635,8 @@ void App::handleRadio() {
|
||||
opticalWakeActive_ = false;
|
||||
pwm_.stop();
|
||||
memcpy(peer_, r.mac, 6); havePeer_ = true; lastPeerSeenMs_ = lastHeartbeatMs_ = millis();
|
||||
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_);
|
||||
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;
|
||||
}
|
||||
@@ -484,7 +662,8 @@ void App::handleRadio() {
|
||||
sequence_ = r.packet.sequence;
|
||||
state_ = AppState::SLAVE_WAIT_START;
|
||||
params_.testTimeMs = r.packet.testTimeMs;
|
||||
params_.accuracyPct = r.packet.accuracyX100 / 100.0f; requestedHz_ = r.packet.requestedHz;
|
||||
params_.accuracyPct = r.packet.accuracyX100 / 100.0f;
|
||||
requestedHz_ = r.packet.requestedHz; requestedPulseNs_ = r.packet.requestedPulseNs;
|
||||
stageCount_ = r.packet.stageCount;
|
||||
actual_ = {};
|
||||
ProtocolPacket ready = makePacket(MessageType::READY);
|
||||
@@ -498,8 +677,10 @@ void App::handleRadio() {
|
||||
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_.actualDutyPct = r.packet.actualDutyX100 ? r.packet.actualDutyX100 / 100.0f : params_.dutyPct;
|
||||
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) {
|
||||
@@ -535,7 +716,8 @@ void App::handleRadio() {
|
||||
sendLinked(pendingPacket_);
|
||||
} else if (state_ == AppState::SLAVE_WAIT_START && type == MessageType::START_STAGE) {
|
||||
sequence_ = r.packet.sequence;
|
||||
actual_.actualHz = r.packet.actualHz; actual_.actualDutyPct = r.packet.actualDutyX100 / 100.0f;
|
||||
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;
|
||||
@@ -549,6 +731,10 @@ void App::handleRadio() {
|
||||
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);
|
||||
@@ -601,8 +787,12 @@ 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 (ms == MeasureState::STEP_READY) {
|
||||
if (measurement_.takeProgressUpdate()) {
|
||||
StageStats live = {};
|
||||
if (measurement_.statsSnapshot(live)) {
|
||||
ProtocolPacket progress = makePacket(MessageType::PROGRESS);
|
||||
@@ -611,7 +801,6 @@ void App::updateSlave() {
|
||||
progress.sequence = sequence_; sendLinked(progress);
|
||||
showStageResult(live);
|
||||
}
|
||||
measurement_.continueAfterDisplay();
|
||||
return;
|
||||
}
|
||||
if (ms != MeasureState::PASS && ms != MeasureState::FAIL) {
|
||||
@@ -641,7 +830,7 @@ void App::sendAbort(FailReason reason) {
|
||||
++sequence_;
|
||||
ProtocolPacket packet = makePacket(MessageType::ABORT);
|
||||
packet.reason = static_cast<uint8_t>(reason);
|
||||
if (!packet.actualDutyX100) packet.actualDutyX100 = params_.dutyPct * 100U;
|
||||
if (!packet.actualPulseNs) packet.actualPulseNs = requestedPulseNs_;
|
||||
sendLinked(packet);
|
||||
}
|
||||
|
||||
@@ -675,10 +864,9 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
|
||||
setActivePerformance(false);
|
||||
lastUserActivityMs_ = millis();
|
||||
if (slaveLinkLost) {
|
||||
char target[12], one[64];
|
||||
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, target, sizeof(target));
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target,
|
||||
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct);
|
||||
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);
|
||||
@@ -700,10 +888,9 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
|
||||
display_.show(one, role == Role::SLAVE ? UiText::WAIT_MASTER : UiText::START_AGAIN);
|
||||
}
|
||||
else if (requestedHz_) {
|
||||
char frequency[12];
|
||||
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, frequency, sizeof(frequency));
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, frequency,
|
||||
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct);
|
||||
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 {
|
||||
@@ -713,7 +900,10 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
|
||||
}
|
||||
|
||||
bool App::idlePowerSaveAllowed() const {
|
||||
return initialized_ && (state_ == AppState::IDLE || state_ == AppState::MENU ||
|
||||
// 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 initialized_ && (state_ == AppState::IDLE ||
|
||||
state_ == AppState::FINISHED || state_ == AppState::SLAVE_READY);
|
||||
}
|
||||
|
||||
@@ -767,7 +957,9 @@ void App::serviceIdlePowerSave() {
|
||||
idleSleepRadioStopped_ = true;
|
||||
}
|
||||
display_.setPower(false);
|
||||
Log::event("POWER", "idle timeout; OLED off and light sleep started");
|
||||
Log::event("POWER", "idle timeout; preparing light sleep");
|
||||
Serial.flush();
|
||||
delay(2);
|
||||
}
|
||||
|
||||
gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_BUTTON_START),
|
||||
@@ -775,10 +967,6 @@ void App::serviceIdlePowerSave() {
|
||||
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) {
|
||||
// Light-sleep GPIO wake is level-triggered in ESP-IDF. Arm the level
|
||||
// opposite to the one sampled immediately before sleep, which makes a
|
||||
// transition (either edge) necessary and prevents a steady RX level from
|
||||
// waking Slave continuously.
|
||||
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);
|
||||
@@ -786,25 +974,46 @@ void App::serviceIdlePowerSave() {
|
||||
esp_sleep_enable_gpio_wakeup();
|
||||
const esp_err_t result = esp_light_sleep_start();
|
||||
if (result != ESP_OK) {
|
||||
leaveIdlePowerSave(true);
|
||||
delay(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO) {
|
||||
const bool buttonWake = digitalRead(GPIO_BUTTON_START) == BUTTON_ACTIVE_LEVEL ||
|
||||
digitalRead(GPIO_BUTTON_MODE) == BUTTON_ACTIVE_LEVEL;
|
||||
if (buttonWake) {
|
||||
// The wake-up press is deliberately consumed. Holding or releasing it
|
||||
// must not later turn into a SHORT, LONG, or REPEAT event.
|
||||
startButton_.suppressUntilRelease();
|
||||
modeButton_.suppressUntilRelease();
|
||||
leaveIdlePowerSave();
|
||||
Log::event("POWER", "button wake consumed; next press will perform the action");
|
||||
} else if (static_cast<Role>(settings_.role) == Role::SLAVE) {
|
||||
leaveIdlePowerSave();
|
||||
Log::event("POWER", "optical input woke Slave");
|
||||
}
|
||||
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() {
|
||||
@@ -815,12 +1024,16 @@ void App::printConfiguration() {
|
||||
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);
|
||||
Serial.printf("Test %lu..%lu Hz (adjacent exact frequencies), accuracy %.2f%%, %lums, duty %u%%\n",
|
||||
params_.startHz, params_.endHz, params_.accuracyPct, params_.testTimeMs, params_.dutyPct);
|
||||
stageCount_ = frequencyPointCount(params_.startHz, params_.endHz);
|
||||
Serial.printf("Frequencies (%lu): ", stageCount_);
|
||||
for (uint32_t i = 0; i < stageCount_; ++i) Serial.printf("%lu%s", frequencyAt(params_.startHz, params_.endHz, i), i + 1 == stageCount_ ? "\n" : ",");
|
||||
Serial.printf("ALL nominal: %llu us | RX=%s\n", actualNominalTotalUs(), receiver_.highRateBackend() ? "RMT DMA" : "RMT ping-pong");
|
||||
Serial.printf("Test %lu Hz, pulse %lu..%lu ns, accuracy %.2f%%, %lums, RX 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() {
|
||||
@@ -833,28 +1046,32 @@ uint64_t App::actualNominalTotalUs() {
|
||||
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 float measuredDuty = 100.0f * s.activeSum / 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 %s periods=%lu measured=%s duty=%.2f%% skipped=%lu%s%s",
|
||||
requestedText, status, s.periods, measuredText, measuredDuty, s.droppedItems,
|
||||
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];
|
||||
char target[12]; Display::formatTestFrequency(actual_.actualHz, target, sizeof(target));
|
||||
formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
|
||||
if (s.reason != FailReason::NONE) {
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, actual_.actualDutyPct);
|
||||
formatFailure(s.reason, requestedHz_, requestedPulseNs_, one, sizeof(one));
|
||||
if (s.reason == FailReason::PERIOD_OUT && s.badFrequency > 0.0f) {
|
||||
char frequency[12];
|
||||
Display::formatTestFrequency(static_cast<uint32_t>(lroundf(s.badFrequency)), frequency, sizeof(frequency));
|
||||
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 duty[10]; formatErrorDuty(s.badDuty, duty, sizeof(duty));
|
||||
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, duty);
|
||||
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));
|
||||
}
|
||||
@@ -862,17 +1079,16 @@ void App::showStageResult(const StageStats &s) {
|
||||
overallProgressTotal(stageCount_), roleCorner(static_cast<Role>(settings_.role)));
|
||||
return;
|
||||
}
|
||||
char stage[12]; snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_);
|
||||
snprintf(one, sizeof(one), UiText::TEST_FORMAT, target, actual_.actualDutyPct, stage);
|
||||
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 float measuredDuty = 100.0f * s.activeSum / s.periodSum;
|
||||
char frequency[12]; Display::formatFrequency(measuredHz, frequency, sizeof(frequency));
|
||||
snprintf(two, sizeof(two), "F:%-8s D:%4.1f%%", frequency, measuredDuty);
|
||||
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_));
|
||||
}
|
||||
@@ -880,31 +1096,25 @@ void App::showStageResult(const StageStats &s) {
|
||||
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 target[12], one[64], two[64];
|
||||
Display::formatTestFrequency(packet.actualHz ? packet.actualHz : packet.requestedHz,
|
||||
target, sizeof(target));
|
||||
char one[64], two[64];
|
||||
formatTestTarget(packet.requestedHz, packet.requestedPulseNs, one, sizeof(one));
|
||||
if (reason == FailReason::NONE) {
|
||||
char stage[12]; snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_);
|
||||
snprintf(one, sizeof(one), UiText::TEST_FORMAT,
|
||||
target, packet.actualDutyX100 / 100.0f, stage);
|
||||
if (packet.measuredHzX10) {
|
||||
char measured[12];
|
||||
Display::formatFrequency(packet.measuredHzX10 / 10.0f, measured, sizeof(measured));
|
||||
snprintf(two, sizeof(two), "F:%-8s D:%4.1f%%", measured, packet.measuredDutyX10 / 10.0f);
|
||||
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) {
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
|
||||
char measured[12];
|
||||
Display::formatTestFrequency((packet.measuredHzX10 + 5U) / 10U, measured, sizeof(measured));
|
||||
snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, measured);
|
||||
} else if (reason == FailReason::DUTY_OUT && packet.measuredDutyX10) {
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
|
||||
char duty[10]; formatErrorDuty(packet.measuredDutyX10 / 10.0f, duty, sizeof(duty));
|
||||
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, duty);
|
||||
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(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
|
||||
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)));
|
||||
@@ -918,16 +1128,15 @@ void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) co
|
||||
stats.badFrequency > 0.0f;
|
||||
const float measuredHz = badPeriod ? stats.badFrequency :
|
||||
static_cast<float>(receiver_.tickHz()) * stats.periods / stats.periodSum;
|
||||
const float measuredDuty = badPeriod ? stats.badDuty : 100.0f * stats.activeSum / stats.periodSum;
|
||||
packet.measuredHzX10 = static_cast<uint32_t>(lroundf(measuredHz * 10.0f));
|
||||
packet.measuredDutyX10 = static_cast<uint16_t>(lroundf(measuredDuty * 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() {
|
||||
char target[12], one[64], stage[12];
|
||||
Display::formatTestFrequency(actual_.actualHz, target, sizeof(target));
|
||||
snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_);
|
||||
snprintf(one, sizeof(one), UiText::TEST_FORMAT, target, actual_.actualDutyPct, stage);
|
||||
char one[64];
|
||||
formatTestTarget(requestedHz_, requestedPulseNs_, one, sizeof(one));
|
||||
display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, 0),
|
||||
overallProgressTotal(stageCount_));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user