Улучшения

- улушчено отображение на OLED
- убраны настройки шага частоты и количества повторов
- сделан перебор только реализуемых частот
- увеличена точность, на 1МГц 1.25%, в остальных до 1%
- точное измерение TOTAL TIME
This commit is contained in:
2026-08-08 10:18:50 +03:00
parent a8099bf2b8
commit 21f8fe8e13
12 changed files with 293 additions and 151 deletions

View File

@@ -26,6 +26,27 @@ void formatErrorDuty(float duty, char *out, size_t size) {
if (fabsf(duty - roundf(duty)) < 0.05f) snprintf(out, size, "%.0f%%", duty); if (fabsf(duty - roundf(duty)) < 0.05f) snprintf(out, size, "%.0f%%", duty);
else snprintf(out, size, "%.1f%%", duty); else snprintf(out, size, "%.1f%%", duty);
} }
void formatMenuLine(const char *label, const char *value, char *out, size_t size) {
constexpr size_t OLED_TEXT_COLUMNS = 21;
const size_t valueLength = strlen(value);
const int labelWidth = static_cast<int>(
valueLength < OLED_TEXT_COLUMNS ? OLED_TEXT_COLUMNS - valueLength : 1U);
snprintf(out, size, "%-*s%s", labelWidth, label, value);
}
uint32_t overallProgress(uint32_t stageIndex, uint8_t step) {
if (step > MEASUREMENT_PROGRESS_STEPS) step = MEASUREMENT_PROGRESS_STEPS;
return stageIndex * MEASUREMENT_PROGRESS_STEPS + step;
}
uint32_t overallProgressTotal(uint32_t stageCount) {
return stageCount * MEASUREMENT_PROGRESS_STEPS;
}
uint32_t stageWallTimeMs(uint32_t testTimeMs, uint32_t frequencyHz) {
return static_cast<uint32_t>((nominalStageUs(frequencyHz, testTimeMs, PWM_SETTLE_CYCLES) + 999ULL) / 1000ULL);
}
} }
App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE), measurement_(receiver_) {} App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE), measurement_(receiver_) {}
@@ -103,7 +124,7 @@ void App::update() {
} }
if (state_ == AppState::MENU) { if (state_ == AppState::MENU) {
if (modeEvent == ButtonEvent::SHORT) { if (modeEvent == ButtonEvent::SHORT) {
menuItem_ = (menuItem_ + 1U) % 7U; 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) { else if (modeEvent == ButtonEvent::LONG) {
sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_); sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_);
@@ -145,24 +166,18 @@ void App::showIdle() {
} }
void App::sanitizeRange() { void App::sanitizeRange() {
settings_.startIndex %= countOf(START_FREQ_OPTIONS_HZ); settings_.endIndex %= countOf(END_FREQ_OPTIONS_HZ); settings_.startIndex %= countOf(START_FREQ_OPTIONS_HZ);
if (END_FREQ_OPTIONS_HZ[settings_.endIndex] <= START_FREQ_OPTIONS_HZ[settings_.startIndex]) { settings_.endIndex %= countOf(END_FREQ_OPTIONS_HZ);
size_t i = 0;
while (i < countOf(END_FREQ_OPTIONS_HZ) && END_FREQ_OPTIONS_HZ[i] <= START_FREQ_OPTIONS_HZ[settings_.startIndex]) ++i;
if (i == countOf(END_FREQ_OPTIONS_HZ)) { settings_.startIndex = 0; i = countOf(END_FREQ_OPTIONS_HZ) - 1; }
settings_.endIndex = i;
}
} }
void App::changeMenu(int d) { void App::changeMenu(int d) {
sanitizeRange();
uint8_t *value = nullptr; size_t count = 0; uint8_t *value = nullptr; size_t count = 0;
switch (menuItem_) { switch (menuItem_) {
case 0: value = &settings_.startIndex; count = countOf(START_FREQ_OPTIONS_HZ); break; 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 1: value = &settings_.endIndex; count = countOf(END_FREQ_OPTIONS_HZ); break;
case 2: value = &settings_.stepIndex; count = countOf(STEP_OPTIONS_HZ); break; case 2: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break;
case 3: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break; case 3: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
case 4: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
case 5: value = &settings_.repeatIndex; count = countOf(REPEAT_OPTIONS); break;
default: value = &settings_.dutyIndex; count = countOf(DUTY_OPTIONS_PCT); break; default: value = &settings_.dutyIndex; count = countOf(DUTY_OPTIONS_PCT); break;
} }
*value = static_cast<uint8_t>((*value + count + d) % count); *value = static_cast<uint8_t>((*value + count + d) % count);
@@ -171,34 +186,51 @@ void App::changeMenu(int d) {
} }
void App::showMenu() { void App::showMenu() {
char one[22], two[22], all[12]; char one[22], value[12], total[22], all[12];
const char *label = nullptr;
Display::formatDuration(actualNominalTotalUs(), all, sizeof(all)); Display::formatDuration(actualNominalTotalUs(), all, sizeof(all));
switch (menuItem_) { switch (menuItem_) {
case 0: snprintf(one, sizeof(one), "START FREQ"); Display::formatFrequency(params_.startHz, two, sizeof(two)); break; case 0:
case 1: snprintf(one, sizeof(one), "END FREQ"); Display::formatFrequency(params_.endHz, two, sizeof(two)); break; Display::formatTestFrequency(params_.startHz, value, sizeof(value));
case 2: snprintf(one, sizeof(one), "FREQ STEP"); Display::formatFrequency(params_.stepHz, two, sizeof(two)); break; strncat(value, " Hz", sizeof(value) - strlen(value) - 1U);
case 3: snprintf(one, sizeof(one), "ACCURACY"); snprintf(two, sizeof(two), "+/-%g%%", params_.accuracyPct); break; label = "START FREQ:";
case 4: snprintf(one, sizeof(one), "TEST TIME"); snprintf(two, sizeof(two), "%.1fs", params_.testTimeMs / 1000.0f); break; break;
case 5: snprintf(one, sizeof(one), "REPEATS"); snprintf(two, sizeof(two), "%ux", params_.repeats); break; case 1:
default: snprintf(one, sizeof(one), "PWM DUTY"); snprintf(two, sizeof(two), "%u%%", params_.dutyPct); break; Display::formatTestFrequency(params_.endHz, value, sizeof(value));
strncat(value, " Hz", sizeof(value) - strlen(value) - 1U);
label = "END FREQ:";
break;
case 2:
snprintf(value, sizeof(value), "+/-%g%%", params_.accuracyPct);
label = "ACCURACY:";
break;
case 3:
snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f);
label = "TEST TIME:";
break;
default:
snprintf(value, sizeof(value), "%u%%", params_.dutyPct);
label = "PWM DUTY:";
break;
} }
const size_t used = strlen(two); snprintf(two + used, sizeof(two) - used, " ALL %s", all); display_.show(one, two); formatMenuLine(label, value, one, sizeof(one));
formatMenuLine("TOTAL TIME:", all, total, sizeof(total));
display_.show(one, total);
} }
void App::startTest() { void App::startTest() {
params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz); params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz);
stageIndex_ = 0; requestedHz_ = 0; pendingReason_ = FailReason::NONE; stageIndex_ = 0; requestedHz_ = 0; pendingReason_ = FailReason::NONE;
havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0;
if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; } if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; }
Log::printf("TEST", "starting role=%s stages=%lu", roleName(static_cast<Role>(settings_.role)), stageCount_); Log::printf("TEST", "starting role=%s stages=%lu", roleName(static_cast<Role>(settings_.role)), stageCount_);
if (SERIAL_MINIMAL_LOG) { if (SERIAL_MINIMAL_LOG) {
char startText[12], endText[12], stepText[12]; char startText[12], endText[12];
Display::formatFrequency(params_.startHz, startText, sizeof(startText)); Display::formatFrequency(params_.startHz, startText, sizeof(startText));
Display::formatFrequency(params_.endHz, endText, sizeof(endText)); Display::formatFrequency(params_.endHz, endText, sizeof(endText));
Display::formatFrequency(params_.stepHz, stepText, sizeof(stepText)); Log::printf("CONFIG", "mode=%s range=%s..%s adjacent accuracy=%.2f%% time=%lums duty=%u%% stages=%lu",
Log::printf("CONFIG", "mode=%s range=%s..%s step=%s accuracy=%.2f%% time=%lums repeats=%u duty=%u%% stages=%lu", roleName(static_cast<Role>(settings_.role)), startText, endText,
roleName(static_cast<Role>(settings_.role)), startText, endText, stepText, params_.accuracyPct, params_.testTimeMs, params_.dutyPct, stageCount_);
params_.accuracyPct, params_.testTimeMs, params_.repeats, params_.dutyPct, stageCount_);
} }
printConfiguration(); printConfiguration();
const Role role = static_cast<Role>(settings_.role); const Role role = static_cast<Role>(settings_.role);
@@ -212,7 +244,7 @@ void App::startTest() {
bool App::armSlave(bool preserveDisplay) { bool App::armSlave(bool preserveDisplay) {
params_ = store_.params(settings_); params_ = store_.params(settings_);
stageIndex_ = 0; stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz); stageIndex_ = 0; stageCount_ = frequencyPointCount(params_.startHz, params_.endHz);
requestedHz_ = 0; session_ = 0; sequence_ = 0; havePeer_ = false; requestedHz_ = 0; session_ = 0; sequence_ = 0; havePeer_ = false;
lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; retries_ = 0; slaveRearmAtMs_ = 0; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; retries_ = 0; slaveRearmAtMs_ = 0;
if (!radio_.begin()) { if (!radio_.begin()) {
@@ -228,7 +260,7 @@ bool App::armSlave(bool preserveDisplay) {
} }
bool App::prepareStage(bool showProgress) { bool App::prepareStage(bool showProgress) {
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_); requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_);
actual_ = {}; actual_ = {};
const uint32_t maxHz = TARGET_IS_C3 ? C3_STRICT_MAX_HZ : const uint32_t maxHz = TARGET_IS_C3 ? C3_STRICT_MAX_HZ :
(receiver_.highRateBackend() ? S3_STRICT_MAX_HZ : C3_STRICT_MAX_HZ); (receiver_.highRateBackend() ? S3_STRICT_MAX_HZ : C3_STRICT_MAX_HZ);
@@ -239,11 +271,13 @@ bool App::prepareStage(bool showProgress) {
GPIO_PWM, requestedHz_); GPIO_PWM, requestedHz_);
finish(false, FailReason::RESOLUTION); return false; finish(false, FailReason::RESOLUTION); return false;
} }
const uint32_t plannedRxHz = receiver_.plannedTickHz(actual_.actualHz, actual_.actualDutyPct);
const FailReason resolution = validateResolution(actual_.actualHz, actual_.actualDutyPct, params_.accuracyPct, const FailReason resolution = validateResolution(actual_.actualHz, actual_.actualDutyPct, params_.accuracyPct,
receiver_.tickHz(), actual_.bits); plannedRxHz, actual_.bits);
if (resolution != FailReason::NONE) { if (resolution != FailReason::NONE) {
Log::printf("PWM", "resolution rejected: actual=%luHz duty=%.3f%% bits=%u RXclock=%luHz tolerance=%.3f%%", Log::printf("PWM", "resolution rejected: actual=%luHz duty=%.3f%% bits=%u RXclock=%luHz tolerance=%.3f%%",
actual_.actualHz, actual_.actualDutyPct, actual_.bits, receiver_.tickHz(), params_.accuracyPct); actual_.actualHz, actual_.actualDutyPct, actual_.bits, plannedRxHz,
effectiveTolerancePct(params_.accuracyPct));
finish(false, resolution); return false; finish(false, resolution); return false;
} }
Log::printf("PWM", "stage=%lu/%lu requested=%luHz actual=%luHz duty=%.2f%% bits=%u STARTED", Log::printf("PWM", "stage=%lu/%lu requested=%luHz actual=%luHz duty=%.2f%% bits=%u STARTED",
@@ -256,9 +290,10 @@ bool App::prepareStage(bool showProgress) {
} }
bool App::startLocalMeasurement(float hz, float duty) { bool App::startLocalMeasurement(float hz, float duty) {
Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% settle=%u cycles window=%lums x%u; per-pulse logging suspended", Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% RX=%luHz settle=%u cycles window=%lums; per-pulse logging suspended",
hz, duty, params_.accuracyPct, PWM_SETTLE_CYCLES, params_.testTimeMs, params_.repeats); hz, duty, effectiveTolerancePct(params_.accuracyPct), receiver_.plannedTickHz(static_cast<uint32_t>(hz + 0.5f), duty),
const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs, params_.repeats, PWM_SETTLE_CYCLES); PWM_SETTLE_CYCLES, params_.testTimeMs);
const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs, PWM_SETTLE_CYCLES);
Log::printf("MEASURE", "receiver start %s, RMT chunk=%u symbols", ok ? "OK" : "FAILED", Log::printf("MEASURE", "receiver start %s, RMT chunk=%u symbols", ok ? "OK" : "FAILED",
receiver_.receiveChunkSymbols()); receiver_.receiveChunkSymbols());
return ok; return ok;
@@ -270,7 +305,7 @@ void App::stagePassed() {
if (++stageIndex_ >= stageCount_) { finish(true, FailReason::NONE); return; } if (++stageIndex_ >= stageCount_) { finish(true, FailReason::NONE); return; }
if (static_cast<Role>(settings_.role) == Role::SOLO) { if (prepareStage()) state_ = AppState::SOLO_MEASURE; } if (static_cast<Role>(settings_.role) == Role::SOLO) { if (prepareStage()) state_ = AppState::SOLO_MEASURE; }
else if (static_cast<Role>(settings_.role) == Role::MASTER) { else if (static_cast<Role>(settings_.role) == Role::MASTER) {
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_); requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_);
actual_ = {}; actual_ = {};
stageStartConfirmed_ = false; stageStartConfirmed_ = false;
pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE); pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE);
@@ -294,7 +329,7 @@ ProtocolPacket App::makePacket(MessageType type) const {
p.requestedHz = requestedHz_; p.actualHz = actual_.actualHz; p.requestedHz = requestedHz_; p.actualHz = actual_.actualHz;
const float packetDuty = actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct; const float packetDuty = actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct;
p.actualDutyX100 = static_cast<uint16_t>(packetDuty * 100.0f + 0.5f); p.actualDutyX100 = static_cast<uint16_t>(packetDuty * 100.0f + 0.5f);
p.testTimeMs = params_.testTimeMs; p.repeats = params_.repeats; p.testTimeMs = params_.testTimeMs;
p.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f); p.settleCycles = PWM_SETTLE_CYCLES; p.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f); p.settleCycles = PWM_SETTLE_CYCLES;
return p; return p;
} }
@@ -349,7 +384,7 @@ void App::handleRadio() {
} }
if (state_ == AppState::MASTER_DISCOVER && type == MessageType::DISCOVER_ACK && r.packet.session == session_) { if (state_ == AppState::MASTER_DISCOVER && type == MessageType::DISCOVER_ACK && r.packet.session == session_) {
memcpy(peer_, r.mac, 6); havePeer_ = true; lastPeerSeenMs_ = lastHeartbeatMs_ = millis(); memcpy(peer_, r.mac, 6); havePeer_ = true; lastPeerSeenMs_ = lastHeartbeatMs_ = millis();
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_); requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_);
sendCurrent(MessageType::PREPARE); state_ = AppState::MASTER_WAIT_READY; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS; 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; char mac[20]; Radio::macText(peer_, mac, sizeof(mac)); Log::printf("ESP-NOW", "Slave selected %s", mac); continue;
} }
@@ -374,7 +409,7 @@ void App::handleRadio() {
stageIndex_ = r.packet.stage; stageIndex_ = r.packet.stage;
sequence_ = r.packet.sequence; sequence_ = r.packet.sequence;
state_ = AppState::SLAVE_WAIT_START; state_ = AppState::SLAVE_WAIT_START;
params_.testTimeMs = r.packet.testTimeMs; params_.repeats = r.packet.repeats; 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;
stageCount_ = r.packet.stageCount; stageCount_ = r.packet.stageCount;
actual_ = {}; actual_ = {};
@@ -402,11 +437,12 @@ void App::handleRadio() {
r.packet.sequence == pendingPacket_.sequence) { r.packet.sequence == pendingPacket_.sequence) {
if (!stageStartConfirmed_) showStageProgress(); if (!stageStartConfirmed_) showStageProgress();
stageStartConfirmed_ = true; stageStartConfirmed_ = true;
deadlineMs_ = millis() + params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS + deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) +
(1000UL * PWM_SETTLE_CYCLES / actual_.actualHz) + 20; LINK_REPLY_TIMEOUT_MS + 20;
} else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::PROGRESS) { } else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::PROGRESS) {
stageStartConfirmed_ = true; stageStartConfirmed_ = true;
deadlineMs_ = millis() + params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS; deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) +
LINK_REPLY_TIMEOUT_MS;
showRemoteResult(r.packet); showRemoteResult(r.packet);
} else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::RESULT) { } else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::RESULT) {
ProtocolPacket ack = makePacket(MessageType::ACK); ack.sequence = r.packet.sequence; ProtocolPacket ack = makePacket(MessageType::ACK); ack.sequence = r.packet.sequence;
@@ -428,7 +464,8 @@ void App::handleRadio() {
actual_.actualHz = r.packet.actualHz; actual_.actualDutyPct = r.packet.actualDutyX100 / 100.0f; actual_.actualHz = r.packet.actualHz; actual_.actualDutyPct = r.packet.actualDutyX100 / 100.0f;
if (!startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { finish(false, FailReason::UNSUPPORTED); continue; } if (!startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { finish(false, FailReason::UNSUPPORTED); continue; }
showStageProgress(); showStageProgress();
state_ = AppState::SLAVE_MEASURE; deadlineMs_ = millis() + params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS; state_ = AppState::SLAVE_MEASURE;
deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) + LINK_REPLY_TIMEOUT_MS;
ProtocolPacket started = makePacket(MessageType::READY); ProtocolPacket started = makePacket(MessageType::READY);
started.sequence = r.packet.sequence; sendLinked(started); started.sequence = r.packet.sequence; sendLinked(started);
} else if (state_ == AppState::SLAVE_MEASURE && type == MessageType::START_STAGE) { } else if (state_ == AppState::SLAVE_MEASURE && type == MessageType::START_STAGE) {
@@ -475,7 +512,7 @@ void App::updateMaster() {
Log::printf("ESP-NOW", "%s retry=%u", messageName(static_cast<MessageType>(pendingPacket_.type)), retries_ + 1); Log::printf("ESP-NOW", "%s retry=%u", messageName(static_cast<MessageType>(pendingPacket_.type)), retries_ + 1);
sendLinked(pendingPacket_); ++retries_; sendLinked(pendingPacket_); ++retries_;
deadlineMs_ = now + (state_ == AppState::MASTER_WAIT_RESULT ? deadlineMs_ = now + (state_ == AppState::MASTER_WAIT_RESULT ?
(stageStartConfirmed_ ? params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS : LINK_RETRY_INTERVAL_MS) : (stageStartConfirmed_ ? stageWallTimeMs(params_.testTimeMs, actual_.actualHz) + LINK_REPLY_TIMEOUT_MS : LINK_RETRY_INTERVAL_MS) :
LINK_REPLY_TIMEOUT_MS); LINK_REPLY_TIMEOUT_MS);
} }
@@ -488,10 +525,9 @@ void App::updateSlave() {
StageStats live = {}; StageStats live = {};
if (measurement_.statsSnapshot(live)) { if (measurement_.statsSnapshot(live)) {
ProtocolPacket progress = makePacket(MessageType::PROGRESS); ProtocolPacket progress = makePacket(MessageType::PROGRESS);
progress.progressStep = measurement_.progressStep();
fillMeasuredResult(progress, live); fillMeasuredResult(progress, live);
progress.sequence = sequence_; sendLinked(progress); progress.sequence = sequence_; sendLinked(progress);
// oled.display() is synchronous. Resume capture only after the full
// framebuffer has reached the display.
showStageResult(live); showStageResult(live);
} }
measurement_.continueAfterDisplay(); measurement_.continueAfterDisplay();
@@ -503,6 +539,8 @@ void App::updateSlave() {
printStageStats(measurement_.stats(), actual_.actualHz); printStageStats(measurement_.stats(), actual_.actualHz);
showStageResult(measurement_.stats()); showStageResult(measurement_.stats());
pendingPacket_ = makePacket(MessageType::RESULT); pendingPacket_ = makePacket(MessageType::RESULT);
pendingPacket_.progressStep = ms == MeasureState::PASS ?
MEASUREMENT_PROGRESS_STEPS : measurement_.progressStep();
pendingPacket_.passed = ms == MeasureState::PASS && measurement_.reason() == FailReason::NONE; pendingPacket_.passed = ms == MeasureState::PASS && measurement_.reason() == FailReason::NONE;
pendingPacket_.reason = static_cast<uint8_t>(measurement_.reason()); pendingPacket_.periods = measurement_.stats().periods; pendingPacket_.reason = static_cast<uint8_t>(measurement_.reason()); pendingPacket_.periods = measurement_.stats().periods;
fillMeasuredResult(pendingPacket_, measurement_.stats()); fillMeasuredResult(pendingPacket_, measurement_.stats());
@@ -567,7 +605,7 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
if (pass) { if (pass) {
const Role role = static_cast<Role>(settings_.role); const Role role = static_cast<Role>(settings_.role);
snprintf(one, sizeof(one), "%s PASS", roleName(role)); snprintf(one, sizeof(one), "%s PASS", roleName(role));
display_.show(one, role == Role::SLAVE ? "WAIT MASTER" : "START=REPEAT"); display_.show(one, role == Role::SLAVE ? "WAIT MASTER" : "START=AGAIN");
} }
else if (requestedHz_) { else if (requestedHz_) {
char frequency[12]; char frequency[12];
@@ -588,11 +626,11 @@ 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("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, 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); GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL);
Serial.printf("Test %lu..%lu step %lu Hz, accuracy %.2f%%, %lums x%u, duty %u%%\n", Serial.printf("Test %lu..%lu Hz (adjacent exact frequencies), accuracy %.2f%%, %lums, duty %u%%\n",
params_.startHz, params_.endHz, params_.stepHz, params_.accuracyPct, params_.testTimeMs, params_.repeats, params_.dutyPct); params_.startHz, params_.endHz, params_.accuracyPct, params_.testTimeMs, params_.dutyPct);
stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz);
Serial.printf("Frequencies (%lu): ", stageCount_); Serial.printf("Frequencies (%lu): ", stageCount_);
for (uint32_t i = 0; i < stageCount_; ++i) Serial.printf("%lu%s", frequencyAt(params_.startHz, params_.endHz, params_.stepHz, i), i + 1 == stageCount_ ? "\n" : ","); 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("ALL nominal: %llu us | RX=%s\n", actualNominalTotalUs(), receiver_.highRateBackend() ? "RMT DMA" : "RMT ping-pong");
} }
@@ -631,20 +669,23 @@ void App::showStageResult(const StageStats &s) {
} else { } else {
snprintf(two, sizeof(two), "%s", failName(s.reason)); snprintf(two, sizeof(two), "%s", failName(s.reason));
} }
display_.show(one, two, stageIndex_ + 1, stageCount_); display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()),
overallProgressTotal(stageCount_));
return; return;
} }
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu", snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_); target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_);
if (!s.periods || !s.periodSum) { if (!s.periods || !s.periodSum) {
display_.show(one, "F:--- D:---%", stageIndex_ + 1, stageCount_); display_.show(one, "F:--- D:---%", overallProgress(stageIndex_, measurement_.progressStep()),
overallProgressTotal(stageCount_));
return; return;
} }
const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum; const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum;
const float measuredDuty = 100.0f * s.activeSum / s.periodSum; const float measuredDuty = 100.0f * s.activeSum / s.periodSum;
char frequency[12]; Display::formatFrequency(measuredHz, frequency, sizeof(frequency)); char frequency[12]; Display::formatFrequency(measuredHz, frequency, sizeof(frequency));
snprintf(two, sizeof(two), "F:%-8s D:%4.1f%%", frequency, measuredDuty); snprintf(two, sizeof(two), "F:%-8s D:%4.1f%%", frequency, measuredDuty);
display_.show(one, two, stageIndex_ + 1, stageCount_); display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()),
overallProgressTotal(stageCount_));
} }
void App::showRemoteResult(const ProtocolPacket &packet) { void App::showRemoteResult(const ProtocolPacket &packet) {
@@ -674,7 +715,8 @@ void App::showRemoteResult(const ProtocolPacket &packet) {
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target, packet.actualDutyX100 / 100.0f); snprintf(one, sizeof(one), "FAIL %s %.0f%%", target, packet.actualDutyX100 / 100.0f);
snprintf(two, sizeof(two), "%s", failName(reason)); snprintf(two, sizeof(two), "%s", failName(reason));
} }
display_.show(one, two, stageIndex_ + 1, stageCount_); display_.show(one, two, overallProgress(stageIndex_, packet.progressStep),
overallProgressTotal(stageCount_));
} }
void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const { void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const {
@@ -695,5 +737,6 @@ void App::showStageProgress() {
Display::formatTestFrequency(actual_.actualHz, target, sizeof(target)); Display::formatTestFrequency(actual_.actualHz, target, sizeof(target));
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu", snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_); target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_);
display_.show(one, "F:--- D:---%", stageIndex_ + 1, stageCount_); display_.show(one, "F:--- D:---%", overallProgress(stageIndex_, 0),
overallProgressTotal(stageCount_));
} }

View File

@@ -57,14 +57,16 @@ constexpr uint32_t RMT_TARGET_CHUNK_US = 5000;
constexpr uint8_t RMT_QUEUE_BLOCKS = 8; constexpr uint8_t RMT_QUEUE_BLOCKS = 8;
constexpr uint16_t PERIOD_BATCH_SIZE = 128; constexpr uint16_t PERIOD_BATCH_SIZE = 128;
constexpr uint8_t MEASUREMENT_PROGRESS_STEPS = 10; constexpr uint8_t MEASUREMENT_PROGRESS_STEPS = 10;
constexpr uint32_t OLED_PROGRESS_UPDATE_MS = 15;
// Conservative sustained validation rate calibrated from real C3 logs.
constexpr uint32_t RX_PROCESSING_PERIODS_PER_SECOND = 300000;
constexpr uint32_t C3_STRICT_MAX_HZ = 1000000; constexpr uint32_t C3_STRICT_MAX_HZ = 1000000;
constexpr uint32_t S3_STRICT_MAX_HZ = 1000000; constexpr uint32_t S3_STRICT_MAX_HZ = 1000000;
// RMT stores each HIGH/LOW duration in 15 bits. At 80 MHz that limits a // RMT stores each HIGH/LOW duration in 15 bits. Select the fastest clock that
// single level to about 409 us, so even a 1 kHz signal with 50% duty cannot // still fits both levels of the current PWM signal: 20, 40 or 80 MHz.
// be captured. 20 MHz still provides 20 ticks at 1 MHz (5% resolution), constexpr uint32_t CAPTURE_RESOLUTION_OPTIONS_HZ[] = {20000000, 40000000, 80000000};
// while allowing level durations up to about 1.64 ms for the 1 kHz/90% case. constexpr uint32_t RMT_MAX_LEVEL_TICKS = 32766;
constexpr uint32_t CAPTURE_RESOLUTION_HZ = 20000000;
// Arduino-ESP32 uses the 40 MHz crystal as the default LEDC clock on C3/S3. // Arduino-ESP32 uses the 40 MHz crystal as the default LEDC clock on C3/S3.
// Keep this explicit so the resolution calculation never asks LEDC for an // Keep this explicit so the resolution calculation never asks LEDC for an
// impossible frequency/resolution combination. // impossible frequency/resolution combination.
@@ -73,12 +75,21 @@ constexpr uint8_t LEDC_CHANNEL = 0;
constexpr uint8_t LEDC_MAX_BITS = 14; constexpr uint8_t LEDC_MAX_BITS = 14;
// -------------------------- Menu value arrays ----------------------------- // -------------------------- Menu value arrays -----------------------------
constexpr uint32_t START_FREQ_OPTIONS_HZ[] = {1000, 2000, 5000, 10000, 20000, 50000}; // START and END deliberately have separate, independently cycling menu lists.
constexpr uint32_t END_FREQ_OPTIONS_HZ[] = {100000, 200000, 500000, 750000, 1000000}; // Every value is exactly achievable from the 40 MHz XTAL with an integer LEDC
constexpr uint32_t STEP_OPTIONS_HZ[] = {1000, 2000, 5000, 10000, 20000, 50000, 100000}; // divider. The test itself walks TEST_FREQUENCIES_HZ between the selected
// endpoints, so there is no separately configurable step.
constexpr uint32_t START_FREQ_OPTIONS_HZ[] = {1000, 10000, 100000};
constexpr uint32_t END_FREQ_OPTIONS_HZ[] = {100000, 500000, 1000000};
// All achievable whole-number frequencies in the supported 1 kHz..1 MHz
// range, used for adjacent test stages rather than direct menu selection.
constexpr uint32_t TEST_FREQUENCIES_HZ[] = {
1000, 2000, 5000, 10000, 25000, 50000,
100000, 200000, 312500, 400000, 500000, 625000, 800000, 1000000
};
constexpr float ACCURACY_OPTIONS_PCT[] = {1.0f, 2.0f, 5.0f, 10.0f}; constexpr float ACCURACY_OPTIONS_PCT[] = {1.0f, 2.0f, 5.0f, 10.0f};
constexpr uint32_t TEST_TIME_OPTIONS_MS[] = {100, 250, 500, 1000, 2000, 5000}; constexpr uint32_t TEST_TIME_OPTIONS_MS[] = {100, 250, 500, 1000, 2000, 5000};
constexpr uint8_t REPEAT_OPTIONS[] = {1, 2, 3, 5, 10};
constexpr uint8_t DUTY_OPTIONS_PCT[] = {10, 25, 50, 75, 90}; constexpr uint8_t DUTY_OPTIONS_PCT[] = {10, 25, 50, 75, 90};
template <typename T, size_t N> constexpr size_t countOf(const T (&)[N]) { return N; } template <typename T, size_t N> constexpr size_t countOf(const T (&)[N]) { return N; }

View File

@@ -1,4 +1,5 @@
#include "Core.h" #include "Core.h"
#include "Config.h"
#include <math.h> #include <math.h>
#include <string.h> #include <string.h>
@@ -30,28 +31,52 @@ uint32_t settingsChecksum(const Settings &s) {
return hash; return hash;
} }
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz, uint32_t stepHz) { uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz) {
if (!startHz || !stepHz || endHz <= startHz) return 0; if (!startHz || endHz <= startHz) return 0;
const uint64_t span = static_cast<uint64_t>(endHz) - startHz; uint32_t count = 0;
return static_cast<uint32_t>(span / stepHz + 1U + ((span % stepHz) ? 1U : 0U)); for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i)
if (TEST_FREQUENCIES_HZ[i] >= startHz && TEST_FREQUENCIES_HZ[i] <= endHz) ++count;
return count;
} }
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t stepHz, uint32_t index) { uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t index) {
const uint32_t count = frequencyPointCount(startHz, endHz, stepHz); for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i) {
if (!count || index >= count) return 0; const uint32_t frequency = TEST_FREQUENCIES_HZ[i];
if (index == count - 1) return endHz; if (frequency < startHz || frequency > endHz) continue;
const uint64_t v = static_cast<uint64_t>(startHz) + static_cast<uint64_t>(stepHz) * index; if (!index--) return frequency;
return v < endHz ? static_cast<uint32_t>(v) : endHz; }
return 0;
}
uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t settleCycles) {
if (!frequencyHz || !sampleTimeMs) return 0;
const uint64_t sampleUs = static_cast<uint64_t>(sampleTimeMs) * 1000ULL;
// At high frequency the CPU needs longer than the requested sample window
// to validate every captured period. Use the measured sustained C3 rate.
const uint64_t processingUs =
(static_cast<uint64_t>(frequencyHz) * sampleTimeMs * 1000ULL +
RX_PROCESSING_PERIODS_PER_SECOND - 1U) / RX_PROCESSING_PERIODS_PER_SECOND;
const uint64_t samplingWallUs = processingUs > sampleUs ? processingUs : sampleUs;
uint64_t chunkSymbols =
(static_cast<uint64_t>(frequencyHz) * RMT_TARGET_CHUNK_US + 999999ULL) / 1000000ULL;
if (chunkSymbols < RMT_MIN_RECEIVE_SYMBOLS) chunkSymbols = RMT_MIN_RECEIVE_SYMBOLS;
if (chunkSymbols > RMT_MAX_RECEIVE_SYMBOLS) chunkSymbols = RMT_MAX_RECEIVE_SYMBOLS;
const uint64_t batchWaitUs =
((chunkSymbols * 1000000ULL + frequencyHz - 1U) / frequencyHz) * MEASUREMENT_PROGRESS_STEPS;
const uint64_t settleUs =
(1000000ULL * settleCycles * MEASUREMENT_PROGRESS_STEPS + frequencyHz - 1U) / frequencyHz;
// Initial stage screen, nine intermediate screens and the final result.
const uint64_t displayUs = static_cast<uint64_t>(OLED_PROGRESS_UPDATE_MS) * 1000ULL *
(MEASUREMENT_PROGRESS_STEPS + 1U);
return samplingWallUs + batchWaitUs + settleUs + displayUs;
} }
uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles) { uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles) {
uint64_t total = 0; uint64_t total = 0;
const uint32_t count = frequencyPointCount(p.startHz, p.endHz, p.stepHz); const uint32_t count = frequencyPointCount(p.startHz, p.endHz);
for (uint32_t i = 0; i < count; ++i) { for (uint32_t i = 0; i < count; ++i)
const uint32_t f = frequencyAt(p.startHz, p.endHz, p.stepHz, i); total += nominalStageUs(frequencyAt(p.startHz, p.endHz, i), p.testTimeMs, settleCycles);
total += (1000000ULL * settleCycles + f - 1) / f;
total += static_cast<uint64_t>(p.testTimeMs) * 1000ULL * p.repeats;
}
return total; return total;
} }
@@ -63,6 +88,10 @@ bool dutyWithin(float measured, float expected, float tolerance) {
return fabsf(measured - expected) <= tolerance + 0.0001f; return fabsf(measured - expected) <= tolerance + 0.0001f;
} }
float effectiveTolerancePct(float configured) {
return configured > 0.0f && configured <= 1.0001f ? 1.25f : configured;
}
uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz, uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
uint8_t maxBits) { uint8_t maxBits) {
if (!frequencyHz || !sourceClockHz || !maxBits) return 0; if (!frequencyHz || !sourceClockHz || !maxBits) return 0;
@@ -148,7 +177,8 @@ FailReason validateResolution(uint32_t frequencyHz, float dutyPct, float accurac
// Measurement uses the duty actually programmed into LEDC. A coarse PWM // Measurement uses the duty actually programmed into LEDC. A coarse PWM
// step is not itself an error when the requested value (e.g. 50%) is exactly // step is not itself an error when the requested value (e.g. 50%) is exactly
// representable; only the selected value's actual quantization matters. // representable; only the selected value's actual quantization matters.
return (timerPeriodError > accuracyPct || timerDutyError > accuracyPct) const float effectiveAccuracy = effectiveTolerancePct(accuracyPct);
return (timerPeriodError > effectiveAccuracy || timerDutyError > effectiveAccuracy)
? FailReason::RESOLUTION : FailReason::NONE; ? FailReason::RESOLUTION : FailReason::NONE;
} }

