Compare commits
6 Commits
a8099bf2b8
...
61d3aeaba6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61d3aeaba6 | ||
|
|
34581913ff | ||
|
|
be1a90afac | ||
|
|
70fe3566b4 | ||
|
|
c952c165f8 | ||
|
|
21f8fe8e13 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1 +1,7 @@
|
||||
/tests/
|
||||
/PCB/Project Logs*/
|
||||
/*/__Previews
|
||||
/*/History
|
||||
/*/Project Logs*/
|
||||
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
118
OpticalChannelTester/Config_Text.h
Normal file
118
OpticalChannelTester/Config_Text.h
Normal file
@@ -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;
|
||||
};
|
||||
|
||||
57
OpticalChannelTester/Font_Cyrillic.h
Normal file
57
OpticalChannelTester/Font_Cyrillic.h
Normal file
@@ -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]};
|
||||
}
|
||||
|
||||
BIN
PCB/Libs/ESP32-C3-SuperMini.PcbLib
Normal file
BIN
PCB/Libs/ESP32-C3-SuperMini.PcbLib
Normal file
Binary file not shown.
BIN
PCB/Libs/ESP32-C3-SuperMini.SchLib
Normal file
BIN
PCB/Libs/ESP32-C3-SuperMini.SchLib
Normal file
Binary file not shown.
BIN
PCB/Libs/ESP32-S3-SuperMini.PcbLib
Normal file
BIN
PCB/Libs/ESP32-S3-SuperMini.PcbLib
Normal file
Binary file not shown.
BIN
PCB/Libs/ESP32-S3-SuperMini.SchLib
Normal file
BIN
PCB/Libs/ESP32-S3-SuperMini.SchLib
Normal file
Binary file not shown.
BIN
PCB/Libs/HFBR-1528Z_HFBR-2528Z.PcbLib
Normal file
BIN
PCB/Libs/HFBR-1528Z_HFBR-2528Z.PcbLib
Normal file
Binary file not shown.
BIN
PCB/Libs/J_ARK2.Schlib
Normal file
BIN
PCB/Libs/J_ARK2.Schlib
Normal file
Binary file not shown.
BIN
PCB/Libs/J_ARK2_2.54.PcbLib
Normal file
BIN
PCB/Libs/J_ARK2_2.54.PcbLib
Normal file
Binary file not shown.
BIN
PCB/Libs/MOD_OLED_0.91_128x32.PcbLib
Normal file
BIN
PCB/Libs/MOD_OLED_0.91_128x32.PcbLib
Normal file
Binary file not shown.
BIN
PCB/Libs/MOD_OLED_0.91_128x32.SchLib
Normal file
BIN
PCB/Libs/MOD_OLED_0.91_128x32.SchLib
Normal file
Binary file not shown.
BIN
PCB/Libs/SN75451BP.PcbLib
Normal file
BIN
PCB/Libs/SN75451BP.PcbLib
Normal file
Binary file not shown.
BIN
PCB/Libs/SN75451BP.SchLib
Normal file
BIN
PCB/Libs/SN75451BP.SchLib
Normal file
Binary file not shown.
BIN
PCB/Libs/Sensor Light.SchLib
Normal file
BIN
PCB/Libs/Sensor Light.SchLib
Normal file
Binary file not shown.
BIN
PCB/Libs/Switch Push Botton.PcbLib
Normal file
BIN
PCB/Libs/Switch Push Botton.PcbLib
Normal file
Binary file not shown.
BIN
PCB/Libs/Switch Push Botton.SchLib
Normal file
BIN
PCB/Libs/Switch Push Botton.SchLib
Normal file
Binary file not shown.
BIN
PCB/OptoTest.PcbDoc
Normal file
BIN
PCB/OptoTest.PcbDoc
Normal file
Binary file not shown.
1347
PCB/OptoTest.PrjPcb
Normal file
1347
PCB/OptoTest.PrjPcb
Normal file
File diff suppressed because one or more lines are too long
1
PCB/OptoTest.PrjPcbStructure
Normal file
1
PCB/OptoTest.PrjPcbStructure
Normal file
@@ -0,0 +1 @@
|
||||
Record=TopLevelDocument|FileName=OptoTest.SchDoc
|
||||
BIN
PCB/OptoTest.SchDoc
Normal file
BIN
PCB/OptoTest.SchDoc
Normal file
Binary file not shown.
Reference in New Issue
Block a user