Compare commits
6
Commits
a8099bf2b8
...
61d3aeaba6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61d3aeaba6 | ||
|
|
34581913ff | ||
|
|
be1a90afac | ||
|
|
70fe3566b4 | ||
|
|
c952c165f8 | ||
|
|
21f8fe8e13 |
@@ -1 +1,7 @@
|
||||
/tests/
|
||||
/PCB/Project Logs*/
|
||||
/*/__Previews
|
||||
/*/History
|
||||
/*/Project Logs*/
|
||||
|
||||
|
||||
|
||||
+247
-93
@@ -1,9 +1,13 @@
|
||||
#include "App.h"
|
||||
#include "Config.h"
|
||||
#include "Config_Text.h"
|
||||
#include "Log.h"
|
||||
#include <WiFi.h>
|
||||
#include <esp_mac.h>
|
||||
#include <esp_sleep.h>
|
||||
#include <esp_system.h>
|
||||
#include <esp32-hal-cpu.h>
|
||||
#include <driver/gpio.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -26,6 +30,51 @@ 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);
|
||||
}
|
||||
|
||||
size_t utf8CharacterCount(const char *text) {
|
||||
size_t count = 0;
|
||||
while (text && *text) {
|
||||
const uint8_t byte = static_cast<uint8_t>(*text++);
|
||||
// Continuation bytes (10xxxxxx) belong to the preceding UTF-8 character.
|
||||
if ((byte & 0xC0U) != 0x80U) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const char *uiRoleName(Role role) {
|
||||
const uint8_t index = static_cast<uint8_t>(role);
|
||||
return index < sizeof(UiText::ROLE_NAMES) / sizeof(UiText::ROLE_NAMES[0])
|
||||
? UiText::ROLE_NAMES[index] : "?";
|
||||
}
|
||||
|
||||
const char *uiFailName(FailReason reason) {
|
||||
const uint8_t index = static_cast<uint8_t>(reason);
|
||||
return index < sizeof(UiText::FAIL_NAMES) / sizeof(UiText::FAIL_NAMES[0])
|
||||
? UiText::FAIL_NAMES[index] : "UNKNOWN";
|
||||
}
|
||||
|
||||
void formatMenuLine(const char *label, const char *value, char *out, size_t size) {
|
||||
constexpr size_t OLED_TEXT_COLUMNS = 21;
|
||||
const size_t labelLength = utf8CharacterCount(label);
|
||||
const size_t valueLength = utf8CharacterCount(value);
|
||||
const size_t usedColumns = labelLength + valueLength;
|
||||
const int padding = static_cast<int>(
|
||||
usedColumns < OLED_TEXT_COLUMNS ? OLED_TEXT_COLUMNS - usedColumns : 0U);
|
||||
snprintf(out, size, "%s%*s%s", label, padding, "", value);
|
||||
}
|
||||
|
||||
uint32_t overallProgress(uint32_t stageIndex, uint8_t step) {
|
||||
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_) {}
|
||||
@@ -53,12 +102,15 @@ void App::finishInitialization(bool factoryReset) {
|
||||
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");
|
||||
lastUserActivityMs_ = millis();
|
||||
setActivePerformance(false);
|
||||
printConfiguration();
|
||||
if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
|
||||
else showIdle();
|
||||
}
|
||||
|
||||
void App::update() {
|
||||
serviceIdlePowerSave();
|
||||
const uint32_t now = millis();
|
||||
const ButtonEvent startEvent = startButton_.update(now);
|
||||
const ButtonEvent modeEvent = modeButton_.update(now);
|
||||
@@ -66,6 +118,10 @@ void App::update() {
|
||||
Log::printf("INPUT", "START %s state=%s", buttonEventName(startEvent), appStateName(state_));
|
||||
if (modeEvent != ButtonEvent::NONE)
|
||||
Log::printf("INPUT", "MODE %s state=%s", buttonEventName(modeEvent), appStateName(state_));
|
||||
if (startEvent != ButtonEvent::NONE || modeEvent != ButtonEvent::NONE) {
|
||||
lastUserActivityMs_ = now;
|
||||
leaveIdlePowerSave();
|
||||
}
|
||||
if (!initialized_) {
|
||||
if (!startButton_.pressed() || !modeButton_.pressed()) finishInitialization(false);
|
||||
else if (now - bootCheckStartedMs_ >= FACTORY_RESET_HOLD_MS) finishInitialization(true);
|
||||
@@ -103,7 +159,7 @@ void App::update() {
|
||||
}
|
||||
if (state_ == AppState::MENU) {
|
||||
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) {
|
||||
sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_);
|
||||
@@ -140,29 +196,26 @@ void App::update() {
|
||||
}
|
||||
|
||||
void App::showIdle() {
|
||||
char one[24]; snprintf(one, sizeof(one), "MODE: %s", roleName(static_cast<Role>(settings_.role)));
|
||||
display_.show(one, "START=RUN");
|
||||
setActivePerformance(false);
|
||||
lastUserActivityMs_ = millis();
|
||||
char one[64]; snprintf(one, sizeof(one), "%s%s", UiText::MODE_PREFIX,
|
||||
uiRoleName(static_cast<Role>(settings_.role)));
|
||||
display_.show(one, UiText::START_RUN);
|
||||
}
|
||||
|
||||
void App::sanitizeRange() {
|
||||
settings_.startIndex %= countOf(START_FREQ_OPTIONS_HZ); settings_.endIndex %= countOf(END_FREQ_OPTIONS_HZ);
|
||||
if (END_FREQ_OPTIONS_HZ[settings_.endIndex] <= START_FREQ_OPTIONS_HZ[settings_.startIndex]) {
|
||||
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;
|
||||
}
|
||||
settings_.startIndex %= countOf(START_FREQ_OPTIONS_HZ);
|
||||
settings_.endIndex %= countOf(END_FREQ_OPTIONS_HZ);
|
||||
}
|
||||
|
||||
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_.stepIndex; count = countOf(STEP_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;
|
||||
case 5: value = &settings_.repeatIndex; count = countOf(REPEAT_OPTIONS); 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: value = &settings_.dutyIndex; count = countOf(DUTY_OPTIONS_PCT); break;
|
||||
}
|
||||
*value = static_cast<uint8_t>((*value + count + d) % count);
|
||||
@@ -171,34 +224,53 @@ void App::changeMenu(int d) {
|
||||
}
|
||||
|
||||
void App::showMenu() {
|
||||
char one[22], two[22], all[12];
|
||||
char one[64], value[24], total[64], all[12];
|
||||
const char *label = nullptr;
|
||||
Display::formatDuration(actualNominalTotalUs(), all, sizeof(all));
|
||||
switch (menuItem_) {
|
||||
case 0: snprintf(one, sizeof(one), "START FREQ"); Display::formatFrequency(params_.startHz, two, sizeof(two)); break;
|
||||
case 1: snprintf(one, sizeof(one), "END FREQ"); Display::formatFrequency(params_.endHz, two, sizeof(two)); break;
|
||||
case 2: snprintf(one, sizeof(one), "FREQ STEP"); Display::formatFrequency(params_.stepHz, two, sizeof(two)); break;
|
||||
case 3: snprintf(one, sizeof(one), "ACCURACY"); snprintf(two, sizeof(two), "+/-%g%%", params_.accuracyPct); break;
|
||||
case 4: snprintf(one, sizeof(one), "TEST TIME"); snprintf(two, sizeof(two), "%.1fs", params_.testTimeMs / 1000.0f); break;
|
||||
case 5: snprintf(one, sizeof(one), "REPEATS"); snprintf(two, sizeof(two), "%ux", params_.repeats); break;
|
||||
default: snprintf(one, sizeof(one), "PWM DUTY"); snprintf(two, sizeof(two), "%u%%", params_.dutyPct); break;
|
||||
case 0:
|
||||
Display::formatTestFrequency(params_.startHz, value, sizeof(value));
|
||||
strncat(value, UiText::FREQUENCY_UNIT, sizeof(value) - strlen(value) - 1U);
|
||||
label = UiText::MENU_START_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;
|
||||
break;
|
||||
case 2:
|
||||
snprintf(value, sizeof(value), "+/-%g%%", params_.accuracyPct);
|
||||
label = UiText::MENU_ACCURACY;
|
||||
break;
|
||||
case 3:
|
||||
snprintf(value, sizeof(value), "%.1fs", params_.testTimeMs / 1000.0f);
|
||||
label = UiText::MENU_TEST_TIME;
|
||||
break;
|
||||
default:
|
||||
snprintf(value, sizeof(value), "%u%%", params_.dutyPct);
|
||||
label = UiText::MENU_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(UiText::MENU_TOTAL_TIME, all, total, sizeof(total));
|
||||
display_.show(one, total);
|
||||
}
|
||||
|
||||
void App::startTest() {
|
||||
params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz);
|
||||
leaveIdlePowerSave();
|
||||
setActivePerformance(true);
|
||||
params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz);
|
||||
stageIndex_ = 0; requestedHz_ = 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], stepText[12];
|
||||
char startText[12], endText[12];
|
||||
Display::formatFrequency(params_.startHz, startText, sizeof(startText));
|
||||
Display::formatFrequency(params_.endHz, endText, sizeof(endText));
|
||||
Display::formatFrequency(params_.stepHz, stepText, sizeof(stepText));
|
||||
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, stepText,
|
||||
params_.accuracyPct, params_.testTimeMs, params_.repeats, params_.dutyPct, stageCount_);
|
||||
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_);
|
||||
}
|
||||
printConfiguration();
|
||||
const Role role = static_cast<Role>(settings_.role);
|
||||
@@ -207,28 +279,31 @@ void App::startTest() {
|
||||
state_ = AppState::SOLO_MEASURE;
|
||||
} else if (!radio_.begin()) finish(false, FailReason::LINK_LOST);
|
||||
else if (role == Role::MASTER) startMasterDiscovery();
|
||||
else { state_ = AppState::SLAVE_READY; Log::event("TEST", "Slave armed and waiting for Master"); display_.show("SLAVE READY", "WAIT MASTER"); }
|
||||
else { state_ = AppState::SLAVE_READY; Log::event("TEST", "Slave armed and waiting for Master"); display_.show(UiText::SLAVE_READY, UiText::WAIT_MASTER); }
|
||||
}
|
||||
|
||||
bool App::armSlave(bool preserveDisplay) {
|
||||
setActivePerformance(false);
|
||||
lastUserActivityMs_ = millis();
|
||||
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;
|
||||
lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; retries_ = 0; slaveRearmAtMs_ = 0;
|
||||
if (!radio_.begin()) {
|
||||
state_ = AppState::FINISHED; pendingReason_ = FailReason::LINK_LOST;
|
||||
slaveRearmAtMs_ = millis() + LINK_HEARTBEAT_TIMEOUT_MS;
|
||||
display_.show("LINK FAILED", "RADIO ERROR");
|
||||
display_.show(UiText::LINK_FAILED, UiText::RADIO_ERROR);
|
||||
return false;
|
||||
}
|
||||
radio_.setWindowedReceive(true);
|
||||
radio_.flush(); state_ = AppState::SLAVE_READY;
|
||||
Log::event("TEST", "Slave automatically armed and waiting for Master");
|
||||
if (!preserveDisplay) display_.show("SLAVE READY", "WAIT MASTER");
|
||||
if (!preserveDisplay) display_.show(UiText::SLAVE_READY, UiText::WAIT_MASTER);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool App::prepareStage(bool showProgress) {
|
||||
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_);
|
||||
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_);
|
||||
actual_ = {};
|
||||
const uint32_t maxHz = TARGET_IS_C3 ? C3_STRICT_MAX_HZ :
|
||||
(receiver_.highRateBackend() ? S3_STRICT_MAX_HZ : C3_STRICT_MAX_HZ);
|
||||
@@ -239,11 +314,13 @@ bool App::prepareStage(bool showProgress) {
|
||||
GPIO_PWM, requestedHz_);
|
||||
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,
|
||||
receiver_.tickHz(), actual_.bits);
|
||||
plannedRxHz, actual_.bits);
|
||||
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, receiver_.tickHz(), params_.accuracyPct);
|
||||
actual_.actualHz, actual_.actualDutyPct, actual_.bits, plannedRxHz,
|
||||
effectiveTolerancePct(params_.accuracyPct));
|
||||
finish(false, resolution); return false;
|
||||
}
|
||||
Log::printf("PWM", "stage=%lu/%lu requested=%luHz actual=%luHz duty=%.2f%% bits=%u STARTED",
|
||||
@@ -256,9 +333,10 @@ bool App::prepareStage(bool showProgress) {
|
||||
}
|
||||
|
||||
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",
|
||||
hz, duty, params_.accuracyPct, PWM_SETTLE_CYCLES, params_.testTimeMs, params_.repeats);
|
||||
const bool ok = measurement_.start(hz, duty, params_.accuracyPct, params_.testTimeMs, params_.repeats, PWM_SETTLE_CYCLES);
|
||||
Log::printf("MEASURE", "arming expected=%.3fHz duty=%.3f%% tolerance=%.3f%% RX=%luHz settle=%u cycles window=%lums; per-pulse logging suspended",
|
||||
hz, duty, effectiveTolerancePct(params_.accuracyPct), receiver_.plannedTickHz(static_cast<uint32_t>(hz + 0.5f), duty),
|
||||
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",
|
||||
receiver_.receiveChunkSymbols());
|
||||
return ok;
|
||||
@@ -270,7 +348,7 @@ void App::stagePassed() {
|
||||
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, params_.stepHz, stageIndex_);
|
||||
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_);
|
||||
actual_ = {};
|
||||
stageStartConfirmed_ = false;
|
||||
pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE);
|
||||
@@ -284,7 +362,7 @@ void App::startMasterDiscovery() {
|
||||
pendingPacket_ = makePacket(MessageType::DISCOVER); radio_.sendBroadcast(pendingPacket_);
|
||||
lastSendMs_ = millis(); retries_ = 0;
|
||||
state_ = AppState::MASTER_DISCOVER; Log::printf("ESP-NOW", "discovery started session=%08lX", session_);
|
||||
display_.show("MASTER SEARCH", "HOLD START=STOP");
|
||||
display_.show(UiText::MASTER_SEARCH, UiText::HOLD_START_STOP);
|
||||
}
|
||||
|
||||
ProtocolPacket App::makePacket(MessageType type) const {
|
||||
@@ -294,7 +372,7 @@ ProtocolPacket App::makePacket(MessageType type) const {
|
||||
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.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;
|
||||
return p;
|
||||
}
|
||||
@@ -342,14 +420,17 @@ void App::handleRadio() {
|
||||
messageName(type), r.packet.session, r.packet.stage, r.packet.sequence);
|
||||
if ((state_ == AppState::SLAVE_READY || state_ == AppState::SLAVE_WAIT_START) &&
|
||||
type == MessageType::DISCOVER && (!havePeer_ || !memcmp(peer_, r.mac, 6))) {
|
||||
leaveIdlePowerSave();
|
||||
setActivePerformance(true);
|
||||
radio_.setWindowedReceive(false);
|
||||
memcpy(peer_, r.mac, 6); havePeer_ = true; session_ = r.packet.session; stageIndex_ = 0; sequence_ = r.packet.sequence;
|
||||
lastPeerSeenMs_ = millis();
|
||||
ProtocolPacket ack = makePacket(MessageType::DISCOVER_ACK); ack.sequence = r.packet.sequence; sendLinked(ack);
|
||||
state_ = AppState::SLAVE_WAIT_START; display_.show("MASTER SEEN", "ACK SENT"); continue;
|
||||
state_ = AppState::SLAVE_WAIT_START; display_.show(UiText::MASTER_SEEN, UiText::ACK_SENT); continue;
|
||||
}
|
||||
if (state_ == AppState::MASTER_DISCOVER && type == MessageType::DISCOVER_ACK && r.packet.session == session_) {
|
||||
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;
|
||||
char mac[20]; Radio::macText(peer_, mac, sizeof(mac)); Log::printf("ESP-NOW", "Slave selected %s", mac); continue;
|
||||
}
|
||||
@@ -374,7 +455,7 @@ void App::handleRadio() {
|
||||
stageIndex_ = r.packet.stage;
|
||||
sequence_ = r.packet.sequence;
|
||||
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;
|
||||
stageCount_ = r.packet.stageCount;
|
||||
actual_ = {};
|
||||
@@ -402,11 +483,12 @@ void App::handleRadio() {
|
||||
r.packet.sequence == pendingPacket_.sequence) {
|
||||
if (!stageStartConfirmed_) showStageProgress();
|
||||
stageStartConfirmed_ = true;
|
||||
deadlineMs_ = millis() + params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS +
|
||||
(1000UL * PWM_SETTLE_CYCLES / actual_.actualHz) + 20;
|
||||
deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) +
|
||||
LINK_REPLY_TIMEOUT_MS + 20;
|
||||
} else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::PROGRESS) {
|
||||
stageStartConfirmed_ = true;
|
||||
deadlineMs_ = millis() + params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS;
|
||||
deadlineMs_ = millis() + stageWallTimeMs(params_.testTimeMs, actual_.actualHz) +
|
||||
LINK_REPLY_TIMEOUT_MS;
|
||||
showRemoteResult(r.packet);
|
||||
} else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::RESULT) {
|
||||
ProtocolPacket ack = makePacket(MessageType::ACK); ack.sequence = r.packet.sequence;
|
||||
@@ -428,7 +510,8 @@ void App::handleRadio() {
|
||||
actual_.actualHz = r.packet.actualHz; actual_.actualDutyPct = r.packet.actualDutyX100 / 100.0f;
|
||||
if (!startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { finish(false, FailReason::UNSUPPORTED); continue; }
|
||||
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);
|
||||
started.sequence = r.packet.sequence; sendLinked(started);
|
||||
} else if (state_ == AppState::SLAVE_MEASURE && type == MessageType::START_STAGE) {
|
||||
@@ -440,7 +523,7 @@ void App::handleRadio() {
|
||||
if (pendingPacket_.passed) {
|
||||
if (r.packet.passed) {
|
||||
radio_.end(); pendingReason_ = FailReason::NONE;
|
||||
if (armSlave(true)) display_.show("SLAVE PASS", "WAIT MASTER");
|
||||
if (armSlave(true)) display_.show(UiText::PASS_WORD, UiText::WAIT_MASTER);
|
||||
} else {
|
||||
stageIndex_ = static_cast<uint32_t>(r.packet.stage) + 1U;
|
||||
state_ = AppState::SLAVE_WAIT_START;
|
||||
@@ -454,8 +537,9 @@ void App::handleRadio() {
|
||||
void App::updateMaster() {
|
||||
const uint32_t now = millis();
|
||||
if (state_ == AppState::MASTER_DISCOVER) {
|
||||
if (now - lastSendMs_ >= LINK_RETRY_INTERVAL_MS) {
|
||||
Log::event("ESP-NOW", "DISCOVER retry"); radio_.sendBroadcast(pendingPacket_);
|
||||
if (now - lastSendMs_ >= DISCOVERY_RETRY_INTERVAL_MS) {
|
||||
if (++retries_ % 50U == 0U) Log::event("ESP-NOW", "DISCOVER burst continues");
|
||||
radio_.sendBroadcast(pendingPacket_);
|
||||
lastSendMs_ = now;
|
||||
}
|
||||
return;
|
||||
@@ -475,7 +559,7 @@ void App::updateMaster() {
|
||||
Log::printf("ESP-NOW", "%s retry=%u", messageName(static_cast<MessageType>(pendingPacket_.type)), retries_ + 1);
|
||||
sendLinked(pendingPacket_); ++retries_;
|
||||
deadlineMs_ = now + (state_ == AppState::MASTER_WAIT_RESULT ?
|
||||
(stageStartConfirmed_ ? 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);
|
||||
}
|
||||
|
||||
@@ -488,10 +572,9 @@ void App::updateSlave() {
|
||||
StageStats live = {};
|
||||
if (measurement_.statsSnapshot(live)) {
|
||||
ProtocolPacket progress = makePacket(MessageType::PROGRESS);
|
||||
progress.progressStep = measurement_.progressStep();
|
||||
fillMeasuredResult(progress, live);
|
||||
progress.sequence = sequence_; sendLinked(progress);
|
||||
// oled.display() is synchronous. Resume capture only after the full
|
||||
// framebuffer has reached the display.
|
||||
showStageResult(live);
|
||||
}
|
||||
measurement_.continueAfterDisplay();
|
||||
@@ -503,6 +586,8 @@ void App::updateSlave() {
|
||||
printStageStats(measurement_.stats(), actual_.actualHz);
|
||||
showStageResult(measurement_.stats());
|
||||
pendingPacket_ = makePacket(MessageType::RESULT);
|
||||
pendingPacket_.progressStep = ms == MeasureState::PASS ?
|
||||
MEASUREMENT_PROGRESS_STEPS : measurement_.progressStep();
|
||||
pendingPacket_.passed = ms == MeasureState::PASS && measurement_.reason() == FailReason::NONE;
|
||||
pendingPacket_.reason = static_cast<uint8_t>(measurement_.reason()); pendingPacket_.periods = measurement_.stats().periods;
|
||||
fillMeasuredResult(pendingPacket_, measurement_.stats());
|
||||
@@ -552,31 +637,94 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
|
||||
if (!pass && masterActive && havePeer_ && reason != FailReason::ABORTED) sendAbort(reason);
|
||||
if (state_ != AppState::IDLE && state_ != AppState::MENU) radio_.end();
|
||||
state_ = AppState::FINISHED; pendingReason_ = reason;
|
||||
setActivePerformance(false);
|
||||
lastUserActivityMs_ = millis();
|
||||
if (slaveLinkLost) {
|
||||
char target[12], one[24];
|
||||
char target[12], one[64];
|
||||
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, target, sizeof(target));
|
||||
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target,
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target,
|
||||
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct);
|
||||
display_.show(one, failName(reason), stageIndex_ + 1, stageCount_);
|
||||
display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_);
|
||||
armSlave(true);
|
||||
return;
|
||||
}
|
||||
if (static_cast<Role>(settings_.role) == Role::SLAVE) slaveRearmAtMs_ = millis() + 2000;
|
||||
if (preserveDisplay) return;
|
||||
char one[24];
|
||||
char one[64];
|
||||
if (pass) {
|
||||
const Role role = static_cast<Role>(settings_.role);
|
||||
snprintf(one, sizeof(one), "%s PASS", roleName(role));
|
||||
display_.show(one, role == Role::SLAVE ? "WAIT MASTER" : "START=REPEAT");
|
||||
snprintf(one, sizeof(one), "%s %s", uiRoleName(role), UiText::PASS_WORD);
|
||||
display_.show(one, role == Role::SLAVE ? UiText::WAIT_MASTER : UiText::START_AGAIN);
|
||||
}
|
||||
else if (requestedHz_) {
|
||||
char frequency[12];
|
||||
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, frequency, sizeof(frequency));
|
||||
snprintf(one, sizeof(one), "FAIL %s %.0f%%", frequency,
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, frequency,
|
||||
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct);
|
||||
display_.show(one, failName(reason), stageIndex_ + 1, stageCount_);
|
||||
display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_);
|
||||
} else {
|
||||
display_.show("TEST FAILED", failName(reason));
|
||||
display_.show(UiText::TEST_FAILED, uiFailName(reason));
|
||||
}
|
||||
}
|
||||
|
||||
bool App::idlePowerSaveAllowed() const {
|
||||
return initialized_ && (state_ == AppState::IDLE || state_ == AppState::MENU ||
|
||||
state_ == AppState::FINISHED || state_ == AppState::SLAVE_READY);
|
||||
}
|
||||
|
||||
void App::setActivePerformance(bool active) {
|
||||
const uint32_t targetMhz = active ? 160U : 80U;
|
||||
if (getCpuFrequencyMhz() != targetMhz && !setCpuFrequencyMhz(targetMhz))
|
||||
Log::printf("POWER", "CPU frequency change to %luMHz FAILED", targetMhz);
|
||||
}
|
||||
|
||||
void App::leaveIdlePowerSave(bool wakeDisplay) {
|
||||
if (!idlePowerSave_) {
|
||||
if (wakeDisplay) display_.setPower(true);
|
||||
return;
|
||||
}
|
||||
idlePowerSave_ = false;
|
||||
lastUserActivityMs_ = millis();
|
||||
if (wakeDisplay) display_.setPower(true);
|
||||
Log::event("POWER", "idle light sleep ended");
|
||||
}
|
||||
|
||||
void App::serviceIdlePowerSave() {
|
||||
if (!idlePowerSaveAllowed()) {
|
||||
leaveIdlePowerSave(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t now = millis();
|
||||
if (!idlePowerSave_) {
|
||||
if (now - lastUserActivityMs_ < IDLE_POWER_SAVE_TIMEOUT_MS) {
|
||||
delay(1); // allow the FreeRTOS idle task to halt the CPU between UI polls
|
||||
return;
|
||||
}
|
||||
idlePowerSave_ = true;
|
||||
display_.setPower(false);
|
||||
Log::event("POWER", "idle timeout; OLED off and light sleep started");
|
||||
}
|
||||
|
||||
gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_BUTTON_START),
|
||||
BUTTON_ACTIVE_LEVEL == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
|
||||
gpio_wakeup_enable(static_cast<gpio_num_t>(GPIO_BUTTON_MODE),
|
||||
BUTTON_ACTIVE_LEVEL == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
|
||||
esp_sleep_enable_gpio_wakeup();
|
||||
esp_sleep_enable_timer_wakeup(IDLE_LIGHT_SLEEP_SLICE_US);
|
||||
const esp_err_t result = esp_light_sleep_start();
|
||||
if (result != ESP_OK) {
|
||||
delay(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO) {
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,11 +736,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("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 step %lu Hz, accuracy %.2f%%, %lums x%u, duty %u%%\n",
|
||||
params_.startHz, params_.endHz, params_.stepHz, params_.accuracyPct, params_.testTimeMs, params_.repeats, params_.dutyPct);
|
||||
stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz);
|
||||
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, 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");
|
||||
}
|
||||
|
||||
@@ -617,64 +765,69 @@ void App::printStageStats(const StageStats &s, uint32_t hz) {
|
||||
}
|
||||
|
||||
void App::showStageResult(const StageStats &s) {
|
||||
char one[24], two[24];
|
||||
char one[64], two[64];
|
||||
char target[12]; Display::formatTestFrequency(actual_.actualHz, target, sizeof(target));
|
||||
if (s.reason != FailReason::NONE) {
|
||||
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target, actual_.actualDutyPct);
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, actual_.actualDutyPct);
|
||||
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));
|
||||
snprintf(two, sizeof(two), "PERIOD OUT %s", 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), "DUTY OUT %s", duty);
|
||||
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, duty);
|
||||
} else {
|
||||
snprintf(two, sizeof(two), "%s", failName(s.reason));
|
||||
snprintf(two, sizeof(two), "%s", uiFailName(s.reason));
|
||||
}
|
||||
display_.show(one, two, stageIndex_ + 1, stageCount_);
|
||||
display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()),
|
||||
overallProgressTotal(stageCount_));
|
||||
return;
|
||||
}
|
||||
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
|
||||
target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_);
|
||||
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, "F:--- D:---%", stageIndex_ + 1, stageCount_);
|
||||
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);
|
||||
display_.show(one, two, stageIndex_ + 1, stageCount_);
|
||||
display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()),
|
||||
overallProgressTotal(stageCount_));
|
||||
}
|
||||
|
||||
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[24], two[24];
|
||||
char target[12], one[64], two[64];
|
||||
Display::formatTestFrequency(packet.actualHz ? packet.actualHz : packet.requestedHz,
|
||||
target, sizeof(target));
|
||||
if (reason == FailReason::NONE) {
|
||||
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
|
||||
target, packet.actualDutyX100 / 100.0f, stageIndex_ + 1, stageCount_);
|
||||
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);
|
||||
} else snprintf(two, sizeof(two), "F:--- D:---%%");
|
||||
} else snprintf(two, sizeof(two), "%s", UiText::NO_MEASUREMENT);
|
||||
} else if (reason == FailReason::PERIOD_OUT && packet.measuredHzX10) {
|
||||
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target, packet.actualDutyX100 / 100.0f);
|
||||
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), "PERIOD OUT %s", measured);
|
||||
snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, measured);
|
||||
} else if (reason == FailReason::DUTY_OUT && packet.measuredDutyX10) {
|
||||
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target, packet.actualDutyX100 / 100.0f);
|
||||
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), "DUTY OUT %s", duty);
|
||||
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, duty);
|
||||
} else {
|
||||
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target, packet.actualDutyX100 / 100.0f);
|
||||
snprintf(two, sizeof(two), "%s", failName(reason));
|
||||
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
|
||||
snprintf(two, sizeof(two), "%s", uiFailName(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 {
|
||||
@@ -691,9 +844,10 @@ void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) co
|
||||
}
|
||||
|
||||
void App::showStageProgress() {
|
||||
char target[12], one[24];
|
||||
char target[12], one[64], stage[12];
|
||||
Display::formatTestFrequency(actual_.actualHz, target, sizeof(target));
|
||||
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
|
||||
target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_);
|
||||
display_.show(one, "F:--- D:---%", stageIndex_ + 1, stageCount_);
|
||||
snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_);
|
||||
snprintf(one, sizeof(one), UiText::TEST_FORMAT, target, actual_.actualDutyPct, stage);
|
||||
display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, 0),
|
||||
overallProgressTotal(stageCount_));
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ class App {
|
||||
void sendCurrent(MessageType type);
|
||||
void updateHeartbeat();
|
||||
bool packetForCurrent(const ProtocolPacket &p) const;
|
||||
void serviceIdlePowerSave();
|
||||
void leaveIdlePowerSave(bool wakeDisplay = true);
|
||||
bool idlePowerSaveAllowed() const;
|
||||
void setActivePerformance(bool active);
|
||||
|
||||
Button startButton_, modeButton_;
|
||||
Display display_;
|
||||
@@ -75,4 +79,6 @@ class App {
|
||||
uint32_t bootCheckStartedMs_ = 0;
|
||||
uint32_t slaveRearmAtMs_ = 0;
|
||||
bool stageStartConfirmed_ = false;
|
||||
uint32_t lastUserActivityMs_ = 0;
|
||||
bool idlePowerSave_ = false;
|
||||
};
|
||||
|
||||
@@ -7,8 +7,21 @@ void Button::begin() {
|
||||
changedAt_ = millis();
|
||||
}
|
||||
|
||||
void Button::suppressUntilRelease() {
|
||||
suppressed_ = true;
|
||||
raw_ = stable_ = (digitalRead(pin_) == BUTTON_ACTIVE_LEVEL);
|
||||
changedAt_ = millis();
|
||||
longSent_ = true;
|
||||
}
|
||||
|
||||
ButtonEvent Button::update(uint32_t now) {
|
||||
const bool sample = (digitalRead(pin_) == BUTTON_ACTIVE_LEVEL);
|
||||
if (suppressed_) {
|
||||
raw_ = stable_ = sample;
|
||||
changedAt_ = now;
|
||||
if (!sample) { suppressed_ = false; longSent_ = false; }
|
||||
return ButtonEvent::NONE;
|
||||
}
|
||||
if (sample != raw_) { raw_ = sample; changedAt_ = now; }
|
||||
if (raw_ != stable_ && now - changedAt_ >= BUTTON_DEBOUNCE_MS) {
|
||||
stable_ = raw_;
|
||||
|
||||
@@ -8,10 +8,12 @@ class Button {
|
||||
explicit Button(uint8_t pin) : pin_(pin) {}
|
||||
void begin();
|
||||
ButtonEvent update(uint32_t nowMs);
|
||||
void suppressUntilRelease();
|
||||
bool pressed() const { return stable_; }
|
||||
private:
|
||||
uint8_t pin_;
|
||||
bool raw_ = false, stable_ = false, longSent_ = false;
|
||||
bool suppressed_ = false;
|
||||
uint32_t changedAt_ = 0, pressedAt_ = 0, nextRepeat_ = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,23 +3,43 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
// ------------------------- Hardware configuration -------------------------
|
||||
// Uncomment for the hand-wired prototype. The production PCB assignments
|
||||
// below follow the physical header positions shown in the schematic.
|
||||
// #define MAKETKA
|
||||
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
constexpr bool TARGET_IS_C3 = true;
|
||||
constexpr uint8_t GPIO_PWM = 3;
|
||||
constexpr uint8_t GPIO_RX = 4;
|
||||
#ifdef MAKETKA
|
||||
constexpr uint8_t GPIO_BUTTON_MODE = 0;
|
||||
constexpr uint8_t GPIO_BUTTON_START = 1;
|
||||
#else
|
||||
constexpr uint8_t GPIO_BUTTON_MODE = 1;
|
||||
constexpr uint8_t GPIO_BUTTON_START = 0;
|
||||
#endif
|
||||
constexpr uint8_t GPIO_SDA = 6;
|
||||
constexpr uint8_t GPIO_SCL = 7;
|
||||
#elif CONFIG_IDF_TARGET_ESP32S3
|
||||
constexpr bool TARGET_IS_C3 = false;
|
||||
#ifdef MAKETKA
|
||||
constexpr uint8_t GPIO_PWM = 4;
|
||||
constexpr uint8_t GPIO_RX = 5;
|
||||
constexpr uint8_t GPIO_BUTTON_MODE = 6;
|
||||
constexpr uint8_t GPIO_BUTTON_START = 7;
|
||||
constexpr uint8_t GPIO_BUTTON_MODE = 0;
|
||||
constexpr uint8_t GPIO_BUTTON_START = 1;
|
||||
constexpr uint8_t GPIO_SDA = 8;
|
||||
constexpr uint8_t GPIO_SCL = 9;
|
||||
#else
|
||||
// The S3 SuperMini is fitted so its 5V and GND pins occupy the same PCB
|
||||
// contacts as on the C3 SuperMini. Signals therefore follow header position.
|
||||
constexpr uint8_t GPIO_PWM = 12;
|
||||
constexpr uint8_t GPIO_RX = 13;
|
||||
constexpr uint8_t GPIO_BUTTON_MODE = 10;
|
||||
constexpr uint8_t GPIO_BUTTON_START = 9;
|
||||
constexpr uint8_t GPIO_SDA = 44;
|
||||
constexpr uint8_t GPIO_SCL = 1;
|
||||
#endif
|
||||
#else
|
||||
#error "Only ESP32-C3 and ESP32-S3 are supported"
|
||||
#endif
|
||||
|
||||
@@ -38,7 +58,7 @@ constexpr bool SERIAL_MINIMAL_LOG = true;
|
||||
#define PWM_SETTLE_CYCLES 5U
|
||||
|
||||
constexpr uint32_t BUTTON_DEBOUNCE_MS = 30;
|
||||
constexpr uint32_t BUTTON_LONG_PRESS_MS = 800;
|
||||
constexpr uint32_t BUTTON_LONG_PRESS_MS = 500;
|
||||
constexpr uint32_t BUTTON_REPEAT_DELAY_MS = 600;
|
||||
constexpr uint32_t BUTTON_REPEAT_MS = 180;
|
||||
constexpr uint32_t FACTORY_RESET_HOLD_MS = 1500;
|
||||
@@ -46,6 +66,7 @@ constexpr uint32_t FACTORY_RESET_HOLD_MS = 1500;
|
||||
constexpr uint32_t LINK_REPLY_TIMEOUT_MS = 1500;
|
||||
constexpr uint8_t LINK_PACKET_RETRIES = 10;
|
||||
constexpr uint32_t LINK_RETRY_INTERVAL_MS = 1000;
|
||||
constexpr uint32_t DISCOVERY_RETRY_INTERVAL_MS = 20;
|
||||
constexpr uint32_t LINK_HEARTBEAT_INTERVAL_MS = 500;
|
||||
constexpr uint32_t LINK_HEARTBEAT_TIMEOUT_MS = 2500;
|
||||
constexpr uint32_t FINAL_ACK_RETRY_INTERVAL_MS = 50;
|
||||
@@ -57,28 +78,51 @@ constexpr uint32_t RMT_TARGET_CHUNK_US = 5000;
|
||||
constexpr uint8_t RMT_QUEUE_BLOCKS = 8;
|
||||
constexpr uint16_t PERIOD_BATCH_SIZE = 128;
|
||||
constexpr uint8_t MEASUREMENT_PROGRESS_STEPS = 10;
|
||||
constexpr uint32_t OLED_PROGRESS_UPDATE_MS = 15;
|
||||
|
||||
constexpr uint32_t IDLE_POWER_SAVE_TIMEOUT_MS = 60000;
|
||||
constexpr uint32_t IDLE_LIGHT_SLEEP_SLICE_US = 10000;
|
||||
constexpr uint16_t SLAVE_LISTEN_INTERVAL_MS = 100;
|
||||
constexpr uint16_t SLAVE_LISTEN_WINDOW_MS = 20;
|
||||
static_assert(SLAVE_LISTEN_WINDOW_MS < SLAVE_LISTEN_INTERVAL_MS,
|
||||
"Slave listen window must be shorter than its interval");
|
||||
// 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 S3_STRICT_MAX_HZ = 1000000;
|
||||
// RMT stores each HIGH/LOW duration in 15 bits. At 80 MHz that limits a
|
||||
// single level to about 409 us, so even a 1 kHz signal with 50% duty cannot
|
||||
// be captured. 20 MHz still provides 20 ticks at 1 MHz (5% resolution),
|
||||
// while allowing level durations up to about 1.64 ms for the 1 kHz/90% case.
|
||||
constexpr uint32_t CAPTURE_RESOLUTION_HZ = 20000000;
|
||||
// Arduino-ESP32 uses the 40 MHz crystal as the default LEDC clock on C3/S3.
|
||||
// RMT stores each HIGH/LOW duration in 15 bits. Select the fastest clock that
|
||||
// still fits both levels of the current PWM signal: 20, 40 or 80 MHz.
|
||||
constexpr uint32_t CAPTURE_RESOLUTION_OPTIONS_HZ[] = {20000000, 40000000, 80000000};
|
||||
constexpr uint32_t RMT_MAX_LEVEL_TICKS = 32766;
|
||||
// C3 uses the 40 MHz crystal as the LEDC clock.
|
||||
// Keep this explicit so the resolution calculation never asks LEDC for an
|
||||
// impossible frequency/resolution combination.
|
||||
constexpr uint32_t LEDC_SOURCE_CLOCK_HZ = 40000000;
|
||||
constexpr uint8_t LEDC_CHANNEL = 0;
|
||||
constexpr uint8_t LEDC_MAX_BITS = 14;
|
||||
// S3 uses the dedicated MCPWM peripheral. A 40 MHz timer clock keeps the
|
||||
// longest 1 kHz period within the S3's 16-bit MCPWM counter and makes every
|
||||
// frequency in TEST_FREQUENCIES_HZ exact.
|
||||
constexpr uint32_t MCPWM_RESOLUTION_HZ = 40000000;
|
||||
constexpr uint32_t MCPWM_MAX_PERIOD_TICKS = 65535;
|
||||
|
||||
// -------------------------- Menu value arrays -----------------------------
|
||||
constexpr uint32_t START_FREQ_OPTIONS_HZ[] = {1000, 2000, 5000, 10000, 20000, 50000};
|
||||
constexpr uint32_t END_FREQ_OPTIONS_HZ[] = {100000, 200000, 500000, 750000, 1000000};
|
||||
constexpr uint32_t STEP_OPTIONS_HZ[] = {1000, 2000, 5000, 10000, 20000, 50000, 100000};
|
||||
// START and END deliberately have separate, independently cycling menu lists.
|
||||
// Every value is exactly achievable from a 40 MHz timer clock. The test 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 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};
|
||||
|
||||
template <typename T, size_t N> constexpr size_t countOf(const T (&)[N]) { return N; }
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
#pragma once
|
||||
|
||||
// Select the language used by the OLED interface.
|
||||
// Build with UI_LANGUAGE_EN for English or UI_LANGUAGE_RU for Russian.
|
||||
#define UI_LANGUAGE_EN 0
|
||||
#define UI_LANGUAGE_RU 1
|
||||
|
||||
#ifndef UI_LANGUAGE
|
||||
#define UI_LANGUAGE UI_LANGUAGE_RU
|
||||
#endif
|
||||
|
||||
namespace UiText {
|
||||
|
||||
#if UI_LANGUAGE == UI_LANGUAGE_RU
|
||||
|
||||
constexpr const char *ROLE_NAMES[] = {
|
||||
"СОЛО", "МАСТЕР", "СЛЕЙВ"
|
||||
};
|
||||
|
||||
constexpr const char *FAIL_NAMES[] = {
|
||||
"НЕТ ОШИБКИ",
|
||||
"НЕТ СИГНАЛА",
|
||||
"ПЕРИОД ВНЕ ДОПУСКА",
|
||||
"ЗАПОЛН. ВНЕ ДОПУСКА",
|
||||
"ЛИШНИЙ ФРОНТ",
|
||||
"ИМПУЛЬСНАЯ ПОМЕХА",
|
||||
"ПРОПУЩЕН ФРОНТ",
|
||||
"ОШИБКА ПОТЕРИ ДАННЫХ",
|
||||
"СВЯЗЬ ПОТЕРЯНА",
|
||||
"РЕЖИМ НЕ ПОДДЕРЖИВ.",
|
||||
"НЕ ХВАТАЕТ ТОЧНОСТИ",
|
||||
"ТЕСТ ОСТАНОВЛЕН"
|
||||
};
|
||||
|
||||
constexpr const char *MODE_PREFIX = "РЕЖИМ: ";
|
||||
constexpr const char *START_RUN = "ГОТОВ К ЗАПУСКУ";
|
||||
|
||||
constexpr const char *MENU_START_FREQUENCY = "ЧАСТОТА ОТ:";
|
||||
constexpr const char *MENU_END_FREQUENCY = "ЧАСТОТА ДО:";
|
||||
constexpr const char *MENU_ACCURACY = "ТОЧНОСТЬ:";
|
||||
constexpr const char *MENU_TEST_TIME = "ВРЕМЯ ВЫБОРКИ:";
|
||||
constexpr const char *MENU_PWM_DUTY = "ЗАПОЛНЕНИЕ:";
|
||||
constexpr const char *MENU_TOTAL_TIME = "ОБЩЕЕ ВРЕМЯ:";
|
||||
constexpr const char *FREQUENCY_UNIT = " Гц";
|
||||
|
||||
constexpr const char *SLAVE_READY = "СЛЕЙВ ГОТОВ";
|
||||
constexpr const char *WAIT_MASTER = "ОЖИДАНИЕ МАСТЕРА";
|
||||
constexpr const char *LINK_FAILED = "СВЯЗЬ НЕ УСТАНОВЛЕНА";
|
||||
constexpr const char *RADIO_ERROR = "ОШИБКА СВЯЗИ";
|
||||
constexpr const char *MASTER_SEARCH = "ПОИСК СЛЕЙВА";
|
||||
constexpr const char *HOLD_START_STOP = "УДЕРЖ. START ДЛЯ СТОП";
|
||||
constexpr const char *MASTER_SEEN = "МАСТЕР ОБНАРУЖЕН";
|
||||
constexpr const char *ACK_SENT = "ОТВЕТ ОТПРАВЛЕН";
|
||||
constexpr const char *START_AGAIN = "ГОТОВ К ЗАПУСКУ";
|
||||
constexpr const char *TEST_FAILED = "ТЕСТ НЕ ПРОЙДЕН";
|
||||
|
||||
constexpr const char *PASS_WORD = "ТЕСТ ПРОЙДЕН";
|
||||
constexpr const char *FAIL_FORMAT = "СБОЙ %s %.0f%%";
|
||||
constexpr const char *TEST_FORMAT = "Тест:%-6s %2.0f%% %5s";
|
||||
constexpr const char *PERIOD_OUT_FORMAT = "ОШИБКА ЧАСТОТЫ %s";
|
||||
constexpr const char *DUTY_OUT_FORMAT = "ОШИБКА ЗАПОЛН. %s";
|
||||
constexpr const char *NO_MEASUREMENT = "F:--- D:---%";
|
||||
|
||||
#elif UI_LANGUAGE == UI_LANGUAGE_EN
|
||||
|
||||
constexpr const char *ROLE_NAMES[] = {
|
||||
"SOLO", "MASTER", "SLAVE"
|
||||
};
|
||||
|
||||
constexpr const char *FAIL_NAMES[] = {
|
||||
"NONE",
|
||||
"NO SIGNAL",
|
||||
"PERIOD OUT",
|
||||
"DUTY OUT",
|
||||
"EXTRA EDGE",
|
||||
"GLITCH",
|
||||
"LOST EDGE",
|
||||
"DATA LOSS ERROR",
|
||||
"LINK LOST",
|
||||
"UNSUPPORTED",
|
||||
"RESOLUTION",
|
||||
"ABORTED"
|
||||
};
|
||||
|
||||
constexpr const char *MODE_PREFIX = "MODE: ";
|
||||
constexpr const char *START_RUN = "READY TO START";
|
||||
|
||||
constexpr const char *MENU_START_FREQUENCY = "START FREQ:";
|
||||
constexpr const char *MENU_END_FREQUENCY = "END FREQ:";
|
||||
constexpr const char *MENU_ACCURACY = "ACCURACY:";
|
||||
constexpr const char *MENU_TEST_TIME = "TEST TIME:";
|
||||
constexpr const char *MENU_PWM_DUTY = "PWM DUTY:";
|
||||
constexpr const char *MENU_TOTAL_TIME = "TOTAL TIME:";
|
||||
constexpr const char *FREQUENCY_UNIT = " Hz";
|
||||
|
||||
constexpr const char *SLAVE_READY = "SLAVE READY";
|
||||
constexpr const char *WAIT_MASTER = "WAIT MASTER";
|
||||
constexpr const char *LINK_FAILED = "LINK FAILED";
|
||||
constexpr const char *RADIO_ERROR = "RADIO ERROR";
|
||||
constexpr const char *MASTER_SEARCH = "MASTER SEARCH";
|
||||
constexpr const char *HOLD_START_STOP = "HOLD START TO STOP";
|
||||
constexpr const char *MASTER_SEEN = "MASTER SEEN";
|
||||
constexpr const char *ACK_SENT = "ACK SENT";
|
||||
constexpr const char *START_AGAIN = "READY TO START";
|
||||
constexpr const char *TEST_FAILED = "TEST FAILED";
|
||||
|
||||
constexpr const char *PASS_WORD = "TEST PASS";
|
||||
constexpr const char *FAIL_FORMAT = "FAIL %s %.0f%%";
|
||||
constexpr const char *TEST_FORMAT = "Test:%-6s %2.0f%% %5s";
|
||||
constexpr const char *PERIOD_OUT_FORMAT = "PERIOD OUT %s";
|
||||
constexpr const char *DUTY_OUT_FORMAT = "DUTY OUT %s";
|
||||
constexpr const char *NO_MEASUREMENT = "F:--- D:---%";
|
||||
|
||||
#else
|
||||
#error "UI_LANGUAGE must be UI_LANGUAGE_EN or UI_LANGUAGE_RU"
|
||||
#endif
|
||||
|
||||
} // namespace UiText
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "Core.h"
|
||||
#include "Config.h"
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -10,7 +11,7 @@ const char *roleName(Role r) {
|
||||
|
||||
const char *failName(FailReason r) {
|
||||
static const char *names[] = {"NONE", "NO SIGNAL", "PERIOD OUT", "DUTY OUT",
|
||||
"EXTRA EDGE", "GLITCH", "LOST EDGE", "TOO FEW PERIODS", "LINK LOST",
|
||||
"EXTRA EDGE", "GLITCH", "LOST EDGE", "DATA LOSS ERROR", "LINK LOST",
|
||||
"UNSUPPORTED", "RESOLUTION", "ABORTED"};
|
||||
const uint8_t i = static_cast<uint8_t>(r);
|
||||
return i < (sizeof(names) / sizeof(names[0])) ? names[i] : "UNKNOWN";
|
||||
@@ -30,28 +31,52 @@ uint32_t settingsChecksum(const Settings &s) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz, uint32_t stepHz) {
|
||||
if (!startHz || !stepHz || endHz <= startHz) return 0;
|
||||
const uint64_t span = static_cast<uint64_t>(endHz) - startHz;
|
||||
return static_cast<uint32_t>(span / stepHz + 1U + ((span % stepHz) ? 1U : 0U));
|
||||
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz) {
|
||||
if (!startHz || endHz <= startHz) return 0;
|
||||
uint32_t count = 0;
|
||||
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) {
|
||||
const uint32_t count = frequencyPointCount(startHz, endHz, stepHz);
|
||||
if (!count || index >= count) return 0;
|
||||
if (index == count - 1) return endHz;
|
||||
const uint64_t v = static_cast<uint64_t>(startHz) + static_cast<uint64_t>(stepHz) * index;
|
||||
return v < endHz ? static_cast<uint32_t>(v) : endHz;
|
||||
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t index) {
|
||||
for (size_t i = 0; i < countOf(TEST_FREQUENCIES_HZ); ++i) {
|
||||
const uint32_t frequency = TEST_FREQUENCIES_HZ[i];
|
||||
if (frequency < startHz || frequency > endHz) continue;
|
||||
if (!index--) return frequency;
|
||||
}
|
||||
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 total = 0;
|
||||
const uint32_t count = frequencyPointCount(p.startHz, p.endHz, p.stepHz);
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const uint32_t f = frequencyAt(p.startHz, p.endHz, p.stepHz, i);
|
||||
total += (1000000ULL * settleCycles + f - 1) / f;
|
||||
total += static_cast<uint64_t>(p.testTimeMs) * 1000ULL * p.repeats;
|
||||
}
|
||||
const uint32_t count = frequencyPointCount(p.startHz, p.endHz);
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
total += nominalStageUs(frequencyAt(p.startHz, p.endHz, i), p.testTimeMs, settleCycles);
|
||||
return total;
|
||||
}
|
||||
|
||||
@@ -63,6 +88,10 @@ bool dutyWithin(float measured, float expected, float tolerance) {
|
||||
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 maxBits) {
|
||||
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
|
||||
// step is not itself an error when the requested value (e.g. 50%) is exactly
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
enum class Role : uint8_t { SOLO, MASTER, SLAVE };
|
||||
enum class FailReason : uint8_t {
|
||||
NONE, NO_SIGNAL, PERIOD_OUT, DUTY_OUT, EXTRA_EDGE, GLITCH, LOST_EDGE,
|
||||
TOO_FEW_PERIODS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED
|
||||
DATA_LOSS, LINK_LOST, UNSUPPORTED, RESOLUTION, ABORTED
|
||||
};
|
||||
|
||||
const char *roleName(Role role);
|
||||
@@ -17,10 +17,8 @@ struct Settings {
|
||||
uint8_t role;
|
||||
uint8_t startIndex;
|
||||
uint8_t endIndex;
|
||||
uint8_t stepIndex;
|
||||
uint8_t accuracyIndex;
|
||||
uint8_t timeIndex;
|
||||
uint8_t repeatIndex;
|
||||
uint8_t dutyIndex;
|
||||
uint32_t checksum;
|
||||
};
|
||||
@@ -28,10 +26,8 @@ struct Settings {
|
||||
struct TestParams {
|
||||
uint32_t startHz;
|
||||
uint32_t endHz;
|
||||
uint32_t stepHz;
|
||||
float accuracyPct;
|
||||
uint32_t testTimeMs;
|
||||
uint8_t repeats;
|
||||
uint8_t dutyPct;
|
||||
};
|
||||
|
||||
@@ -72,11 +68,13 @@ struct IntegerPwmConfig {
|
||||
};
|
||||
|
||||
uint32_t settingsChecksum(const Settings &s);
|
||||
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz, uint32_t stepHz);
|
||||
uint32_t frequencyAt(uint32_t startHz, uint32_t endHz, uint32_t stepHz, uint32_t index);
|
||||
uint32_t frequencyPointCount(uint32_t startHz, uint32_t endHz);
|
||||
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);
|
||||
bool periodWithin(float measuredHz, float expectedHz, 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 maxBits);
|
||||
uint8_t chooseStablePwmResolution(uint32_t frequencyHz, uint32_t sourceClockHz,
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
#include "Display.h"
|
||||
#include "Config.h"
|
||||
#include "Font_Cyrillic.h"
|
||||
#include "Log.h"
|
||||
#include <Wire.h>
|
||||
#include <esp_log.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
uint32_t nextUtf8Codepoint(const char *&text) {
|
||||
const uint8_t first = static_cast<uint8_t>(*text++);
|
||||
if (first < 0x80U) return first;
|
||||
if ((first & 0xE0U) == 0xC0U) {
|
||||
const uint8_t second = static_cast<uint8_t>(*text);
|
||||
if ((second & 0xC0U) == 0x80U) {
|
||||
++text;
|
||||
return ((first & 0x1FU) << 6) | (second & 0x3FU);
|
||||
}
|
||||
}
|
||||
return '?';
|
||||
}
|
||||
}
|
||||
|
||||
Display::Display() : oled_(128, 32, &Wire, -1) {}
|
||||
|
||||
bool Display::begin() {
|
||||
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.
|
||||
// Probe it once and keep the I2C driver quiet when no display is connected.
|
||||
esp_log_level_set("i2c.master", ESP_LOG_NONE);
|
||||
@@ -23,29 +40,51 @@ bool Display::begin() {
|
||||
oled_.setRotation(OLED_ROTATION);
|
||||
oled_.setTextColor(SSD1306_WHITE);
|
||||
oled_.setTextSize(1);
|
||||
powered_ = true;
|
||||
}
|
||||
Log::printf("OLED", "initialization %s, I2C address=0x%02X", ok_ ? "OK" : "FAILED", OLED_ADDRESS);
|
||||
return ok_;
|
||||
}
|
||||
|
||||
void Display::fit(char *s) {
|
||||
int16_t x, y; uint16_t w, h;
|
||||
while (*s) {
|
||||
oled_.getTextBounds(s, 0, 0, &x, &y, &w, &h);
|
||||
if (w <= 128) break;
|
||||
s[strlen(s) - 1] = '\0';
|
||||
void Display::setPower(bool enabled) {
|
||||
if (!ok_ || powered_ == enabled) return;
|
||||
oled_.ssd1306_command(enabled ? SSD1306_DISPLAYON : SSD1306_DISPLAYOFF);
|
||||
powered_ = enabled;
|
||||
Log::printf("OLED", "display power %s", enabled ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
void Display::drawTextLine(const char *text, int16_t y) {
|
||||
int16_t x = 0;
|
||||
while (text && *text && x + CyrillicFont::WIDTH <= oled_.width()) {
|
||||
const uint32_t codepoint = nextUtf8Codepoint(text);
|
||||
const uint8_t *glyph = CyrillicFont::glyph(codepoint);
|
||||
if (glyph) {
|
||||
for (uint8_t row = 0; row < CyrillicFont::HEIGHT; ++row) {
|
||||
const uint8_t pixels = pgm_read_byte(glyph + row);
|
||||
for (uint8_t column = 0; column < CyrillicFont::WIDTH; ++column)
|
||||
if (pixels & (1U << (CyrillicFont::WIDTH - 1U - column)))
|
||||
oled_.drawPixel(x + column, y + row, SSD1306_WHITE);
|
||||
}
|
||||
} else if (codepoint < 0x100U) {
|
||||
oled_.drawChar(x, y, static_cast<unsigned char>(codepoint),
|
||||
SSD1306_WHITE, SSD1306_BLACK, 1);
|
||||
} else {
|
||||
oled_.drawChar(x, y, '?', SSD1306_WHITE, SSD1306_BLACK, 1);
|
||||
}
|
||||
x += CyrillicFont::ADVANCE;
|
||||
}
|
||||
}
|
||||
|
||||
void Display::show(const char *a, const char *b, uint32_t progress, uint32_t progressTotal) {
|
||||
char one[32], two[32];
|
||||
char one[64], two[64];
|
||||
snprintf(one, sizeof(one), "%s", a ? a : ""); snprintf(two, sizeof(two), "%s", b ? b : "");
|
||||
// Serial is the primary UI mirror and remains available when OLED is absent.
|
||||
Log::printf("UI", "%s | %s", one, two);
|
||||
if (!ok_) return;
|
||||
fit(one); fit(two);
|
||||
oled_.clearDisplay(); oled_.setCursor(0, 3); oled_.print(one);
|
||||
oled_.setCursor(0, 19); oled_.print(two);
|
||||
setPower(true);
|
||||
oled_.clearDisplay();
|
||||
drawTextLine(one, 3);
|
||||
drawTextLine(two, 19);
|
||||
if (progressTotal) {
|
||||
if (progress > progressTotal) progress = progressTotal;
|
||||
const uint16_t width = static_cast<uint16_t>(
|
||||
@@ -81,7 +120,8 @@ void Display::formatTestFrequency(uint32_t hz, char *out, size_t n) {
|
||||
}
|
||||
|
||||
void Display::formatDuration(uint64_t us, char *out, size_t n) {
|
||||
const uint64_t minutes = us / 60000000ULL;
|
||||
if (minutes < 60) snprintf(out, n, "%02llu:%02llu", minutes, (us / 1000000ULL) % 60ULL);
|
||||
const uint64_t totalSeconds = (us + 999999ULL) / 1000000ULL;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -9,12 +9,15 @@ class Display {
|
||||
bool begin();
|
||||
void show(const char *line1, const char *line2,
|
||||
uint32_t progress = 0, uint32_t progressTotal = 0);
|
||||
void setPower(bool enabled);
|
||||
bool available() const { return ok_; }
|
||||
bool powered() const { return powered_; }
|
||||
static void formatFrequency(float hz, char *out, size_t size);
|
||||
static void formatTestFrequency(uint32_t hz, char *out, size_t size);
|
||||
static void formatDuration(uint64_t us, char *out, size_t size);
|
||||
static void formatDuration(uint64_t us, char *out, size_t size);
|
||||
private:
|
||||
void fit(char *text);
|
||||
void drawTextLine(const char *text, int16_t y);
|
||||
Adafruit_SSD1306 oled_;
|
||||
bool ok_ = false;
|
||||
bool powered_ = false;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// Compact 5x7 uppercase Cyrillic font. Lowercase UTF-8 letters are deliberately
|
||||
// rendered with the same uppercase glyphs: at 5x7 pixels this is considerably
|
||||
// clearer than trying to preserve lowercase shapes.
|
||||
namespace CyrillicFont {
|
||||
|
||||
constexpr uint8_t WIDTH = 5;
|
||||
constexpr uint8_t HEIGHT = 7;
|
||||
constexpr uint8_t ADVANCE = 6;
|
||||
|
||||
// Rows are stored top-to-bottom; the low five bits are pixels left-to-right.
|
||||
const uint8_t GLYPHS[][HEIGHT] PROGMEM = {
|
||||
{0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11}, // А
|
||||
{0x1F, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x1E}, // Б
|
||||
{0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E}, // В
|
||||
{0x1F, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10}, // Г
|
||||
{0x0E, 0x0A, 0x0A, 0x0A, 0x11, 0x1F, 0x11}, // Д
|
||||
{0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F}, // Е
|
||||
{0x15, 0x15, 0x0E, 0x04, 0x0E, 0x15, 0x15}, // Ж
|
||||
{0x0E, 0x11, 0x01, 0x06, 0x01, 0x11, 0x0E}, // З
|
||||
{0x11, 0x13, 0x15, 0x15, 0x19, 0x11, 0x11}, // И
|
||||
{0x0A, 0x11, 0x13, 0x15, 0x19, 0x11, 0x11}, // Й
|
||||
{0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11}, // К
|
||||
{0x07, 0x09, 0x09, 0x09, 0x11, 0x11, 0x11}, // Л
|
||||
{0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11}, // М
|
||||
{0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11}, // Н
|
||||
{0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E}, // О
|
||||
{0x1F, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}, // П
|
||||
{0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10}, // Р
|
||||
{0x0F, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0F}, // С
|
||||
{0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}, // Т
|
||||
{0x11, 0x11, 0x11, 0x0F, 0x01, 0x11, 0x0E}, // У
|
||||
{0x04, 0x0E, 0x15, 0x15, 0x0E, 0x04, 0x04}, // Ф
|
||||
{0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11}, // Х
|
||||
{0x12, 0x12, 0x12, 0x12, 0x12, 0x1E, 0x01}, // Ц
|
||||
{0x11, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x01}, // Ч
|
||||
{0x15, 0x15, 0x15, 0x15, 0x15, 0x15, 0x1F}, // Ш
|
||||
{0x15, 0x15, 0x15, 0x15, 0x15, 0x1E, 0x01}, // Щ
|
||||
{0x18, 0x08, 0x08, 0x0E, 0x09, 0x09, 0x0E}, // Ъ
|
||||
{0x11, 0x11, 0x11, 0x1D, 0x15, 0x15, 0x1D}, // Ы
|
||||
{0x10, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x1E}, // Ь
|
||||
{0x0E, 0x11, 0x01, 0x07, 0x01, 0x11, 0x0E}, // Э
|
||||
{0x12, 0x15, 0x15, 0x1D, 0x15, 0x15, 0x12}, // Ю
|
||||
{0x0F, 0x11, 0x11, 0x0F, 0x05, 0x09, 0x11} // Я
|
||||
};
|
||||
|
||||
inline const uint8_t *glyph(uint32_t codepoint) {
|
||||
if (codepoint >= 0x0430U && codepoint <= 0x044FU) codepoint -= 0x20U;
|
||||
if (codepoint == 0x0401U || codepoint == 0x0451U) codepoint = 0x0415U; // Ё -> Е
|
||||
if (codepoint < 0x0410U || codepoint > 0x042FU) return nullptr;
|
||||
return GLYPHS[codepoint - 0x0410U];
|
||||
}
|
||||
|
||||
} // namespace CyrillicFont
|
||||
@@ -3,24 +3,26 @@
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
expectedHz_ = static_cast<uint32_t>(hz + 0.5f);
|
||||
if (!expectedHz_ || !timeMs || !repeats || repeats > 10 ||
|
||||
!makePeriodLimits(expectedHz_, duty, tolerance, receiver_.tickHz(), limits_) ||
|
||||
!receiver_.start(expectedHz_)) return false;
|
||||
repeats_ = repeats; settleCycles_ = settleCycles; settleLeft_ = settleCycles;
|
||||
expectedDutyPct_ = duty;
|
||||
tolerance = effectiveTolerancePct(tolerance);
|
||||
if (!expectedHz_ || !timeMs ||
|
||||
!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;
|
||||
stepTicks_ = static_cast<uint64_t>(receiver_.tickHz()) * timeMs /
|
||||
(1000ULL * MEASUREMENT_PROGRESS_STEPS);
|
||||
if (!stepTicks_) stepTicks_ = 1;
|
||||
totalSteps_ = repeats * MEASUREMENT_PROGRESS_STEPS;
|
||||
currentStep_ = currentRepeat_ = 0;
|
||||
stats_.reset(); memset(repeatPeriods_, 0, sizeof(repeatPeriods_));
|
||||
currentStep_ = 0;
|
||||
stats_.reset();
|
||||
publishStats();
|
||||
measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis();
|
||||
measurementStartMs_ = lastPeriodMs_ = 0;
|
||||
currentRepeat_ = 0;
|
||||
expectedPeriodMs_ = static_cast<uint32_t>((1000ULL + expectedHz_ - 1U) / expectedHz_);
|
||||
if (!expectedPeriodMs_) expectedPeriodMs_ = 1;
|
||||
state_ = MeasureState::SETTLING;
|
||||
@@ -45,18 +47,17 @@ void Measurement::fail(FailReason reason) {
|
||||
receiver_.stop(); state_ = MeasureState::FAIL;
|
||||
}
|
||||
|
||||
void Measurement::completeStep() {
|
||||
void Measurement::completeMeasurement() {
|
||||
receiver_.stop();
|
||||
stats_.droppedItems += receiver_.takeDroppedItems();
|
||||
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return; }
|
||||
++currentStep_;
|
||||
publishStats();
|
||||
if (currentStep_ < totalSteps_) {
|
||||
if (++currentStep_ < MEASUREMENT_PROGRESS_STEPS) {
|
||||
state_ = MeasureState::STEP_READY;
|
||||
return;
|
||||
}
|
||||
for (uint8_t i = 0; i < repeats_; ++i) if (!repeatPeriods_[i]) {
|
||||
fail(FailReason::TOO_FEW_PERIODS); return;
|
||||
if (!stats_.periods) {
|
||||
fail(FailReason::DATA_LOSS); return;
|
||||
}
|
||||
state_ = MeasureState::PASS;
|
||||
}
|
||||
@@ -91,16 +92,14 @@ MeasureState Measurement::processOnce() {
|
||||
if (!settleLeft_) {
|
||||
measurementStartTick_ = period.startTick + period.periodTicks;
|
||||
deadlineTick_ = measurementStartTick_ + stepTicks_;
|
||||
currentRepeat_ = currentStep_ / MEASUREMENT_PROGRESS_STEPS;
|
||||
measurementStartMs_ = lastPeriodMs_ = millis(); state_ = MeasureState::RUNNING;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const uint64_t endTick = period.startTick + period.periodTicks;
|
||||
if (period.startTick < measurementStartTick_) continue; // leading incomplete period
|
||||
if (endTick > deadlineTick_) { completeStep(); return state_; } // trailing incomplete period
|
||||
++repeatPeriods_[currentRepeat_];
|
||||
const FailReason r = evaluatePeriodFast(period, receiver_.tickHz(), limits_, currentRepeat_ + 1, stats_);
|
||||
if (endTick > deadlineTick_) { completeMeasurement(); return state_; } // trailing incomplete period
|
||||
const FailReason r = evaluatePeriodFast(period, receiver_.tickHz(), limits_, 1, stats_);
|
||||
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::RUNNING && measurementStartTick_) {
|
||||
const uint32_t now = millis();
|
||||
const uint32_t edgeTimeoutMs = expectedPeriodMs_ * NO_SIGNAL_TIMEOUT_PERIODS + 2;
|
||||
if (now - measurementStartMs_ < stepTimeMs_ && now - lastPeriodMs_ > edgeTimeoutMs) {
|
||||
// RMT reports a block only after its user buffer has filled. At 1 kHz the
|
||||
// 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_;
|
||||
}
|
||||
if (now - measurementStartMs_ > stepTimeMs_ + expectedPeriodMs_ + 2) completeStep();
|
||||
if (now - measurementStartMs_ > stepTimeMs_ + expectedPeriodMs_ + 2) completeMeasurement();
|
||||
}
|
||||
return state_;
|
||||
}
|
||||
@@ -126,7 +132,7 @@ MeasureState Measurement::update() { return state_; }
|
||||
|
||||
bool Measurement::continueAfterDisplay() {
|
||||
if (state_ != MeasureState::STEP_READY) return false;
|
||||
if (!receiver_.start(expectedHz_)) {
|
||||
if (!receiver_.start(expectedHz_, expectedDutyPct_)) {
|
||||
fail(FailReason::UNSUPPORTED);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7,20 +7,21 @@ class Measurement {
|
||||
public:
|
||||
explicit Measurement(PulseReceiver &receiver) : receiver_(receiver) {}
|
||||
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();
|
||||
bool continueAfterDisplay();
|
||||
void abort();
|
||||
MeasureState state() const { return state_; }
|
||||
FailReason reason() const { return stats_.reason; }
|
||||
const StageStats &stats() const { return stats_; }
|
||||
uint8_t progressStep() const { return currentStep_; }
|
||||
bool statsSnapshot(StageStats &out) const;
|
||||
private:
|
||||
static void taskEntry(void *context);
|
||||
void taskLoop();
|
||||
MeasureState processOnce();
|
||||
void fail(FailReason reason);
|
||||
void completeStep();
|
||||
void completeMeasurement();
|
||||
void publishStats();
|
||||
PulseReceiver &receiver_;
|
||||
volatile MeasureState state_ = MeasureState::IDLE;
|
||||
@@ -30,11 +31,11 @@ class Measurement {
|
||||
mutable portMUX_TYPE statsMux_ = portMUX_INITIALIZER_UNLOCKED;
|
||||
PeriodLimits limits_ = {};
|
||||
uint32_t expectedHz_ = 0;
|
||||
uint8_t repeats_ = 0, settleCycles_ = 0, settleLeft_ = 0;
|
||||
uint8_t currentRepeat_ = 0, currentStep_ = 0, totalSteps_ = 0;
|
||||
float expectedDutyPct_ = 0.0f;
|
||||
uint8_t settleCycles_ = 0, settleLeft_ = 0;
|
||||
uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, stepTicks_ = 0;
|
||||
uint32_t startedMs_ = 0, measurementStartMs_ = 0, lastPeriodMs_ = 0;
|
||||
uint32_t stepTimeMs_ = 1, expectedPeriodMs_ = 1;
|
||||
uint32_t repeatPeriods_[10] = {};
|
||||
volatile uint8_t currentStep_ = 0;
|
||||
PulsePeriod periodBatch_[PERIOD_BATCH_SIZE] = {};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "Core.h"
|
||||
|
||||
constexpr uint16_t PROTOCOL_MAGIC = 0x4F43;
|
||||
constexpr uint8_t PROTOCOL_VERSION = 6;
|
||||
constexpr uint8_t PROTOCOL_VERSION = 8;
|
||||
|
||||
enum class MessageType : uint8_t {
|
||||
DISCOVER, DISCOVER_ACK, PREPARE, READY, START_STAGE, RESULT, ACK, ABORT,
|
||||
@@ -24,9 +24,9 @@ struct ProtocolPacket {
|
||||
uint32_t actualHz;
|
||||
uint16_t actualDutyX100;
|
||||
uint32_t testTimeMs;
|
||||
uint8_t repeats;
|
||||
uint16_t accuracyX100;
|
||||
uint8_t settleCycles;
|
||||
uint8_t progressStep;
|
||||
uint8_t passed;
|
||||
uint8_t reason;
|
||||
uint32_t periods;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
#include "Pwm.h"
|
||||
#include "Config.h"
|
||||
#include "Core.h"
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
#include <hal/ledc_ll.h>
|
||||
#elif CONFIG_IDF_TARGET_ESP32S3
|
||||
#include <driver/mcpwm_prelude.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
constexpr ledc_mode_t PWM_SPEED_MODE = LEDC_LOW_SPEED_MODE;
|
||||
constexpr ledc_timer_t PWM_TIMER = LEDC_TIMER_0;
|
||||
|
||||
@@ -22,16 +27,90 @@ bool integerDividerIsSet(uint16_t expected) {
|
||||
ledc_ll_get_clock_divider(LEDC_LL_GET_HW(), PWM_SPEED_MODE, PWM_TIMER, &rawDivider);
|
||||
return rawDivider == (static_cast<uint32_t>(expected) << LEDC_LL_FRACTIONAL_BITS);
|
||||
}
|
||||
#elif CONFIG_IDF_TARGET_ESP32S3
|
||||
mcpwm_timer_handle_t mcpwmTimer = nullptr;
|
||||
mcpwm_oper_handle_t mcpwmOperator = nullptr;
|
||||
mcpwm_cmpr_handle_t mcpwmComparator = nullptr;
|
||||
mcpwm_gen_handle_t mcpwmGenerator = nullptr;
|
||||
uint32_t mcpwmFrequencyHz = 0;
|
||||
|
||||
void releaseMcpwm() {
|
||||
if (mcpwmGenerator) {
|
||||
mcpwm_del_generator(mcpwmGenerator);
|
||||
mcpwmGenerator = nullptr;
|
||||
}
|
||||
if (mcpwmComparator) {
|
||||
mcpwm_del_comparator(mcpwmComparator);
|
||||
mcpwmComparator = nullptr;
|
||||
}
|
||||
if (mcpwmOperator) {
|
||||
mcpwm_del_operator(mcpwmOperator);
|
||||
mcpwmOperator = nullptr;
|
||||
}
|
||||
if (mcpwmTimer) {
|
||||
mcpwm_timer_disable(mcpwmTimer);
|
||||
mcpwm_del_timer(mcpwmTimer);
|
||||
mcpwmTimer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t periodResolutionBits(uint32_t periodTicks) {
|
||||
uint8_t bits = 0;
|
||||
while (periodTicks > 1U) {
|
||||
periodTicks >>= 1U;
|
||||
++bits;
|
||||
}
|
||||
return bits ? bits : 1U;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void PwmGenerator::begin() {
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
// Match LEDC_SOURCE_CLOCK_HZ and make the timer calculation deterministic.
|
||||
ledcSetClockSource(LEDC_USE_XTAL_CLK);
|
||||
pinMode(GPIO_PWM, OUTPUT);
|
||||
stop();
|
||||
#elif CONFIG_IDF_TARGET_ESP32S3
|
||||
mcpwm_timer_config_t timerConfig = {};
|
||||
timerConfig.group_id = 0;
|
||||
timerConfig.clk_src = MCPWM_TIMER_CLK_SRC_PLL160M;
|
||||
timerConfig.resolution_hz = MCPWM_RESOLUTION_HZ;
|
||||
timerConfig.count_mode = MCPWM_TIMER_COUNT_MODE_UP;
|
||||
timerConfig.period_ticks = MCPWM_RESOLUTION_HZ / 1000U;
|
||||
|
||||
mcpwm_operator_config_t operatorConfig = {};
|
||||
operatorConfig.group_id = 0;
|
||||
mcpwm_comparator_config_t comparatorConfig = {};
|
||||
mcpwm_generator_config_t generatorConfig = {};
|
||||
generatorConfig.gen_gpio_num = GPIO_PWM;
|
||||
|
||||
bool ok = mcpwm_new_timer(&timerConfig, &mcpwmTimer) == ESP_OK;
|
||||
ok = ok && mcpwm_new_operator(&operatorConfig, &mcpwmOperator) == ESP_OK;
|
||||
ok = ok && mcpwm_operator_connect_timer(mcpwmOperator, mcpwmTimer) == ESP_OK;
|
||||
ok = ok && mcpwm_new_comparator(mcpwmOperator, &comparatorConfig, &mcpwmComparator) == ESP_OK;
|
||||
ok = ok && mcpwm_new_generator(mcpwmOperator, &generatorConfig, &mcpwmGenerator) == ESP_OK;
|
||||
ok = ok && mcpwm_comparator_set_compare_value(mcpwmComparator,
|
||||
timerConfig.period_ticks / 2U) == ESP_OK;
|
||||
ok = ok && mcpwm_generator_set_action_on_timer_event(mcpwmGenerator,
|
||||
MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
|
||||
MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH)) == ESP_OK;
|
||||
ok = ok && mcpwm_generator_set_action_on_compare_event(mcpwmGenerator,
|
||||
MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP,
|
||||
mcpwmComparator, MCPWM_GEN_ACTION_LOW)) == ESP_OK;
|
||||
ok = ok && mcpwm_timer_enable(mcpwmTimer) == ESP_OK;
|
||||
if (!ok) {
|
||||
releaseMcpwm();
|
||||
pinMode(GPIO_PWM, OUTPUT);
|
||||
digitalWrite(GPIO_PWM, PWM_SAFE_LEVEL);
|
||||
return;
|
||||
}
|
||||
mcpwm_generator_set_force_level(mcpwmGenerator, PWM_SAFE_LEVEL, true);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) {
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
IntegerPwmConfig config = {};
|
||||
if (!chooseIntegerPwmConfig(hz, LEDC_SOURCE_CLOCK_HZ, LEDC_MAX_BITS, dutyPct, config)) return false;
|
||||
const uint8_t bits = config.bits;
|
||||
@@ -64,10 +143,44 @@ bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) {
|
||||
}
|
||||
pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, PWM_SAFE_LEVEL);
|
||||
return false;
|
||||
#elif CONFIG_IDF_TARGET_ESP32S3
|
||||
if (!mcpwmTimer || !mcpwmComparator || !mcpwmGenerator || !hz || dutyPct > 100U ||
|
||||
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<uint64_t>(periodTicks) * dutyPct + 50U) / 100U;
|
||||
if (activeTicks == 0U) activeTicks = 1U;
|
||||
if (activeTicks >= periodTicks) activeTicks = periodTicks - 1U;
|
||||
|
||||
stop();
|
||||
bool ok = mcpwm_timer_set_period(mcpwmTimer, periodTicks) == ESP_OK;
|
||||
ok = ok && mcpwm_comparator_set_compare_value(mcpwmComparator, activeTicks) == ESP_OK;
|
||||
ok = ok && mcpwm_generator_set_force_level(mcpwmGenerator, -1, false) == ESP_OK;
|
||||
ok = ok && mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_START_NO_STOP) == ESP_OK;
|
||||
if (!ok) {
|
||||
mcpwm_generator_set_force_level(mcpwmGenerator, PWM_SAFE_LEVEL, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
a = {hz, hz, 100.0f * activeTicks / periodTicks, periodResolutionBits(periodTicks)};
|
||||
mcpwmFrequencyHz = hz;
|
||||
running_ = true;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void PwmGenerator::stop() {
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
if (running_) ledcDetach(GPIO_PWM);
|
||||
pinMode(GPIO_PWM, OUTPUT); digitalWrite(GPIO_PWM, PWM_SAFE_LEVEL);
|
||||
#elif CONFIG_IDF_TARGET_ESP32S3
|
||||
if (mcpwmGenerator) mcpwm_generator_set_force_level(mcpwmGenerator, PWM_SAFE_LEVEL, true);
|
||||
if (running_ && mcpwmTimer) {
|
||||
mcpwm_timer_start_stop(mcpwmTimer, MCPWM_TIMER_STOP_EMPTY);
|
||||
const uint32_t waitUs = mcpwmFrequencyHz ? (1000000U / mcpwmFrequencyHz + 2U) : 2U;
|
||||
delayMicroseconds(waitUs);
|
||||
}
|
||||
mcpwmFrequencyHz = 0;
|
||||
#endif
|
||||
running_ = false;
|
||||
}
|
||||
|
||||
@@ -55,9 +55,39 @@ void Radio::end() {
|
||||
if (queue_) xQueueReset(queue_);
|
||||
if (heartbeatQueue_) xQueueReset(heartbeatQueue_);
|
||||
if (instance_ == this) instance_ = nullptr;
|
||||
windowedReceive_ = false;
|
||||
Log::event("ESP-NOW", "stopped");
|
||||
}
|
||||
|
||||
bool Radio::setWindowedReceive(bool enabled) {
|
||||
if (!active_) return false;
|
||||
if (windowedReceive_ == enabled) return true;
|
||||
|
||||
const uint16_t window = enabled ? SLAVE_LISTEN_WINDOW_MS : UINT16_MAX;
|
||||
const uint16_t interval = enabled ? SLAVE_LISTEN_INTERVAL_MS :
|
||||
ESP_WIFI_CONNECTIONLESS_INTERVAL_DEFAULT_MODE;
|
||||
const esp_err_t windowResult = esp_now_set_wake_window(window);
|
||||
const esp_err_t intervalResult = esp_wifi_connectionless_module_set_wake_interval(interval);
|
||||
const bool sleepResult = WiFi.setSleep(enabled);
|
||||
const bool ok = windowResult == ESP_OK && intervalResult == ESP_OK && sleepResult;
|
||||
|
||||
bool continuousRestored = true;
|
||||
if (!ok) {
|
||||
continuousRestored = esp_now_set_wake_window(UINT16_MAX) == ESP_OK;
|
||||
continuousRestored =
|
||||
esp_wifi_connectionless_module_set_wake_interval(
|
||||
ESP_WIFI_CONNECTIONLESS_INTERVAL_DEFAULT_MODE) == ESP_OK && continuousRestored;
|
||||
continuousRestored = WiFi.setSleep(false) && continuousRestored;
|
||||
windowedReceive_ = false;
|
||||
} else windowedReceive_ = enabled;
|
||||
|
||||
Log::printf("ESP-NOW", "RX power-save %s window=%ums interval=%ums %s%s",
|
||||
enabled ? "ON" : "OFF", window, interval,
|
||||
ok ? "OK" : "FAILED; continuous RX restore ",
|
||||
ok ? "" : (continuousRestored ? "OK" : "FAILED"));
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool Radio::ensurePeer(const uint8_t mac[6]) {
|
||||
if (esp_now_is_peer_exist(mac)) return true;
|
||||
esp_now_peer_info_t peer = {};
|
||||
@@ -80,7 +110,8 @@ bool Radio::sendTo(const uint8_t mac[6], ProtocolPacket p) {
|
||||
finalizePacket(p);
|
||||
const bool ok = esp_now_send(mac, reinterpret_cast<const uint8_t *>(&p), sizeof(p)) == ESP_OK;
|
||||
const MessageType type = static_cast<MessageType>(p.type);
|
||||
if (type != MessageType::HEARTBEAT && type != MessageType::HEARTBEAT_ACK && type != MessageType::PROGRESS)
|
||||
if (type != MessageType::DISCOVER && type != MessageType::HEARTBEAT &&
|
||||
type != MessageType::HEARTBEAT_ACK && type != MessageType::PROGRESS)
|
||||
Log::printf("ESP-NOW", "TX %s to %s session=%08lX stage=%u seq=%u %s",
|
||||
messageName(type), peer, p.session, p.stage, p.sequence, ok ? "QUEUED" : "FAILED");
|
||||
return ok;
|
||||
|
||||
@@ -12,6 +12,7 @@ class Radio {
|
||||
bool sendBroadcast(ProtocolPacket packet);
|
||||
bool sendTo(const uint8_t mac[6], ProtocolPacket packet);
|
||||
bool receive(ReceivedPacket &received);
|
||||
bool setWindowedReceive(bool enabled);
|
||||
void flush();
|
||||
uint32_t lastReceiveMs() const { return __atomic_load_n(&lastValidRxMs_, __ATOMIC_RELAXED); }
|
||||
static void macText(const uint8_t mac[6], char *out, size_t size);
|
||||
@@ -28,5 +29,6 @@ class Radio {
|
||||
volatile bool active_ = false;
|
||||
volatile uint32_t lastValidRxMs_ = 0;
|
||||
uint32_t lastChannelCheckMs_ = 0;
|
||||
bool windowedReceive_ = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,17 +8,55 @@
|
||||
|
||||
uint32_t PulseReceiver::tickHz() const {
|
||||
#if OPTICAL_USE_RMT_DMA
|
||||
return CAPTURE_RESOLUTION_HZ;
|
||||
return captureResolutionHz_;
|
||||
#else
|
||||
return cpuTickHz_;
|
||||
#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() {
|
||||
#if OPTICAL_USE_RMT_DMA
|
||||
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 = {};
|
||||
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.flags.invert_in = RX_SIGNAL_INVERTED;
|
||||
#if CONFIG_IDF_TARGET_ESP32S3
|
||||
@@ -30,20 +68,23 @@ bool PulseReceiver::begin() {
|
||||
cfg.mem_block_symbols = RMT_MIN_RECEIVE_SYMBOLS;
|
||||
cfg.flags.with_dma = false; // C3 uses hardware RMT ping-pong partial reception
|
||||
#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;
|
||||
return rmt_rx_register_event_callbacks(channel_, &callbacks, this) == ESP_OK;
|
||||
#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 (rmt_rx_register_event_callbacks(channel_, &callbacks, this) != ESP_OK) {
|
||||
rmt_del_channel(channel_); channel_ = nullptr; return false;
|
||||
}
|
||||
captureResolutionHz_ = resolutionHz;
|
||||
return true;
|
||||
}
|
||||
#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();
|
||||
#if OPTICAL_USE_RMT_DMA
|
||||
// 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);
|
||||
if (rmt_enable(channel_) != ESP_OK) return false;
|
||||
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);
|
||||
// A duration field is 15 bits. Keep the driver's end-of-signal threshold
|
||||
// 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.flags.en_partial_rx = true;
|
||||
if (rmt_receive(channel_, receiveBuffer_,
|
||||
receiveChunkSymbols_ * sizeof(receiveBuffer_[0]), &cfg) != ESP_OK) {
|
||||
rmt_disable(channel_); return false;
|
||||
}
|
||||
#else
|
||||
(void)expectedHz;
|
||||
#endif
|
||||
running_ = true; return true;
|
||||
}
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
class PulseReceiver {
|
||||
public:
|
||||
bool begin();
|
||||
bool start(uint32_t expectedHz);
|
||||
bool start(uint32_t expectedHz, float expectedDutyPct);
|
||||
void stop();
|
||||
void resetStream();
|
||||
size_t readPeriods(PulsePeriod *periods, size_t capacity, TickType_t waitTicks = 0);
|
||||
bool overflowed();
|
||||
uint32_t takeDroppedItems();
|
||||
uint32_t tickHz() const;
|
||||
uint32_t plannedTickHz(uint32_t expectedHz, float expectedDutyPct) const;
|
||||
uint16_t receiveChunkSymbols() const { return receiveChunkSymbols_; }
|
||||
bool highRateBackend() const {
|
||||
#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;
|
||||
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 *);
|
||||
bool configureRmt(uint32_t resolutionHz);
|
||||
bool nextRmtEdge(Edge &edge, TickType_t waitTicks);
|
||||
rmt_channel_handle_t channel_ = nullptr;
|
||||
uint32_t captureResolutionHz_ = 0;
|
||||
rmt_symbol_word_t receiveBuffer_[RMT_MAX_RECEIVE_SYMBOLS];
|
||||
uint16_t receiveChunkSymbols_ = 0;
|
||||
SymbolBlock isrBlock_ = {};
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
#include "Log.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 {
|
||||
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);
|
||||
}
|
||||
|
||||
bool SettingsStore::valid(const Settings &s) const {
|
||||
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.stepIndex < countOf(STEP_OPTIONS_HZ) && s.accuracyIndex < countOf(ACCURACY_OPTIONS_PCT) &&
|
||||
s.timeIndex < countOf(TEST_TIME_OPTIONS_MS) && s.repeatIndex < countOf(REPEAT_OPTIONS) &&
|
||||
s.dutyIndex < countOf(DUTY_OPTIONS_PCT) && s.checksum == settingsChecksum(s) &&
|
||||
s.accuracyIndex < countOf(ACCURACY_OPTIONS_PCT) &&
|
||||
s.timeIndex < countOf(TEST_TIME_OPTIONS_MS) && s.dutyIndex < countOf(DUTY_OPTIONS_PCT) &&
|
||||
s.checksum == settingsChecksum(s) &&
|
||||
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 {
|
||||
return {START_FREQ_OPTIONS_HZ[s.startIndex], END_FREQ_OPTIONS_HZ[s.endIndex],
|
||||
STEP_OPTIONS_HZ[s.stepIndex], ACCURACY_OPTIONS_PCT[s.accuracyIndex],
|
||||
TEST_TIME_OPTIONS_MS[s.timeIndex], REPEAT_OPTIONS[s.repeatIndex],
|
||||
ACCURACY_OPTIONS_PCT[s.accuracyIndex], TEST_TIME_OPTIONS_MS[s.timeIndex],
|
||||
DUTY_OPTIONS_PCT[s.dutyIndex]};
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1347
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
Record=TopLevelDocument|FileName=OptoTest.SchDoc
|
||||
Binary file not shown.
Reference in New Issue
Block a user