View File

@@ -17,10 +17,8 @@ struct Settings {
uint8_t role; uint8_t role;
uint8_t startIndex; uint8_t startIndex;
uint8_t endIndex; uint8_t endIndex;
uint8_t stepIndex;
uint8_t accuracyIndex; uint8_t accuracyIndex;
uint8_t timeIndex; uint8_t timeIndex;
uint8_t repeatIndex;
uint8_t dutyIndex; uint8_t dutyIndex;
uint32_t checksum; uint32_t checksum;
}; };
@@ -28,10 +26,8 @@ struct Settings {
struct TestParams { struct TestParams {
uint32_t startHz; uint32_t startHz;
uint32_t endHz; uint32_t endHz;
uint32_t stepHz;
float accuracyPct; float accuracyPct;
uint32_t testTimeMs; uint32_t testTimeMs;
uint8_t repeats;
uint8_t dutyPct; uint8_t dutyPct;
}; };
@@ -72,11 +68,13 @@ struct IntegerPwmConfig {
}; };
uint32_t settingsChecksum(const Settings &s); uint32_t settingsChecksum(const Settings &s);
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz, uint32_t stepHz); uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz);
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t stepHz, uint32_t index); uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t index);
uint64_t nominalStageUs(uint32_t frequencyHz, uint32_t sampleTimeMs, uint32_t settleCycles);
uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles); uint64_t nominalTotalUs(const TestParams &p, uint32_t settleCycles);
bool periodWithin(float measuredHz, float expectedHz, float tolerancePct); bool periodWithin(float measuredHz, float expectedHz, float tolerancePct);
bool dutyWithin(float measuredPct, float expectedPct, float tolerancePct); bool dutyWithin(float measuredPct, float expectedPct, float tolerancePct);
float effectiveTolerancePct(float configuredPct);
uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz, uint8_t choosePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
uint8_t maxBits); uint8_t maxBits);
uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz, uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,

