854 lines
41 KiB
C++
854 lines
41 KiB
C++
#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>
|
|
|
|
namespace {
|
|
const char *appStateName(AppState state) {
|
|
static const char *names[] = {"IDLE", "MENU", "SOLO_MEASURE", "MASTER_DISCOVER",
|
|
"MASTER_WAIT_READY", "MASTER_WAIT_RESULT", "MASTER_FINALIZE", "SLAVE_READY", "SLAVE_WAIT_START",
|
|
"SLAVE_MEASURE", "SLAVE_WAIT_ACK", "FINISHED"};
|
|
const uint8_t index = static_cast<uint8_t>(state);
|
|
return index < sizeof(names) / sizeof(names[0]) ? names[index] : "UNKNOWN";
|
|
}
|
|
|
|
const char *buttonEventName(ButtonEvent event) {
|
|
static const char *names[] = {"NONE", "SHORT", "LONG", "REPEAT"};
|
|
const uint8_t index = static_cast<uint8_t>(event);
|
|
return index < sizeof(names) / sizeof(names[0]) ? names[index] : "UNKNOWN";
|
|
}
|
|
|
|
void formatErrorDuty(float duty, char *out, size_t size) {
|
|
if (fabsf(duty - roundf(duty)) < 0.05f) snprintf(out, size, "%.0f%%", duty);
|
|
else snprintf(out, size, "%.1f%%", duty);
|
|
}
|
|
|
|
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_) {}
|
|
|
|
void App::begin() {
|
|
Serial.begin(SERIAL_BAUD);
|
|
Log::printf("BOOT", "firmware start, Serial=%lu baud", SERIAL_BAUD);
|
|
startButton_.begin(); modeButton_.begin(); pwm_.begin();
|
|
bootCheckStartedMs_ = millis();
|
|
bootResetCandidate_ = startButton_.pressed() && modeButton_.pressed();
|
|
Log::printf("BOOT", "buttons initialized, factory-reset candidate=%s", bootResetCandidate_ ? "YES" : "NO");
|
|
if (!bootResetCandidate_) finishInitialization(false);
|
|
}
|
|
|
|
void App::finishInitialization(bool factoryReset) {
|
|
if (initialized_) return;
|
|
Log::printf("BOOT", "initialization continues, factory-reset=%s", factoryReset ? "YES" : "NO");
|
|
if (factoryReset) {
|
|
store_.defaults(settings_); store_.save(settings_); Log::event("BOOT", "FACTORY DEFAULTS RESTORED");
|
|
} else if (!store_.load(settings_)) {
|
|
store_.save(settings_); Log::event("BOOT", "NVS invalid/missing: defaults loaded");
|
|
}
|
|
params_ = store_.params(settings_);
|
|
if (!display_.begin()) Log::event("BOOT", "OLED unavailable; Serial UI remains fully operational");
|
|
initialized_ = true;
|
|
if (!receiver_.begin()) { Log::event("BOOT", "FATAL: capture peripheral init failed"); finish(false, FailReason::UNSUPPORTED); return; }
|
|
Log::printf("BOOT", "capture initialized: %s", receiver_.highRateBackend() ? "RMT DMA" : "RMT ping-pong");
|
|
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);
|
|
if (startEvent != ButtonEvent::NONE)
|
|
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);
|
|
return;
|
|
}
|
|
if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED &&
|
|
startEvent == ButtonEvent::LONG) { abortTest(); return; }
|
|
if (state_ != AppState::IDLE && state_ != AppState::MENU && state_ != AppState::FINISHED &&
|
|
modeEvent != ButtonEvent::NONE) Log::event("ACTION", "MODE ignored while test is active");
|
|
|
|
if (state_ == AppState::IDLE || state_ == AppState::FINISHED) {
|
|
if (modeEvent == ButtonEvent::SHORT) {
|
|
settings_.role = (settings_.role + 1U) % 3U; const bool saved = store_.save(settings_);
|
|
params_ = store_.params(settings_);
|
|
if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
|
|
else showIdle();
|
|
Log::printf("ACTION", "role changed to %s, NVS=%s", roleName(static_cast<Role>(settings_.role)), saved ? "OK" : "FAILED");
|
|
} else if (modeEvent == ButtonEvent::LONG) {
|
|
state_ = AppState::MENU; menuItem_ = 0; Log::event("ACTION", "settings menu entered"); showMenu();
|
|
} else if (startEvent == ButtonEvent::SHORT) { Log::event("ACTION", "test start requested"); startTest(); }
|
|
else if (state_ == AppState::FINISHED && static_cast<Role>(settings_.role) == Role::SLAVE &&
|
|
now >= slaveRearmAtMs_) armSlave(pendingReason_ != FailReason::NONE);
|
|
return;
|
|
}
|
|
if (state_ == AppState::SLAVE_READY && modeEvent != ButtonEvent::NONE) {
|
|
radio_.end(); havePeer_ = false;
|
|
if (modeEvent == ButtonEvent::SHORT) {
|
|
settings_.role = static_cast<uint8_t>(Role::SOLO); const bool saved = store_.save(settings_);
|
|
params_ = store_.params(settings_); state_ = AppState::IDLE; showIdle();
|
|
Log::printf("ACTION", "role changed to SOLO, NVS=%s", saved ? "OK" : "FAILED");
|
|
} else if (modeEvent == ButtonEvent::LONG) {
|
|
state_ = AppState::MENU; menuItem_ = 0; showMenu();
|
|
}
|
|
return;
|
|
}
|
|
if (state_ == AppState::MENU) {
|
|
if (modeEvent == ButtonEvent::SHORT) {
|
|
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_);
|
|
Log::printf("ACTION", "settings menu saved and closed, NVS=%s", saved ? "OK" : "FAILED");
|
|
state_ = AppState::IDLE; printConfiguration();
|
|
if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
|
|
else showIdle();
|
|
} else if (startEvent == ButtonEvent::SHORT) changeMenu(+1);
|
|
else if (startEvent == ButtonEvent::LONG || startEvent == ButtonEvent::REPEAT) changeMenu(-1);
|
|
return;
|
|
}
|
|
if (state_ == AppState::SOLO_MEASURE) {
|
|
const MeasureState ms = measurement_.update();
|
|
if (ms == MeasureState::FAIL) {
|
|
printStageStats(measurement_.stats(), actual_.actualHz);
|
|
showStageResult(measurement_.stats());
|
|
finish(false, measurement_.reason(), true);
|
|
}
|
|
else if (ms == MeasureState::PASS) {
|
|
printStageStats(measurement_.stats(), actual_.actualHz);
|
|
showStageResult(measurement_.stats());
|
|
stagePassed();
|
|
} else if (ms == MeasureState::STEP_READY) {
|
|
StageStats live = {};
|
|
if (measurement_.statsSnapshot(live)) showStageResult(live);
|
|
measurement_.continueAfterDisplay();
|
|
}
|
|
} else if (state_ == AppState::MASTER_DISCOVER || state_ == AppState::MASTER_WAIT_READY ||
|
|
state_ == AppState::MASTER_WAIT_RESULT || state_ == AppState::MASTER_FINALIZE) {
|
|
handleRadio(); updateMaster();
|
|
} else {
|
|
handleRadio(); updateSlave();
|
|
}
|
|
}
|
|
|
|
void App::showIdle() {
|
|
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);
|
|
}
|
|
|
|
void App::changeMenu(int d) {
|
|
sanitizeRange();
|
|
uint8_t *value = nullptr; size_t count = 0;
|
|
switch (menuItem_) {
|
|
case 0: value = &settings_.startIndex; count = countOf(START_FREQ_OPTIONS_HZ); break;
|
|
case 1: value = &settings_.endIndex; count = countOf(END_FREQ_OPTIONS_HZ); break;
|
|
case 2: value = &settings_.accuracyIndex; count = countOf(ACCURACY_OPTIONS_PCT); break;
|
|
case 3: value = &settings_.timeIndex; count = countOf(TEST_TIME_OPTIONS_MS); break;
|
|
default: value = &settings_.dutyIndex; count = countOf(DUTY_OPTIONS_PCT); break;
|
|
}
|
|
*value = static_cast<uint8_t>((*value + count + d) % count);
|
|
Log::printf("ACTION", "menu item=%u changed direction=%+d new-index=%u", menuItem_, d, *value);
|
|
sanitizeRange(); params_ = store_.params(settings_); showMenu();
|
|
}
|
|
|
|
void App::showMenu() {
|
|
char one[64], value[24], total[64], all[12];
|
|
const char *label = nullptr;
|
|
Display::formatDuration(actualNominalTotalUs(), all, sizeof(all));
|
|
switch (menuItem_) {
|
|
case 0:
|
|
Display::formatTestFrequency(params_.startHz, value, sizeof(value));
|
|
strncat(value, UiText::FREQUENCY_UNIT, sizeof(value) - strlen(value) - 1U);
|
|
label = UiText::MENU_START_FREQUENCY;
|
|
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;
|
|
}
|
|
formatMenuLine(label, value, one, sizeof(one));
|
|
formatMenuLine(UiText::MENU_TOTAL_TIME, all, total, sizeof(total));
|
|
display_.show(one, total);
|
|
}
|
|
|
|
void App::startTest() {
|
|
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];
|
|
Display::formatFrequency(params_.startHz, startText, sizeof(startText));
|
|
Display::formatFrequency(params_.endHz, endText, sizeof(endText));
|
|
Log::printf("CONFIG", "mode=%s range=%s..%s adjacent accuracy=%.2f%% time=%lums duty=%u%% stages=%lu",
|
|
roleName(static_cast<Role>(settings_.role)), startText, endText,
|
|
params_.accuracyPct, params_.testTimeMs, params_.dutyPct, stageCount_);
|
|
}
|
|
printConfiguration();
|
|
const Role role = static_cast<Role>(settings_.role);
|
|
if (role == Role::SOLO) {
|
|
if (!prepareStage()) return;
|
|
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(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);
|
|
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(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(UiText::SLAVE_READY, UiText::WAIT_MASTER);
|
|
return true;
|
|
}
|
|
|
|
bool App::prepareStage(bool showProgress) {
|
|
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);
|
|
if (requestedHz_ > maxHz) { finish(false, FailReason::UNSUPPORTED); return false; }
|
|
Log::printf("PWM", "starting GPIO=%u requested=%luHz duty=%u%%", GPIO_PWM, requestedHz_, params_.dutyPct);
|
|
if (!pwm_.start(requestedHz_, params_.dutyPct, actual_)) {
|
|
Log::printf("PWM", "START FAILED GPIO=%u requested=%luHz; LEDC attach/write/read failed",
|
|
GPIO_PWM, requestedHz_);
|
|
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,
|
|
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, plannedRxHz,
|
|
effectiveTolerancePct(params_.accuracyPct));
|
|
finish(false, resolution); return false;
|
|
}
|
|
Log::printf("PWM", "stage=%lu/%lu requested=%luHz actual=%luHz duty=%.2f%% bits=%u STARTED",
|
|
stageIndex_ + 1, stageCount_, requestedHz_, actual_.actualHz, actual_.actualDutyPct, actual_.bits);
|
|
if (showProgress) showStageProgress();
|
|
if (static_cast<Role>(settings_.role) == Role::SOLO && !startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) {
|
|
finish(false, FailReason::UNSUPPORTED); return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool App::startLocalMeasurement(float hz, float duty) {
|
|
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;
|
|
}
|
|
|
|
void App::stagePassed() {
|
|
Log::printf("TEST", "stage %lu/%lu PASS; PWM stopping", stageIndex_ + 1, stageCount_);
|
|
pwm_.stop();
|
|
if (++stageIndex_ >= stageCount_) { finish(true, FailReason::NONE); return; }
|
|
if (static_cast<Role>(settings_.role) == Role::SOLO) { if (prepareStage()) state_ = AppState::SOLO_MEASURE; }
|
|
else if (static_cast<Role>(settings_.role) == Role::MASTER) {
|
|
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_);
|
|
actual_ = {};
|
|
stageStartConfirmed_ = false;
|
|
pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE);
|
|
state_ = AppState::MASTER_WAIT_READY; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS;
|
|
}
|
|
}
|
|
|
|
void App::startMasterDiscovery() {
|
|
session_ = esp_random(); if (!session_) session_ = 1;
|
|
sequence_ = 1; stageIndex_ = 0; requestedHz_ = 0; havePeer_ = false; radio_.flush();
|
|
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(UiText::MASTER_SEARCH, UiText::HOLD_START_STOP);
|
|
}
|
|
|
|
ProtocolPacket App::makePacket(MessageType type) const {
|
|
ProtocolPacket p = {};
|
|
p.type = static_cast<uint8_t>(type); p.session = session_; p.stage = stageIndex_;
|
|
p.stageCount = static_cast<uint16_t>(stageCount_); p.sequence = sequence_;
|
|
p.requestedHz = requestedHz_; p.actualHz = actual_.actualHz;
|
|
const float packetDuty = actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct;
|
|
p.actualDutyX100 = static_cast<uint16_t>(packetDuty * 100.0f + 0.5f);
|
|
p.testTimeMs = params_.testTimeMs;
|
|
p.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f); p.settleCycles = PWM_SETTLE_CYCLES;
|
|
return p;
|
|
}
|
|
|
|
void App::sendCurrent(MessageType type) {
|
|
++sequence_; pendingPacket_ = makePacket(type);
|
|
const bool ok = sendLinked(pendingPacket_); lastSendMs_ = millis();
|
|
if (!ok) Log::printf("ESP-NOW", "sendCurrent %s FAILED", messageName(type));
|
|
}
|
|
|
|
bool App::sendLinked(ProtocolPacket packet) {
|
|
return havePeer_ ? radio_.sendTo(peer_, packet) : radio_.sendBroadcast(packet);
|
|
}
|
|
|
|
void App::updateHeartbeat() {
|
|
if (!havePeer_ || state_ == AppState::FINISHED) return;
|
|
const uint32_t now = millis();
|
|
const uint32_t radioRxMs = radio_.lastReceiveMs();
|
|
if (radioRxMs && now - radioRxMs < now - lastPeerSeenMs_) lastPeerSeenMs_ = radioRxMs;
|
|
if (now - lastPeerSeenMs_ >= LINK_HEARTBEAT_TIMEOUT_MS) {
|
|
Log::event("ESP-NOW", "peer heartbeat timeout");
|
|
finish(false, FailReason::LINK_LOST);
|
|
return;
|
|
}
|
|
if (static_cast<Role>(settings_.role) == Role::MASTER &&
|
|
now - lastHeartbeatMs_ >= LINK_HEARTBEAT_INTERVAL_MS) {
|
|
ProtocolPacket heartbeat = makePacket(MessageType::HEARTBEAT);
|
|
heartbeat.sequence = sequence_;
|
|
sendLinked(heartbeat);
|
|
lastHeartbeatMs_ = now;
|
|
}
|
|
}
|
|
|
|
bool App::packetForCurrent(const ProtocolPacket &p) const {
|
|
return p.session == session_ && p.stage == stageIndex_;
|
|
}
|
|
|
|
void App::handleRadio() {
|
|
ReceivedPacket r;
|
|
while (radio_.receive(r)) {
|
|
const MessageType type = static_cast<MessageType>(r.packet.type);
|
|
if (type != MessageType::HEARTBEAT && type != MessageType::HEARTBEAT_ACK && type != MessageType::PROGRESS &&
|
|
state_ != AppState::SLAVE_MEASURE)
|
|
Log::printf("ESP-NOW", "RX %s session=%08lX stage=%u seq=%u",
|
|
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(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, 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;
|
|
}
|
|
if (havePeer_ && !memcmp(peer_, r.mac, 6) && r.packet.session == session_) lastPeerSeenMs_ = millis();
|
|
if (havePeer_ && !memcmp(peer_, r.mac, 6) && r.packet.session == session_ &&
|
|
type == MessageType::HEARTBEAT) {
|
|
continue; // Radio's priority heartbeat task has already sent the ACK.
|
|
}
|
|
if (type == MessageType::HEARTBEAT_ACK) continue;
|
|
if (havePeer_ && !memcmp(peer_, r.mac, 6) && type == MessageType::RESULT &&
|
|
r.packet.session == session_ && r.packet.stage < stageIndex_) {
|
|
ProtocolPacket ack = {}; ack.type = static_cast<uint8_t>(MessageType::ACK);
|
|
ack.session = session_; ack.stage = r.packet.stage; ack.sequence = r.packet.sequence;
|
|
sendLinked(ack); continue; // idempotent ACK for a retried old result
|
|
}
|
|
const bool matchingPeer = havePeer_ && !memcmp(peer_, r.mac, 6) && r.packet.session == session_;
|
|
if (matchingPeer && type == MessageType::PREPARE) {
|
|
const bool expected = state_ == AppState::SLAVE_WAIT_START && r.packet.stage == stageIndex_;
|
|
const bool implicitAck = state_ == AppState::SLAVE_WAIT_ACK && pendingPacket_.passed &&
|
|
r.packet.stage == static_cast<uint16_t>(pendingPacket_.stage + 1U);
|
|
if (expected || implicitAck) {
|
|
stageIndex_ = r.packet.stage;
|
|
sequence_ = r.packet.sequence;
|
|
state_ = AppState::SLAVE_WAIT_START;
|
|
params_.testTimeMs = r.packet.testTimeMs;
|
|
params_.accuracyPct = r.packet.accuracyX100 / 100.0f; requestedHz_ = r.packet.requestedHz;
|
|
stageCount_ = r.packet.stageCount;
|
|
actual_ = {};
|
|
ProtocolPacket ready = makePacket(MessageType::READY);
|
|
ready.sequence = r.packet.sequence; sendLinked(ready);
|
|
}
|
|
continue;
|
|
}
|
|
if (!havePeer_ || memcmp(peer_, r.mac, 6) || !packetForCurrent(r.packet)) continue;
|
|
if (type == MessageType::ABORT) {
|
|
const FailReason reason = r.packet.reason > static_cast<uint8_t>(FailReason::NONE) &&
|
|
r.packet.reason <= static_cast<uint8_t>(FailReason::ABORTED)
|
|
? static_cast<FailReason>(r.packet.reason) : FailReason::ABORTED;
|
|
if (r.packet.requestedHz) requestedHz_ = r.packet.requestedHz;
|
|
actual_.actualHz = r.packet.actualHz ? r.packet.actualHz : requestedHz_;
|
|
actual_.actualDutyPct = r.packet.actualDutyX100 ? r.packet.actualDutyX100 / 100.0f : params_.dutyPct;
|
|
measurement_.abort(); finish(false, reason); continue;
|
|
}
|
|
if (state_ == AppState::MASTER_WAIT_READY && type == MessageType::READY) {
|
|
if (!prepareStage(false)) continue;
|
|
sendCurrent(MessageType::START_STAGE); state_ = AppState::MASTER_WAIT_RESULT;
|
|
stageStartConfirmed_ = false; retries_ = 0;
|
|
deadlineMs_ = millis() + LINK_RETRY_INTERVAL_MS;
|
|
} else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::READY &&
|
|
r.packet.sequence == pendingPacket_.sequence) {
|
|
if (!stageStartConfirmed_) showStageProgress();
|
|
stageStartConfirmed_ = true;
|
|
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() + 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;
|
|
ack.passed = r.packet.passed && stageIndex_ + 1U >= stageCount_;
|
|
sendLinked(ack); pwm_.stop();
|
|
showRemoteResult(r.packet);
|
|
if (!r.packet.passed) {
|
|
finish(false, static_cast<FailReason>(r.packet.reason), true);
|
|
} else if (ack.passed) {
|
|
pendingPacket_ = ack;
|
|
state_ = AppState::MASTER_FINALIZE; retries_ = 0;
|
|
deadlineMs_ = millis() + FINAL_ACK_RETRY_INTERVAL_MS;
|
|
} else stagePassed();
|
|
} else if (state_ == AppState::MASTER_FINALIZE && type == MessageType::RESULT) {
|
|
// The Slave did not receive the final ACK and repeated RESULT.
|
|
sendLinked(pendingPacket_);
|
|
} else if (state_ == AppState::SLAVE_WAIT_START && type == MessageType::START_STAGE) {
|
|
sequence_ = r.packet.sequence;
|
|
actual_.actualHz = r.packet.actualHz; actual_.actualDutyPct = r.packet.actualDutyX100 / 100.0f;
|
|
if (!startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { finish(false, FailReason::UNSUPPORTED); continue; }
|
|
showStageProgress();
|
|
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) {
|
|
// START_STAGE or its acknowledgement was lost. Do not restart the
|
|
// measurement; only confirm the already running stage again.
|
|
ProtocolPacket started = makePacket(MessageType::READY);
|
|
started.sequence = r.packet.sequence; sendLinked(started);
|
|
} else if (state_ == AppState::SLAVE_WAIT_ACK && type == MessageType::ACK && r.packet.sequence == pendingPacket_.sequence) {
|
|
if (pendingPacket_.passed) {
|
|
if (r.packet.passed) {
|
|
radio_.end(); pendingReason_ = FailReason::NONE;
|
|
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;
|
|
}
|
|
}
|
|
else finish(false, static_cast<FailReason>(pendingPacket_.reason), true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void App::updateMaster() {
|
|
const uint32_t now = millis();
|
|
if (state_ == AppState::MASTER_DISCOVER) {
|
|
if (now - lastSendMs_ >= DISCOVERY_RETRY_INTERVAL_MS) {
|
|
if (++retries_ % 50U == 0U) Log::event("ESP-NOW", "DISCOVER burst continues");
|
|
radio_.sendBroadcast(pendingPacket_);
|
|
lastSendMs_ = now;
|
|
}
|
|
return;
|
|
}
|
|
if (state_ == AppState::MASTER_FINALIZE) {
|
|
if (now < deadlineMs_) return;
|
|
if (retries_++ < FINAL_ACK_RETRIES) {
|
|
sendLinked(pendingPacket_);
|
|
deadlineMs_ = now + FINAL_ACK_RETRY_INTERVAL_MS;
|
|
} else finish(true, FailReason::NONE);
|
|
return;
|
|
}
|
|
updateHeartbeat();
|
|
if (state_ == AppState::FINISHED) return;
|
|
if (now < deadlineMs_) return;
|
|
if (retries_ >= LINK_PACKET_RETRIES) { finish(false, FailReason::LINK_LOST); return; }
|
|
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_ ? stageWallTimeMs(params_.testTimeMs, actual_.actualHz) + LINK_REPLY_TIMEOUT_MS : LINK_RETRY_INTERVAL_MS) :
|
|
LINK_REPLY_TIMEOUT_MS);
|
|
}
|
|
|
|
void App::updateSlave() {
|
|
updateHeartbeat();
|
|
if (state_ == AppState::FINISHED) return;
|
|
if (state_ == AppState::SLAVE_MEASURE) {
|
|
const MeasureState ms = measurement_.update();
|
|
if (ms == MeasureState::STEP_READY) {
|
|
StageStats live = {};
|
|
if (measurement_.statsSnapshot(live)) {
|
|
ProtocolPacket progress = makePacket(MessageType::PROGRESS);
|
|
progress.progressStep = measurement_.progressStep();
|
|
fillMeasuredResult(progress, live);
|
|
progress.sequence = sequence_; sendLinked(progress);
|
|
showStageResult(live);
|
|
}
|
|
measurement_.continueAfterDisplay();
|
|
return;
|
|
}
|
|
if (ms != MeasureState::PASS && ms != MeasureState::FAIL) {
|
|
return;
|
|
}
|
|
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());
|
|
pendingPacket_.minPeriodTicks = measurement_.stats().minPeriod; pendingPacket_.maxPeriodTicks = measurement_.stats().maxPeriod;
|
|
pendingPacket_.sequence = ++sequence_; sendLinked(pendingPacket_);
|
|
Log::printf("TEST", "Slave result prepared: %s reason=%s periods=%lu",
|
|
pendingPacket_.passed ? "PASS" : "FAIL", failName(static_cast<FailReason>(pendingPacket_.reason)), pendingPacket_.periods);
|
|
state_ = AppState::SLAVE_WAIT_ACK; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS;
|
|
} else if (state_ == AppState::SLAVE_WAIT_ACK && millis() >= deadlineMs_) {
|
|
if (retries_++ >= LINK_PACKET_RETRIES) finish(false, FailReason::LINK_LOST);
|
|
else { Log::printf("ESP-NOW", "RESULT retry=%u", retries_); sendLinked(pendingPacket_); deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS; }
|
|
}
|
|
}
|
|
|
|
void App::sendAbort(FailReason reason) {
|
|
if (!havePeer_) return;
|
|
++sequence_;
|
|
ProtocolPacket packet = makePacket(MessageType::ABORT);
|
|
packet.reason = static_cast<uint8_t>(reason);
|
|
if (!packet.actualDutyX100) packet.actualDutyX100 = params_.dutyPct * 100U;
|
|
sendLinked(packet);
|
|
}
|
|
|
|
void App::abortTest() {
|
|
Log::event("ACTION", "abort requested: sending ABORT, stopping receiver and PWM");
|
|
sendAbort(FailReason::ABORTED); measurement_.abort(); finish(false, FailReason::ABORTED);
|
|
}
|
|
|
|
void App::finish(bool pass, FailReason reason, bool preserveDisplay) {
|
|
Log::printf("TEST", "finishing result=%s reason=%s", pass ? "PASS" : "FAIL", failName(reason));
|
|
const AppState failedState = state_;
|
|
const bool masterLinkLost = reason == FailReason::LINK_LOST &&
|
|
(failedState == AppState::MASTER_DISCOVER || failedState == AppState::MASTER_WAIT_READY ||
|
|
failedState == AppState::MASTER_WAIT_RESULT || failedState == AppState::MASTER_FINALIZE);
|
|
const bool masterActive = failedState == AppState::MASTER_DISCOVER ||
|
|
failedState == AppState::MASTER_WAIT_READY || failedState == AppState::MASTER_WAIT_RESULT ||
|
|
failedState == AppState::MASTER_FINALIZE;
|
|
const bool slaveLinkLost = reason == FailReason::LINK_LOST &&
|
|
(failedState == AppState::SLAVE_READY || failedState == AppState::SLAVE_WAIT_START ||
|
|
failedState == AppState::SLAVE_MEASURE || failedState == AppState::SLAVE_WAIT_ACK);
|
|
pwm_.stop(); receiver_.stop();
|
|
if (masterLinkLost) {
|
|
Log::event("ESP-NOW", "link lost; returning to continuous discovery");
|
|
startMasterDiscovery();
|
|
return;
|
|
}
|
|
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[64];
|
|
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, target, sizeof(target));
|
|
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target,
|
|
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct);
|
|
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[64];
|
|
if (pass) {
|
|
const Role role = static_cast<Role>(settings_.role);
|
|
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), UiText::FAIL_FORMAT, frequency,
|
|
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct);
|
|
display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_);
|
|
} else {
|
|
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");
|
|
}
|
|
}
|
|
|
|
void App::printConfiguration() {
|
|
if (SERIAL_MINIMAL_LOG) return;
|
|
const char *board = TARGET_IS_C3 ? "ESP32-C3" : "ESP32-S3";
|
|
uint8_t mac[6] = {}; esp_read_mac(mac, ESP_MAC_WIFI_STA);
|
|
Serial.printf("\nOptical Channel Tester | %s | mode=%s\n", board, roleName(static_cast<Role>(settings_.role)));
|
|
Serial.printf("MAC=%02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
|
Serial.printf("GPIO PWM=%u RX=%u START=%u MODE=%u SDA=%u SCL=%u\n", GPIO_PWM, GPIO_RX,
|
|
GPIO_BUTTON_START, GPIO_BUTTON_MODE, GPIO_SDA, GPIO_SCL);
|
|
Serial.printf("Test %lu..%lu Hz (adjacent exact frequencies), accuracy %.2f%%, %lums, duty %u%%\n",
|
|
params_.startHz, params_.endHz, params_.accuracyPct, params_.testTimeMs, params_.dutyPct);
|
|
stageCount_ = frequencyPointCount(params_.startHz, params_.endHz);
|
|
Serial.printf("Frequencies (%lu): ", stageCount_);
|
|
for (uint32_t i = 0; i < stageCount_; ++i) Serial.printf("%lu%s", frequencyAt(params_.startHz, params_.endHz, i), i + 1 == stageCount_ ? "\n" : ",");
|
|
Serial.printf("ALL nominal: %llu us | RX=%s\n", actualNominalTotalUs(), receiver_.highRateBackend() ? "RMT DMA" : "RMT ping-pong");
|
|
}
|
|
|
|
uint64_t App::actualNominalTotalUs() {
|
|
// ALL is only an estimate. Do not attach/detach LEDC for every frequency:
|
|
// large sweeps can perform hundreds of unnecessary driver reconfigurations
|
|
// immediately before the real test and leave no observable PWM on failure.
|
|
return nominalTotalUs(params_, PWM_SETTLE_CYCLES);
|
|
}
|
|
|
|
void App::printStageStats(const StageStats &s, uint32_t hz) {
|
|
if (!s.periods) return;
|
|
const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum;
|
|
const float measuredDuty = 100.0f * s.activeSum / s.periodSum;
|
|
char requestedText[12], measuredText[12];
|
|
Display::formatFrequency(hz, requestedText, sizeof(requestedText));
|
|
Display::formatFrequency(measuredHz, measuredText, sizeof(measuredText));
|
|
const char *status = s.reason == FailReason::NONE ? "PASS" : "FAIL";
|
|
Log::printf("RESULT", "%s %s periods=%lu measured=%s duty=%.2f%% skipped=%lu%s%s",
|
|
requestedText, status, s.periods, measuredText, measuredDuty, s.droppedItems,
|
|
s.reason == FailReason::NONE ? "" : " reason=", s.reason == FailReason::NONE ? "" : failName(s.reason));
|
|
}
|
|
|
|
void App::showStageResult(const StageStats &s) {
|
|
char one[64], two[64];
|
|
char target[12]; Display::formatTestFrequency(actual_.actualHz, target, sizeof(target));
|
|
if (s.reason != FailReason::NONE) {
|
|
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), UiText::PERIOD_OUT_FORMAT, frequency);
|
|
} else if (s.reason == FailReason::DUTY_OUT && s.badFrequency > 0.0f) {
|
|
char duty[10]; formatErrorDuty(s.badDuty, duty, sizeof(duty));
|
|
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, duty);
|
|
} else {
|
|
snprintf(two, sizeof(two), "%s", uiFailName(s.reason));
|
|
}
|
|
display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()),
|
|
overallProgressTotal(stageCount_));
|
|
return;
|
|
}
|
|
char stage[12]; snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_);
|
|
snprintf(one, sizeof(one), UiText::TEST_FORMAT, target, actual_.actualDutyPct, stage);
|
|
if (!s.periods || !s.periodSum) {
|
|
display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, measurement_.progressStep()),
|
|
overallProgressTotal(stageCount_));
|
|
return;
|
|
}
|
|
const float measuredHz = static_cast<float>(receiver_.tickHz()) * s.periods / s.periodSum;
|
|
const float measuredDuty = 100.0f * s.activeSum / s.periodSum;
|
|
char frequency[12]; Display::formatFrequency(measuredHz, frequency, sizeof(frequency));
|
|
snprintf(two, sizeof(two), "F:%-8s D:%4.1f%%", frequency, measuredDuty);
|
|
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[64], two[64];
|
|
Display::formatTestFrequency(packet.actualHz ? packet.actualHz : packet.requestedHz,
|
|
target, sizeof(target));
|
|
if (reason == FailReason::NONE) {
|
|
char stage[12]; snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_);
|
|
snprintf(one, sizeof(one), UiText::TEST_FORMAT,
|
|
target, packet.actualDutyX100 / 100.0f, stage);
|
|
if (packet.measuredHzX10) {
|
|
char measured[12];
|
|
Display::formatFrequency(packet.measuredHzX10 / 10.0f, measured, sizeof(measured));
|
|
snprintf(two, sizeof(two), "F:%-8s D:%4.1f%%", measured, packet.measuredDutyX10 / 10.0f);
|
|
} else snprintf(two, sizeof(two), "%s", UiText::NO_MEASUREMENT);
|
|
} else if (reason == FailReason::PERIOD_OUT && packet.measuredHzX10) {
|
|
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
|
|
char measured[12];
|
|
Display::formatTestFrequency((packet.measuredHzX10 + 5U) / 10U, measured, sizeof(measured));
|
|
snprintf(two, sizeof(two), UiText::PERIOD_OUT_FORMAT, measured);
|
|
} else if (reason == FailReason::DUTY_OUT && packet.measuredDutyX10) {
|
|
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
|
|
char duty[10]; formatErrorDuty(packet.measuredDutyX10 / 10.0f, duty, sizeof(duty));
|
|
snprintf(two, sizeof(two), UiText::DUTY_OUT_FORMAT, duty);
|
|
} else {
|
|
snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, packet.actualDutyX100 / 100.0f);
|
|
snprintf(two, sizeof(two), "%s", uiFailName(reason));
|
|
}
|
|
display_.show(one, two, overallProgress(stageIndex_, packet.progressStep),
|
|
overallProgressTotal(stageCount_));
|
|
}
|
|
|
|
void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const {
|
|
packet.reason = static_cast<uint8_t>(stats.reason);
|
|
packet.periods = stats.periods;
|
|
if (!stats.periods || !stats.periodSum) return;
|
|
const bool badPeriod = (stats.reason == FailReason::PERIOD_OUT || stats.reason == FailReason::DUTY_OUT) &&
|
|
stats.badFrequency > 0.0f;
|
|
const float measuredHz = badPeriod ? stats.badFrequency :
|
|
static_cast<float>(receiver_.tickHz()) * stats.periods / stats.periodSum;
|
|
const float measuredDuty = badPeriod ? stats.badDuty : 100.0f * stats.activeSum / stats.periodSum;
|
|
packet.measuredHzX10 = static_cast<uint32_t>(lroundf(measuredHz * 10.0f));
|
|
packet.measuredDutyX10 = static_cast<uint16_t>(lroundf(measuredDuty * 10.0f));
|
|
}
|
|
|
|
void App::showStageProgress() {
|
|
char target[12], one[64], stage[12];
|
|
Display::formatTestFrequency(actual_.actualHz, target, sizeof(target));
|
|
snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_);
|
|
snprintf(one, sizeof(one), UiText::TEST_FORMAT, target, actual_.actualDutyPct, stage);
|
|
display_.show(one, UiText::NO_MEASUREMENT, overallProgress(stageIndex_, 0),
|
|
overallProgressTotal(stageCount_));
|
|
}
|