View File

@@ -9,6 +9,7 @@ Display::Display() : oled_(128, 32, &Wire, -1) {}
bool Display::begin() { bool Display::begin() {
Wire.begin(GPIO_SDA, GPIO_SCL); Wire.begin(GPIO_SDA, GPIO_SCL);
Wire.setClock(400000); // keeps a full 128x32 framebuffer update near 15 ms
// An absent optional OLED produces a large burst of ESP-IDF NACK messages. // An absent optional OLED produces a large burst of ESP-IDF NACK messages.
// Probe it once and keep the I2C driver quiet when no display is connected. // Probe it once and keep the I2C driver quiet when no display is connected.
esp_log_level_set("i2c.master", ESP_LOG_NONE); esp_log_level_set("i2c.master", ESP_LOG_NONE);
@@ -81,7 +82,8 @@ void Display::formatTestFrequency(uint32_t hz, char *out, size_t n) {
} }
void Display::formatDuration(uint64_t us, char *out, size_t n) { void Display::formatDuration(uint64_t us, char *out, size_t n) {
const uint64_t minutes = us / 60000000ULL; const uint64_t totalSeconds = (us + 999999ULL) / 1000000ULL;
if (minutes < 60) snprintf(out, n, "%02llu:%02llu", minutes, (us / 1000000ULL) % 60ULL); const uint64_t minutes = totalSeconds / 60ULL;
if (minutes < 60) snprintf(out, n, "%02llu:%02llu", minutes, totalSeconds % 60ULL);
else snprintf(out, n, "%llu:%02llu", minutes / 60ULL, minutes % 60ULL); else snprintf(out, n, "%llu:%02llu", minutes / 60ULL, minutes % 60ULL);
} }

View File

@@ -3,24 +3,26 @@
#include <string.h> #include <string.h>
bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs, bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs,
uint8_t repeats, uint8_t settleCycles) { uint8_t settleCycles) {
if (!task_ && xTaskCreate(taskEntry, "optical-rx", 4096, this, 4, &task_) != pdPASS) return false; if (!task_ && xTaskCreate(taskEntry, "optical-rx", 4096, this, 4, &task_) != pdPASS) return false;
expectedHz_ = static_cast<uint32_t>(hz + 0.5f); expectedHz_ = static_cast<uint32_t>(hz + 0.5f);
if (!expectedHz_ || !timeMs || !repeats || repeats > 10 || expectedDutyPct_ = duty;
!makePeriodLimits(expectedHz_, duty, tolerance, receiver_.tickHz(), limits_) || tolerance = effectiveTolerancePct(tolerance);
!receiver_.start(expectedHz_)) return false; if (!expectedHz_ || !timeMs ||
repeats_ = repeats; settleCycles_ = settleCycles; settleLeft_ = settleCycles; !receiver_.start(expectedHz_, expectedDutyPct_)) return false;
if (!makePeriodLimits(expectedHz_, duty, tolerance, receiver_.tickHz(), limits_)) {
receiver_.stop(); return false;
}
settleCycles_ = settleCycles; settleLeft_ = settleCycles;
stepTimeMs_ = (timeMs + MEASUREMENT_PROGRESS_STEPS - 1U) / MEASUREMENT_PROGRESS_STEPS; stepTimeMs_ = (timeMs + MEASUREMENT_PROGRESS_STEPS - 1U) / MEASUREMENT_PROGRESS_STEPS;
stepTicks_ = static_cast<uint64_t>(receiver_.tickHz()) * timeMs / stepTicks_ = static_cast<uint64_t>(receiver_.tickHz()) * timeMs /
(1000ULL * MEASUREMENT_PROGRESS_STEPS); (1000ULL * MEASUREMENT_PROGRESS_STEPS);
if (!stepTicks_) stepTicks_ = 1; if (!stepTicks_) stepTicks_ = 1;
totalSteps_ = repeats * MEASUREMENT_PROGRESS_STEPS; currentStep_ = 0;
currentStep_ = currentRepeat_ = 0; stats_.reset();
stats_.reset(); memset(repeatPeriods_, 0, sizeof(repeatPeriods_));
publishStats(); publishStats();
measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis(); measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis();
measurementStartMs_ = lastPeriodMs_ = 0; measurementStartMs_ = lastPeriodMs_ = 0;
currentRepeat_ = 0;
expectedPeriodMs_ = static_cast<uint32_t>((1000ULL + expectedHz_ - 1U) / expectedHz_); expectedPeriodMs_ = static_cast<uint32_t>((1000ULL + expectedHz_ - 1U) / expectedHz_);
if (!expectedPeriodMs_) expectedPeriodMs_ = 1; if (!expectedPeriodMs_) expectedPeriodMs_ = 1;
state_ = MeasureState::SETTLING; state_ = MeasureState::SETTLING;
@@ -45,17 +47,16 @@ void Measurement::fail(FailReason reason) {
receiver_.stop(); state_ = MeasureState::FAIL; receiver_.stop(); state_ = MeasureState::FAIL;
} }
void Measurement::completeStep() { void Measurement::completeMeasurement() {
receiver_.stop(); receiver_.stop();
stats_.droppedItems += receiver_.takeDroppedItems(); stats_.droppedItems += receiver_.takeDroppedItems();
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return; } if (receiver_.overflowed()) { fail(FailReason::GLITCH); return; }
++currentStep_;
publishStats(); publishStats();
if (currentStep_ < totalSteps_) { if (++currentStep_ < MEASUREMENT_PROGRESS_STEPS) {
state_ = MeasureState::STEP_READY; state_ = MeasureState::STEP_READY;
return; return;
} }
for (uint8_t i = 0; i < repeats_; ++i) if (!repeatPeriods_[i]) { if (!stats_.periods) {
fail(FailReason::TOO_FEW_PERIODS); return; fail(FailReason::TOO_FEW_PERIODS); return;
} }
state_ = MeasureState::PASS; state_ = MeasureState::PASS;
@@ -91,16 +92,14 @@ MeasureState Measurement::processOnce() {
if (!settleLeft_) { if (!settleLeft_) {
measurementStartTick_ = period.startTick + period.periodTicks; measurementStartTick_ = period.startTick + period.periodTicks;
deadlineTick_ = measurementStartTick_ + stepTicks_; deadlineTick_ = measurementStartTick_ + stepTicks_;
currentRepeat_ = currentStep_ / MEASUREMENT_PROGRESS_STEPS;
measurementStartMs_ = lastPeriodMs_ = millis(); state_ = MeasureState::RUNNING; measurementStartMs_ = lastPeriodMs_ = millis(); state_ = MeasureState::RUNNING;
} }
continue; continue;
} }
const uint64_t endTick = period.startTick + period.periodTicks; const uint64_t endTick = period.startTick + period.periodTicks;
if (period.startTick < measurementStartTick_) continue; // leading incomplete period if (period.startTick < measurementStartTick_) continue; // leading incomplete period
if (endTick > deadlineTick_) { completeStep(); return state_; } // trailing incomplete period if (endTick > deadlineTick_) { completeMeasurement(); return state_; } // trailing incomplete period
++repeatPeriods_[currentRepeat_]; const FailReason r = evaluatePeriodFast(period, receiver_.tickHz(), limits_, 1, stats_);
const FailReason r = evaluatePeriodFast(period, receiver_.tickHz(), limits_, currentRepeat_ + 1, stats_);
if (r != FailReason::NONE) { fail(r); return state_; } if (r != FailReason::NONE) { fail(r); return state_; }
} }
} }
@@ -113,11 +112,18 @@ MeasureState Measurement::processOnce() {
if (state_ == MeasureState::SETTLING && millis() - startedMs_ > settleTimeout) fail(FailReason::NO_SIGNAL); if (state_ == MeasureState::SETTLING && millis() - startedMs_ > settleTimeout) fail(FailReason::NO_SIGNAL);
if (state_ == MeasureState::RUNNING && measurementStartTick_) { if (state_ == MeasureState::RUNNING && measurementStartTick_) {
const uint32_t now = millis(); const uint32_t now = millis();
const uint32_t edgeTimeoutMs = expectedPeriodMs_ * NO_SIGNAL_TIMEOUT_PERIODS + 2; // RMT reports a block only after its user buffer has filled. At 1 kHz the
if (now - measurementStartMs_ < stepTimeMs_ && now - lastPeriodMs_ > edgeTimeoutMs) { // minimum 48-symbol C3 block contains roughly 48 PWM periods and therefore
// arrives much later than the old 8-period timeout. Do not call that
// normal batching delay a lost edge.
const uint32_t batchPeriods = receiver_.receiveChunkSymbols();
const uint32_t batchTimeoutMs = expectedPeriodMs_ * (batchPeriods + NO_SIGNAL_TIMEOUT_PERIODS) + 2U;
const uint32_t edgeTimeoutMs = expectedPeriodMs_ * NO_SIGNAL_TIMEOUT_PERIODS + 2U;
const uint32_t receiveTimeoutMs = batchTimeoutMs > edgeTimeoutMs ? batchTimeoutMs : edgeTimeoutMs;
if (now - measurementStartMs_ < stepTimeMs_ && now - lastPeriodMs_ > receiveTimeoutMs) {
fail(FailReason::LOST_EDGE); return state_; fail(FailReason::LOST_EDGE); return state_;
} }
if (now - measurementStartMs_ > stepTimeMs_ + expectedPeriodMs_ + 2) completeStep(); if (now - measurementStartMs_ > stepTimeMs_ + expectedPeriodMs_ + 2) completeMeasurement();
} }
return state_; return state_;
} }
@@ -126,7 +132,7 @@ MeasureState Measurement::update() { return state_; }
bool Measurement::continueAfterDisplay() { bool Measurement::continueAfterDisplay() {
if (state_ != MeasureState::STEP_READY) return false; if (state_ != MeasureState::STEP_READY) return false;
if (!receiver_.start(expectedHz_)) { if (!receiver_.start(expectedHz_, expectedDutyPct_)) {
fail(FailReason::UNSUPPORTED); fail(FailReason::UNSUPPORTED);
return false; return false;
} }

View File

@@ -7,20 +7,21 @@ class Measurement {
public: public:
explicit Measurement(PulseReceiver &receiver) : receiver_(receiver) {} explicit Measurement(PulseReceiver &receiver) : receiver_(receiver) {}
bool start(float expectedHz, float expectedDuty, float tolerancePct, bool start(float expectedHz, float expectedDuty, float tolerancePct,
uint32_t testTimeMs, uint8_t repeats, uint8_t settleCycles); uint32_t testTimeMs, uint8_t settleCycles);
MeasureState update(); MeasureState update();
bool continueAfterDisplay(); bool continueAfterDisplay();
void abort(); void abort();
MeasureState state() const { return state_; } MeasureState state() const { return state_; }
FailReason reason() const { return stats_.reason; } FailReason reason() const { return stats_.reason; }
const StageStats &stats() const { return stats_; } const StageStats &stats() const { return stats_; }
uint8_t progressStep() const { return currentStep_; }
bool statsSnapshot(StageStats &out) const; bool statsSnapshot(StageStats &out) const;
private: private:
static void taskEntry(void *context); static void taskEntry(void *context);
void taskLoop(); void taskLoop();
MeasureState processOnce(); MeasureState processOnce();
void fail(FailReason reason); void fail(FailReason reason);
void completeStep(); void completeMeasurement();
void publishStats(); void publishStats();
PulseReceiver &receiver_; PulseReceiver &receiver_;
volatile MeasureState state_ = MeasureState::IDLE; volatile MeasureState state_ = MeasureState::IDLE;
@@ -30,11 +31,11 @@ class Measurement {
mutable portMUX_TYPE statsMux_ = portMUX_INITIALIZER_UNLOCKED; mutable portMUX_TYPE statsMux_ = portMUX_INITIALIZER_UNLOCKED;
PeriodLimits limits_ = {}; PeriodLimits limits_ = {};
uint32_t expectedHz_ = 0; uint32_t expectedHz_ = 0;
uint8_t repeats_ = 0, settleCycles_ = 0, settleLeft_ = 0; float expectedDutyPct_ = 0.0f;
uint8_t currentRepeat_ = 0, currentStep_ = 0, totalSteps_ = 0; uint8_t settleCycles_ = 0, settleLeft_ = 0;
uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, stepTicks_ = 0; uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, stepTicks_ = 0;
uint32_t startedMs_ = 0, measurementStartMs_ = 0, lastPeriodMs_ = 0; uint32_t startedMs_ = 0, measurementStartMs_ = 0, lastPeriodMs_ = 0;
uint32_t stepTimeMs_ = 1, expectedPeriodMs_ = 1; uint32_t stepTimeMs_ = 1, expectedPeriodMs_ = 1;
uint32_t repeatPeriods_[10] = {}; volatile uint8_t currentStep_ = 0;
PulsePeriod periodBatch_[PERIOD_BATCH_SIZE] = {}; PulsePeriod periodBatch_[PERIOD_BATCH_SIZE] = {};
}; };

View File

@@ -2,7 +2,7 @@
#include "Core.h" #include "Core.h"
constexpr uint16_t PROTOCOL_MAGIC = 0x4F43; constexpr uint16_t PROTOCOL_MAGIC = 0x4F43;
constexpr uint8_t PROTOCOL_VERSION = 6; constexpr uint8_t PROTOCOL_VERSION = 8;
enum class MessageType : uint8_t { enum class MessageType : uint8_t {
DISCOVER, DISCOVER_ACK, PREPARE, READY, START_STAGE, RESULT, ACK, ABORT, DISCOVER, DISCOVER_ACK, PREPARE, READY, START_STAGE, RESULT, ACK, ABORT,
@@ -24,9 +24,9 @@ struct ProtocolPacket {
uint32_t actualHz; uint32_t actualHz;
uint16_t actualDutyX100; uint16_t actualDutyX100;
uint32_t testTimeMs; uint32_t testTimeMs;
uint8_t repeats;
uint16_t accuracyX100; uint16_t accuracyX100;
uint8_t settleCycles; uint8_t settleCycles;
uint8_t progressStep;
uint8_t passed; uint8_t passed;
uint8_t reason; uint8_t reason;
uint32_t periods; uint32_t periods;

View File

@@ -8,17 +8,55 @@
uint32_t PulseReceiver::tickHz() const { uint32_t PulseReceiver::tickHz() const {
#if OPTICAL_USE_RMT_DMA #if OPTICAL_USE_RMT_DMA
return CAPTURE_RESOLUTION_HZ; return captureResolutionHz_;
#else #else
return cpuTickHz_; return cpuTickHz_;
#endif #endif
} }
uint32_t PulseReceiver::plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const {
#if OPTICAL_USE_RMT_DMA
if (!expectedHz || expectedDutyPct <= 0.0f || expectedDutyPct >= 100.0f)
return CAPTURE_RESOLUTION_OPTIONS_HZ[0];
uint32_t dutyX100 = static_cast<uint32_t>(expectedDutyPct * 100.0f + 0.5f);
if (dutyX100 < 5000U) dutyX100 = 10000U - dutyX100;
for (int i = static_cast<int>(countOf(CAPTURE_RESOLUTION_OPTIONS_HZ)) - 1; i >= 0; --i) {
const uint32_t resolution = CAPTURE_RESOLUTION_OPTIONS_HZ[i];
const uint64_t levelTicksX100 = static_cast<uint64_t>(resolution) * dutyX100;
const uint64_t limitX100 = static_cast<uint64_t>(expectedHz) * 10000ULL * RMT_MAX_LEVEL_TICKS;
if (levelTicksX100 <= limitX100) return resolution;
}
return CAPTURE_RESOLUTION_OPTIONS_HZ[0];
#else
(void)expectedHz; (void)expectedDutyPct;
return cpuTickHz_;
#endif
}
bool PulseReceiver::begin() { bool PulseReceiver::begin() {
#if OPTICAL_USE_RMT_DMA #if OPTICAL_USE_RMT_DMA
queue_ = xQueueCreate(RMT_QUEUE_BLOCKS, sizeof(SymbolBlock)); queue_ = xQueueCreate(RMT_QUEUE_BLOCKS, sizeof(SymbolBlock));
return queue_ && configureRmt(CAPTURE_RESOLUTION_OPTIONS_HZ[0]);
#else
queue_ = xQueueCreate(256, sizeof(Edge));
if (!queue_) return false;
pinMode(GPIO_RX, INPUT);
cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL;
attachInterruptArg(GPIO_RX, onGpio, this, CHANGE);
return cpuTickHz_ != 0;
#endif
}
#if OPTICAL_USE_RMT_DMA
bool PulseReceiver::configureRmt(uint32_t resolutionHz) {
if (channel_ && captureResolutionHz_ == resolutionHz) return true;
stop();
if (channel_) {
if (rmt_del_channel(channel_) != ESP_OK) return false;
channel_ = nullptr;
}
rmt_rx_channel_config_t cfg = {}; rmt_rx_channel_config_t cfg = {};
cfg.clk_src = RMT_CLK_SRC_DEFAULT; cfg.resolution_hz = CAPTURE_RESOLUTION_HZ; cfg.clk_src = RMT_CLK_SRC_DEFAULT; cfg.resolution_hz = resolutionHz;
cfg.gpio_num = static_cast<gpio_num_t>(GPIO_RX); cfg.gpio_num = static_cast<gpio_num_t>(GPIO_RX);
cfg.flags.invert_in = RX_SIGNAL_INVERTED; cfg.flags.invert_in = RX_SIGNAL_INVERTED;
#if CONFIG_IDF_TARGET_ESP32S3 #if CONFIG_IDF_TARGET_ESP32S3
@@ -30,20 +68,23 @@ bool PulseReceiver::begin() {
cfg.mem_block_symbols = RMT_MIN_RECEIVE_SYMBOLS; cfg.mem_block_symbols = RMT_MIN_RECEIVE_SYMBOLS;
cfg.flags.with_dma = false; // C3 uses hardware RMT ping-pong partial reception cfg.flags.with_dma = false; // C3 uses hardware RMT ping-pong partial reception
#endif #endif
if (!queue_ || rmt_new_rx_channel(&cfg, &channel_) != ESP_OK) return false; if (rmt_new_rx_channel(&cfg, &channel_) != ESP_OK) return false;
rmt_rx_event_callbacks_t callbacks = {}; callbacks.on_recv_done = onRmt; rmt_rx_event_callbacks_t callbacks = {}; callbacks.on_recv_done = onRmt;
return rmt_rx_register_event_callbacks(channel_, &callbacks, this) == ESP_OK; if (rmt_rx_register_event_callbacks(channel_, &callbacks, this) != ESP_OK) {
#else rmt_del_channel(channel_); channel_ = nullptr; return false;
queue_ = xQueueCreate(256, sizeof(Edge)); }
if (!queue_) return false; captureResolutionHz_ = resolutionHz;
pinMode(GPIO_RX, INPUT); return true;
cpuTickHz_ = getCpuFrequencyMhz() * 1000000UL;
attachInterruptArg(GPIO_RX, onGpio, this, CHANGE);
return cpuTickHz_ != 0;
#endif
} }
#endif
bool PulseReceiver::start(uint32_t expectedHz) { bool PulseReceiver::start(uint32_t expectedHz, float expectedDutyPct) {
#if OPTICAL_USE_RMT_DMA
const uint32_t resolutionHz = plannedTickHz(expectedHz, expectedDutyPct);
if (!configureRmt(resolutionHz)) return false;
#else
(void)expectedHz; (void)expectedDutyPct;
#endif
resetStream(); resetStream();
#if OPTICAL_USE_RMT_DMA #if OPTICAL_USE_RMT_DMA
// In partial RX mode the callback is delivered when this user buffer fills. // In partial RX mode the callback is delivered when this user buffer fills.
@@ -54,19 +95,17 @@ bool PulseReceiver::start(uint32_t expectedHz) {
receiveChunkSymbols_ = static_cast<uint16_t>(symbols); receiveChunkSymbols_ = static_cast<uint16_t>(symbols);
if (rmt_enable(channel_) != ESP_OK) return false; if (rmt_enable(channel_) != ESP_OK) return false;
rmt_receive_config_t cfg = {}; rmt_receive_config_t cfg = {};
cfg.signal_range_min_ns = 1000000000UL / CAPTURE_RESOLUTION_HZ; cfg.signal_range_min_ns = 1000000000UL / captureResolutionHz_;
const uint64_t maxNs = 4000000000ULL / (expectedHz ? expectedHz : 1); const uint64_t maxNs = 4000000000ULL / (expectedHz ? expectedHz : 1);
// A duration field is 15 bits. Keep the driver's end-of-signal threshold // A duration field is 15 bits. Keep the driver's end-of-signal threshold
// strictly below that hardware limit (IDF rejects larger values). // strictly below that hardware limit (IDF rejects larger values).
const uint64_t hardwareMaxNs = 32766ULL * 1000000000ULL / CAPTURE_RESOLUTION_HZ; const uint64_t hardwareMaxNs = static_cast<uint64_t>(RMT_MAX_LEVEL_TICKS) * 1000000000ULL / captureResolutionHz_;
cfg.signal_range_max_ns = static_cast<uint32_t>(maxNs > hardwareMaxNs ? hardwareMaxNs : maxNs); cfg.signal_range_max_ns = static_cast<uint32_t>(maxNs > hardwareMaxNs ? hardwareMaxNs : maxNs);
cfg.flags.en_partial_rx = true; cfg.flags.en_partial_rx = true;
if (rmt_receive(channel_, receiveBuffer_, if (rmt_receive(channel_, receiveBuffer_,
receiveChunkSymbols_ * sizeof(receiveBuffer_[0]), &cfg) != ESP_OK) { receiveChunkSymbols_ * sizeof(receiveBuffer_[0]), &cfg) != ESP_OK) {
rmt_disable(channel_); return false; rmt_disable(channel_); return false;
} }
#else
(void)expectedHz;
#endif #endif
running_ = true; return true; running_ = true; return true;
} }

View File

@@ -15,13 +15,14 @@
class PulseReceiver { class PulseReceiver {
public: public:
bool begin(); bool begin();
bool start(uint32_t expectedHz); bool start(uint32_t expectedHz, float expectedDutyPct);
void stop(); void stop();
void resetStream(); void resetStream();
size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0); size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0);
bool overflowed(); bool overflowed();
uint32_t takeDroppedItems(); uint32_t takeDroppedItems();
uint32_t tickHz() const; uint32_t tickHz() const;
uint32_t plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const;
uint16_t receiveChunkSymbols() const { return receiveChunkSymbols_; } uint16_t receiveChunkSymbols() const { return receiveChunkSymbols_; }
bool highRateBackend() const { bool highRateBackend() const {
#if OPTICAL_USE_RMT_DMA && CONFIG_IDF_TARGET_ESP32S3 #if OPTICAL_USE_RMT_DMA && CONFIG_IDF_TARGET_ESP32S3
@@ -38,8 +39,10 @@ class PulseReceiver {
static constexpr size_t BLOCK_SYMBOLS = RMT_MAX_RECEIVE_SYMBOLS; static constexpr size_t BLOCK_SYMBOLS = RMT_MAX_RECEIVE_SYMBOLS;
struct SymbolBlock { uint16_t count; rmt_symbol_word_t symbols[BLOCK_SYMBOLS]; }; struct SymbolBlock { uint16_t count; rmt_symbol_word_t symbols[BLOCK_SYMBOLS]; };
static bool IRAM_ATTR onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *, void *); static bool IRAM_ATTR onRmt(rmt_channel_handle_t, const rmt_rx_done_event_data_t *, void *);
bool configureRmt(uint32_t resolutionHz);
bool nextRmtEdge(Edge &edge, TickType_t waitTicks); bool nextRmtEdge(Edge &edge, TickType_t waitTicks);
rmt_channel_handle_t channel_ = nullptr; rmt_channel_handle_t channel_ = nullptr;
uint32_t captureResolutionHz_ = 0;
rmt_symbol_word_t receiveBuffer_[RMT_MAX_RECEIVE_SYMBOLS]; rmt_symbol_word_t receiveBuffer_[RMT_MAX_RECEIVE_SYMBOLS];
uint16_t receiveChunkSymbols_ = 0; uint16_t receiveChunkSymbols_ = 0;
SymbolBlock isrBlock_ = {}; SymbolBlock isrBlock_ = {};

View File

@@ -3,19 +3,19 @@
#include "Log.h" #include "Log.h"
#include <Preferences.h> #include <Preferences.h>
namespace { constexpr uint16_t SETTINGS_VERSION = 1; constexpr char NAMESPACE[] = "opt-test"; } namespace { constexpr uint16_t SETTINGS_VERSION = 4; constexpr char NAMESPACE[] = "opt-test"; }
void SettingsStore::defaults(Settings &s) const { void SettingsStore::defaults(Settings &s) const {
s = {SETTINGS_VERSION, static_cast<uint8_t>(Role::SOLO), 0, 4, 1, 2, 3, 2, 2, 0}; s = {SETTINGS_VERSION, static_cast<uint8_t>(Role::SOLO), 0, 4, 2, 3, 2, 0};
s.checksum = settingsChecksum(s); s.checksum = settingsChecksum(s);
} }
bool SettingsStore::valid(const Settings &s) const { bool SettingsStore::valid(const Settings &s) const {
return s.version == SETTINGS_VERSION && s.role <= static_cast<uint8_t>(Role::SLAVE) && return s.version == SETTINGS_VERSION && s.role <= static_cast<uint8_t>(Role::SLAVE) &&
s.startIndex < countOf(START_FREQ_OPTIONS_HZ) && s.endIndex < countOf(END_FREQ_OPTIONS_HZ) && s.startIndex < countOf(START_FREQ_OPTIONS_HZ) && s.endIndex < countOf(END_FREQ_OPTIONS_HZ) &&
s.stepIndex < countOf(STEP_OPTIONS_HZ) && s.accuracyIndex < countOf(ACCURACY_OPTIONS_PCT) && s.accuracyIndex < countOf(ACCURACY_OPTIONS_PCT) &&
s.timeIndex < countOf(TEST_TIME_OPTIONS_MS) && s.repeatIndex < countOf(REPEAT_OPTIONS) && s.timeIndex < countOf(TEST_TIME_OPTIONS_MS) && s.dutyIndex < countOf(DUTY_OPTIONS_PCT) &&
s.dutyIndex < countOf(DUTY_OPTIONS_PCT) && s.checksum == settingsChecksum(s) && s.checksum == settingsChecksum(s) &&
END_FREQ_OPTIONS_HZ[s.endIndex] > START_FREQ_OPTIONS_HZ[s.startIndex]; END_FREQ_OPTIONS_HZ[s.endIndex] > START_FREQ_OPTIONS_HZ[s.startIndex];
} }
@@ -40,7 +40,6 @@ bool SettingsStore::save(Settings &s) {
TestParams SettingsStore::params(const Settings &s) const { TestParams SettingsStore::params(const Settings &s) const {
return {START_FREQ_OPTIONS_HZ[s.startIndex], END_FREQ_OPTIONS_HZ[s.endIndex], return {START_FREQ_OPTIONS_HZ[s.startIndex], END_FREQ_OPTIONS_HZ[s.endIndex],
STEP_OPTIONS_HZ[s.stepIndex], ACCURACY_OPTIONS_PCT[s.accuracyIndex], ACCURACY_OPTIONS_PCT[s.accuracyIndex], TEST_TIME_OPTIONS_MS[s.timeIndex],
TEST_TIME_OPTIONS_MS[s.timeIndex], REPEAT_OPTIONS[s.repeatIndex],
DUTY_OPTIONS_PCT[s.dutyIndex]}; DUTY_OPTIONS_PCT[s.dutyIndex]};
} }

File diff suppressed because one or more lines are too long