запущен тест между есп с перемычкой
This commit is contained in:
@@ -4,12 +4,13 @@
|
||||
#include <WiFi.h>
|
||||
#include <esp_mac.h>
|
||||
#include <esp_system.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", "SLAVE_READY", "SLAVE_WAIT_START",
|
||||
"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";
|
||||
@@ -20,6 +21,11 @@ const char *buttonEventName(ButtonEvent event) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
App::App() : startButton_(GPIO_BUTTON_START), modeButton_(GPIO_BUTTON_MODE), measurement_(receiver_) {}
|
||||
@@ -81,7 +87,7 @@ void App::update() {
|
||||
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();
|
||||
now >= slaveRearmAtMs_) armSlave(pendingReason_ != FailReason::NONE);
|
||||
return;
|
||||
}
|
||||
if (state_ == AppState::SLAVE_READY && modeEvent != ButtonEvent::NONE) {
|
||||
@@ -111,13 +117,22 @@ void App::update() {
|
||||
}
|
||||
if (state_ == AppState::SOLO_MEASURE) {
|
||||
const MeasureState ms = measurement_.update();
|
||||
if (ms == MeasureState::FAIL) { printStageStats(measurement_.stats(), actual_.actualHz); finish(false, measurement_.reason()); }
|
||||
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_WAIT_RESULT || state_ == AppState::MASTER_FINALIZE) {
|
||||
handleRadio(); updateMaster();
|
||||
} else {
|
||||
handleRadio(); updateSlave();
|
||||
@@ -195,7 +210,7 @@ void App::startTest() {
|
||||
else { state_ = AppState::SLAVE_READY; Log::event("TEST", "Slave armed and waiting for Master"); display_.show("SLAVE READY", "WAIT MASTER"); }
|
||||
}
|
||||
|
||||
bool App::armSlave() {
|
||||
bool App::armSlave(bool preserveDisplay) {
|
||||
params_ = store_.params(settings_);
|
||||
stageIndex_ = 0; stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz);
|
||||
requestedHz_ = 0; session_ = 0; sequence_ = 0; havePeer_ = false;
|
||||
@@ -208,12 +223,13 @@ bool App::armSlave() {
|
||||
}
|
||||
radio_.flush(); state_ = AppState::SLAVE_READY;
|
||||
Log::event("TEST", "Slave automatically armed and waiting for Master");
|
||||
display_.show("SLAVE READY", "WAIT MASTER");
|
||||
if (!preserveDisplay) display_.show("SLAVE READY", "WAIT MASTER");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool App::prepareStage() {
|
||||
bool App::prepareStage(bool showProgress) {
|
||||
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, 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; }
|
||||
@@ -232,9 +248,7 @@ bool App::prepareStage() {
|
||||
}
|
||||
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);
|
||||
char f[12], one[24], two[24]; Display::formatFrequency(actual_.actualHz, f, sizeof(f));
|
||||
snprintf(one, sizeof(one), "F %s D %.1f%%", f, actual_.actualDutyPct);
|
||||
snprintf(two, sizeof(two), "%lu/%lu RUN", stageIndex_ + 1, stageCount_); display_.show(one, two);
|
||||
if (showProgress) showStageProgress();
|
||||
if (static_cast<Role>(settings_.role) == Role::SOLO && !startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) {
|
||||
finish(false, FailReason::UNSUPPORTED); return false;
|
||||
}
|
||||
@@ -257,6 +271,8 @@ void App::stagePassed() {
|
||||
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_);
|
||||
actual_ = {};
|
||||
stageStartConfirmed_ = false;
|
||||
pendingPacket_ = makePacket(MessageType::PREPARE); sendCurrent(MessageType::PREPARE);
|
||||
state_ = AppState::MASTER_WAIT_READY; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS;
|
||||
}
|
||||
@@ -273,9 +289,11 @@ void App::startMasterDiscovery() {
|
||||
|
||||
ProtocolPacket App::makePacket(MessageType type) const {
|
||||
ProtocolPacket p = {};
|
||||
p.type = static_cast<uint8_t>(type); p.session = session_; p.stage = stageIndex_; p.sequence = sequence_;
|
||||
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;
|
||||
p.actualDutyX100 = static_cast<uint16_t>(actual_.actualDutyPct * 100.0f + 0.5f);
|
||||
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.accuracyX100 = static_cast<uint16_t>(params_.accuracyPct * 100.0f + 0.5f); p.settleCycles = PWM_SETTLE_CYCLES;
|
||||
return p;
|
||||
@@ -283,13 +301,19 @@ ProtocolPacket App::makePacket(MessageType type) const {
|
||||
|
||||
void App::sendCurrent(MessageType type) {
|
||||
++sequence_; pendingPacket_ = makePacket(type);
|
||||
const bool ok = radio_.sendBroadcast(pendingPacket_); lastSendMs_ = millis();
|
||||
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);
|
||||
@@ -299,7 +323,7 @@ void App::updateHeartbeat() {
|
||||
now - lastHeartbeatMs_ >= LINK_HEARTBEAT_INTERVAL_MS) {
|
||||
ProtocolPacket heartbeat = makePacket(MessageType::HEARTBEAT);
|
||||
heartbeat.sequence = sequence_;
|
||||
radio_.sendBroadcast(heartbeat);
|
||||
sendLinked(heartbeat);
|
||||
lastHeartbeatMs_ = now;
|
||||
}
|
||||
}
|
||||
@@ -312,7 +336,7 @@ 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 &&
|
||||
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);
|
||||
@@ -320,7 +344,7 @@ void App::handleRadio() {
|
||||
type == MessageType::DISCOVER && (!havePeer_ || !memcmp(peer_, r.mac, 6))) {
|
||||
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; radio_.sendBroadcast(ack);
|
||||
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;
|
||||
}
|
||||
if (state_ == AppState::MASTER_DISCOVER && type == MessageType::DISCOVER_ACK && r.packet.session == session_) {
|
||||
@@ -332,37 +356,97 @@ void App::handleRadio() {
|
||||
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) {
|
||||
ProtocolPacket ack = makePacket(MessageType::HEARTBEAT_ACK);
|
||||
ack.sequence = r.packet.sequence; radio_.sendBroadcast(ack); continue;
|
||||
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;
|
||||
radio_.sendBroadcast(ack); continue; // idempotent ACK for a retried old result
|
||||
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_.repeats = r.packet.repeats;
|
||||
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) { finish(false, FailReason::ABORTED); 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()) continue;
|
||||
sendCurrent(MessageType::START_STAGE); state_ = AppState::MASTER_WAIT_RESULT; retries_ = 0; deadlineMs_ = millis() +
|
||||
params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS + (1000UL * PWM_SETTLE_CYCLES / actual_.actualHz) + 20;
|
||||
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() + params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS +
|
||||
(1000UL * PWM_SETTLE_CYCLES / actual_.actualHz) + 20;
|
||||
} else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::PROGRESS) {
|
||||
stageStartConfirmed_ = true;
|
||||
deadlineMs_ = millis() + params_.testTimeMs * params_.repeats + 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; radio_.sendBroadcast(ack); pwm_.stop();
|
||||
if (!r.packet.passed) finish(false, static_cast<FailReason>(r.packet.reason)); else stagePassed();
|
||||
} else if (state_ == AppState::SLAVE_WAIT_START && type == MessageType::PREPARE) {
|
||||
params_.testTimeMs = r.packet.testTimeMs; params_.repeats = r.packet.repeats;
|
||||
params_.accuracyPct = r.packet.accuracyX100 / 100.0f; requestedHz_ = r.packet.requestedHz;
|
||||
ProtocolPacket ready = makePacket(MessageType::READY); ready.sequence = r.packet.sequence; radio_.sendBroadcast(ready);
|
||||
display_.show("SLAVE LINKED", "MASTER ONLINE");
|
||||
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() + params_.testTimeMs * params_.repeats + 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) { ++stageIndex_; state_ = AppState::SLAVE_WAIT_START; display_.show("SLAVE READY", "WAIT PREPARE"); }
|
||||
else finish(false, static_cast<FailReason>(pendingPacket_.reason));
|
||||
if (pendingPacket_.passed) {
|
||||
if (r.packet.passed) {
|
||||
radio_.end(); pendingReason_ = FailReason::NONE;
|
||||
if (armSlave(true)) display_.show("SLAVE PASS", "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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,14 +460,23 @@ void App::updateMaster() {
|
||||
}
|
||||
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);
|
||||
radio_.sendBroadcast(pendingPacket_); ++retries_;
|
||||
sendLinked(pendingPacket_); ++retries_;
|
||||
deadlineMs_ = now + (state_ == AppState::MASTER_WAIT_RESULT ?
|
||||
params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS : LINK_REPLY_TIMEOUT_MS);
|
||||
(stageStartConfirmed_ ? params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS : LINK_RETRY_INTERVAL_MS) :
|
||||
LINK_REPLY_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
void App::updateSlave() {
|
||||
@@ -391,35 +484,62 @@ void App::updateSlave() {
|
||||
if (state_ == AppState::FINISHED) return;
|
||||
if (state_ == AppState::SLAVE_MEASURE) {
|
||||
const MeasureState ms = measurement_.update();
|
||||
if (ms != MeasureState::PASS && ms != MeasureState::FAIL) return;
|
||||
if (ms == MeasureState::STEP_READY) {
|
||||
StageStats live = {};
|
||||
if (measurement_.statsSnapshot(live)) {
|
||||
ProtocolPacket progress = makePacket(MessageType::PROGRESS);
|
||||
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();
|
||||
return;
|
||||
}
|
||||
if (ms != MeasureState::PASS && ms != MeasureState::FAIL) {
|
||||
return;
|
||||
}
|
||||
printStageStats(measurement_.stats(), actual_.actualHz);
|
||||
showStageResult(measurement_.stats());
|
||||
pendingPacket_ = makePacket(MessageType::RESULT);
|
||||
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_; radio_.sendBroadcast(pendingPacket_);
|
||||
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_); radio_.sendBroadcast(pendingPacket_); deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS; }
|
||||
else { Log::printf("ESP-NOW", "RESULT retry=%u", retries_); sendLinked(pendingPacket_); deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS; }
|
||||
}
|
||||
}
|
||||
|
||||
void App::sendAbort() { if (havePeer_) sendCurrent(MessageType::ABORT); }
|
||||
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(); measurement_.abort(); finish(false, FailReason::ABORTED);
|
||||
sendAbort(FailReason::ABORTED); measurement_.abort(); finish(false, FailReason::ABORTED);
|
||||
}
|
||||
|
||||
void App::finish(bool pass, FailReason reason) {
|
||||
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_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);
|
||||
@@ -429,18 +549,32 @@ void App::finish(bool pass, FailReason reason) {
|
||||
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;
|
||||
if (slaveLinkLost) {
|
||||
if (armSlave()) display_.show("MASTER LOST", "WAIT MASTER");
|
||||
char target[12], one[24];
|
||||
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, target, sizeof(target));
|
||||
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target,
|
||||
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct);
|
||||
display_.show(one, failName(reason), stageIndex_ + 1, stageCount_);
|
||||
armSlave(true);
|
||||
return;
|
||||
}
|
||||
if (static_cast<Role>(settings_.role) == Role::SLAVE) slaveRearmAtMs_ = millis() + 2000;
|
||||
if (preserveDisplay) return;
|
||||
char one[24];
|
||||
if (pass) display_.show("PASS", "REPEAT");
|
||||
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");
|
||||
}
|
||||
else if (requestedHz_) {
|
||||
char frequency[12]; Display::formatFrequency(requestedHz_, frequency, sizeof(frequency));
|
||||
snprintf(one, sizeof(one), "FAIL %s", frequency); display_.show(one, failName(reason));
|
||||
char frequency[12];
|
||||
Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, frequency, sizeof(frequency));
|
||||
snprintf(one, sizeof(one), "FAIL %s %.0f%%", frequency,
|
||||
actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct);
|
||||
display_.show(one, failName(reason), stageIndex_ + 1, stageCount_);
|
||||
} else {
|
||||
display_.show("TEST FAILED", failName(reason));
|
||||
}
|
||||
@@ -481,3 +615,85 @@ void App::printStageStats(const StageStats &s, uint32_t hz) {
|
||||
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[24], two[24];
|
||||
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);
|
||||
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);
|
||||
} 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);
|
||||
} else {
|
||||
snprintf(two, sizeof(two), "%s", failName(s.reason));
|
||||
}
|
||||
display_.show(one, two, stageIndex_ + 1, stageCount_);
|
||||
return;
|
||||
}
|
||||
snprintf(one, sizeof(one), "Test:%-6s %2.0f%% %2lu/%2lu",
|
||||
target, actual_.actualDutyPct, stageIndex_ + 1, stageCount_);
|
||||
if (!s.periods || !s.periodSum) {
|
||||
display_.show(one, "F:--- D:---%", stageIndex_ + 1, 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_);
|
||||
}
|
||||
|
||||
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];
|
||||
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_);
|
||||
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 if (reason == FailReason::PERIOD_OUT && packet.measuredHzX10) {
|
||||
snprintf(one, sizeof(one), "FAIL %s %.0f%%", 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);
|
||||
} else if (reason == FailReason::DUTY_OUT && packet.measuredDutyX10) {
|
||||
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target, packet.actualDutyX100 / 100.0f);
|
||||
char duty[10]; formatErrorDuty(packet.measuredDutyX10 / 10.0f, duty, sizeof(duty));
|
||||
snprintf(two, sizeof(two), "DUTY OUT %s", duty);
|
||||
} else {
|
||||
snprintf(one, sizeof(one), "FAIL %s %.0f%%", target, packet.actualDutyX100 / 100.0f);
|
||||
snprintf(two, sizeof(two), "%s", failName(reason));
|
||||
}
|
||||
display_.show(one, two, stageIndex_ + 1, 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[24];
|
||||
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_);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
enum class AppState : uint8_t {
|
||||
IDLE, MENU, SOLO_MEASURE, MASTER_DISCOVER, MASTER_WAIT_READY,
|
||||
MASTER_WAIT_RESULT, SLAVE_READY, SLAVE_WAIT_START, SLAVE_MEASURE,
|
||||
MASTER_WAIT_RESULT, MASTER_FINALIZE, SLAVE_READY, SLAVE_WAIT_START, SLAVE_MEASURE,
|
||||
SLAVE_WAIT_ACK, FINISHED
|
||||
};
|
||||
|
||||
@@ -24,21 +24,26 @@ class App {
|
||||
void changeMenu(int direction);
|
||||
void sanitizeRange();
|
||||
void startTest();
|
||||
bool armSlave();
|
||||
bool prepareStage();
|
||||
bool armSlave(bool preserveDisplay = false);
|
||||
bool prepareStage(bool showProgress = true);
|
||||
bool startLocalMeasurement(float hz, float duty);
|
||||
void startMasterDiscovery();
|
||||
void handleRadio();
|
||||
void updateMaster();
|
||||
void updateSlave();
|
||||
void stagePassed();
|
||||
void finish(bool pass, FailReason reason);
|
||||
void finish(bool pass, FailReason reason, bool preserveDisplay = false);
|
||||
void abortTest();
|
||||
void sendAbort();
|
||||
void sendAbort(FailReason reason = FailReason::ABORTED);
|
||||
void printConfiguration();
|
||||
void printStageStats(const StageStats &s, uint32_t hz);
|
||||
void showStageResult(const StageStats &s);
|
||||
void showRemoteResult(const ProtocolPacket &packet);
|
||||
void fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const;
|
||||
void showStageProgress();
|
||||
uint64_t actualNominalTotalUs();
|
||||
ProtocolPacket makePacket(MessageType type) const;
|
||||
bool sendLinked(ProtocolPacket packet);
|
||||
void sendCurrent(MessageType type);
|
||||
void updateHeartbeat();
|
||||
bool packetForCurrent(const ProtocolPacket &p) const;
|
||||
@@ -69,4 +74,5 @@ class App {
|
||||
bool initialized_ = false, bootResetCandidate_ = false;
|
||||
uint32_t bootCheckStartedMs_ = 0;
|
||||
uint32_t slaveRearmAtMs_ = 0;
|
||||
bool stageStartConfirmed_ = false;
|
||||
};
|
||||
|
||||
@@ -48,16 +48,23 @@ constexpr uint8_t LINK_PACKET_RETRIES = 10;
|
||||
constexpr uint32_t LINK_RETRY_INTERVAL_MS = 1000;
|
||||
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;
|
||||
constexpr uint8_t FINAL_ACK_RETRIES = 2;
|
||||
constexpr uint8_t NO_SIGNAL_TIMEOUT_PERIODS = 8;
|
||||
constexpr uint16_t RMT_MIN_RECEIVE_SYMBOLS = 48;
|
||||
constexpr uint16_t RMT_MAX_RECEIVE_SYMBOLS = 512;
|
||||
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 C3_STRICT_MAX_HZ = 1000000;
|
||||
constexpr uint32_t S3_STRICT_MAX_HZ = 1000000;
|
||||
constexpr uint32_t CAPTURE_RESOLUTION_HZ = 80000000;
|
||||
// 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.
|
||||
// Keep this explicit so the resolution calculation never asks LEDC for an
|
||||
// impossible frequency/resolution combination.
|
||||
|
||||
@@ -93,37 +93,29 @@ bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz,
|
||||
if (!requestedHz || !sourceClockHz || !maxBits || dutyPct > 100U) return false;
|
||||
|
||||
bool found = false;
|
||||
uint64_t bestError = 0;
|
||||
uint32_t bestDenominator = 1;
|
||||
uint32_t bestErrorHz = 0;
|
||||
uint32_t bestDutyError = 0;
|
||||
uint32_t bestLevels = 1;
|
||||
|
||||
for (uint8_t bits = 1; bits <= maxBits && bits < 31; ++bits) {
|
||||
const uint32_t levels = 1UL << bits;
|
||||
const uint64_t requestedProduct = static_cast<uint64_t>(requestedHz) * levels;
|
||||
uint32_t lowerDivider = static_cast<uint32_t>(sourceClockHz / requestedProduct);
|
||||
if (lowerDivider < 1U) lowerDivider = 1U;
|
||||
if (lowerDivider > 1023U) lowerDivider = 1023U;
|
||||
|
||||
const uint32_t candidates[] = {lowerDivider,
|
||||
lowerDivider < 1023U ? lowerDivider + 1U : lowerDivider};
|
||||
for (uint8_t candidate = 0; candidate < 2; ++candidate) {
|
||||
const uint32_t divider = candidates[candidate];
|
||||
if (candidate && divider == candidates[0]) continue;
|
||||
for (uint32_t divider = 1; divider <= 1023U; ++divider) {
|
||||
const uint32_t denominator = levels * divider;
|
||||
const uint64_t targetClock = static_cast<uint64_t>(requestedHz) * denominator;
|
||||
const uint64_t error = targetClock > sourceClockHz
|
||||
? targetClock - sourceClockHz : sourceClockHz - targetClock;
|
||||
// A fixed integer divider gives identical PWM periods. Requiring an
|
||||
// exact division also guarantees that the physical frequency is a
|
||||
// whole number of hertz rather than a rounded value.
|
||||
if (sourceClockHz % denominator) continue;
|
||||
const uint32_t actualHz = sourceClockHz / denominator;
|
||||
const uint32_t errorHz = actualHz > requestedHz
|
||||
? actualHz - requestedHz : requestedHz - actualHz;
|
||||
const uint32_t dutyCount = (static_cast<uint64_t>(levels) * dutyPct + 50U) / 100U;
|
||||
const uint32_t representedDuty = dutyCount * 100U;
|
||||
const uint32_t requestedDuty = levels * dutyPct;
|
||||
const uint32_t dutyError = representedDuty > requestedDuty
|
||||
? representedDuty - requestedDuty : requestedDuty - representedDuty;
|
||||
|
||||
const bool frequencyBetter = !found ||
|
||||
error * bestDenominator < bestError * denominator;
|
||||
const bool frequencyEqual = found &&
|
||||
error * bestDenominator == bestError * denominator;
|
||||
const bool frequencyBetter = !found || errorHz < bestErrorHz;
|
||||
const bool frequencyEqual = found && errorHz == bestErrorHz;
|
||||
const bool dutyBetter = frequencyEqual &&
|
||||
static_cast<uint64_t>(dutyError) * bestLevels <
|
||||
static_cast<uint64_t>(bestDutyError) * levels;
|
||||
@@ -132,12 +124,10 @@ bool chooseIntegerPwmConfig(uint32_t requestedHz, uint32_t sourceClockHz,
|
||||
static_cast<uint64_t>(bestDutyError) * levels;
|
||||
if (!frequencyBetter && !dutyBetter && !(dutyEqual && bits > config.bits)) continue;
|
||||
|
||||
config.actualHz = static_cast<uint32_t>(
|
||||
(static_cast<uint64_t>(sourceClockHz) + denominator / 2U) / denominator);
|
||||
config.actualHz = actualHz;
|
||||
config.divider = static_cast<uint16_t>(divider);
|
||||
config.bits = bits;
|
||||
bestError = error;
|
||||
bestDenominator = denominator;
|
||||
bestErrorHz = errorHz;
|
||||
bestDutyError = dutyError;
|
||||
bestLevels = levels;
|
||||
found = true;
|
||||
|
||||
@@ -37,7 +37,7 @@ void Display::fit(char *s) {
|
||||
}
|
||||
}
|
||||
|
||||
void Display::show(const char *a, const char *b) {
|
||||
void Display::show(const char *a, const char *b, uint32_t progress, uint32_t progressTotal) {
|
||||
char one[32], two[32];
|
||||
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.
|
||||
@@ -45,16 +45,39 @@ void Display::show(const char *a, const char *b) {
|
||||
if (!ok_) return;
|
||||
fit(one); fit(two);
|
||||
oled_.clearDisplay(); oled_.setCursor(0, 3); oled_.print(one);
|
||||
oled_.setCursor(0, 19); oled_.print(two); oled_.display();
|
||||
oled_.setCursor(0, 19); oled_.print(two);
|
||||
if (progressTotal) {
|
||||
if (progress > progressTotal) progress = progressTotal;
|
||||
const uint16_t width = static_cast<uint16_t>(
|
||||
(static_cast<uint64_t>(progress) * 128U + progressTotal - 1U) / progressTotal);
|
||||
if (width) oled_.drawFastHLine(0, 31, width, SSD1306_WHITE);
|
||||
}
|
||||
oled_.display();
|
||||
}
|
||||
|
||||
void Display::formatFrequency(float hz, char *out, size_t n) {
|
||||
float value = hz; const char *suffix = "Hz";
|
||||
if (hz >= 1000000.0f) { value = hz / 1000000.0f; suffix = "M"; }
|
||||
if (hz >= 999950.0f) { value = hz / 1000000.0f; suffix = "M"; }
|
||||
else if (hz >= 1000.0f) { value = hz / 1000.0f; suffix = "k"; }
|
||||
if (fabsf(value - roundf(value)) < 0.005f) snprintf(out, n, "%.0f%s", value, suffix);
|
||||
else if (fabsf(value * 10.0f - roundf(value * 10.0f)) < 0.005f) snprintf(out, n, "%.1f%s", value, suffix);
|
||||
else snprintf(out, n, "%.2f%s", value, suffix);
|
||||
if (suffix[0] == 'M' && fabsf(value - roundf(value)) < 0.0005f)
|
||||
snprintf(out, n, "%.0f%s", value, suffix);
|
||||
else if (value >= 100.0f) snprintf(out, n, "%.1f%s", value, suffix);
|
||||
else if (value >= 10.0f) snprintf(out, n, "%.2f%s", value, suffix);
|
||||
else snprintf(out, n, "%.3f%s", value, suffix);
|
||||
}
|
||||
|
||||
void Display::formatTestFrequency(uint32_t hz, char *out, size_t n) {
|
||||
if (hz >= 1000000U && hz % 1000000U == 0)
|
||||
snprintf(out, n, "%luM", hz / 1000000U);
|
||||
else if (hz >= 1000U && hz % 1000U == 0)
|
||||
snprintf(out, n, "%luk", hz / 1000U);
|
||||
else if (hz >= 1000U) {
|
||||
const float khz = hz / 1000.0f;
|
||||
if (khz >= 100.0f) snprintf(out, n, "%.1fk", khz);
|
||||
else if (khz >= 10.0f) snprintf(out, n, "%.2fk", khz);
|
||||
else snprintf(out, n, "%.3fk", khz);
|
||||
} else
|
||||
snprintf(out, n, "%lu", hz);
|
||||
}
|
||||
|
||||
void Display::formatDuration(uint64_t us, char *out, size_t n) {
|
||||
|
||||
@@ -7,13 +7,14 @@ class Display {
|
||||
public:
|
||||
Display();
|
||||
bool begin();
|
||||
void show(const char *line1, const char *line2);
|
||||
void show(const char *line1, const char *line2,
|
||||
uint32_t progress = 0, uint32_t progressTotal = 0);
|
||||
bool available() const { return ok_; }
|
||||
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);
|
||||
private:
|
||||
void fit(char *text);
|
||||
Adafruit_SSD1306 oled_;
|
||||
bool ok_ = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,8 +9,15 @@ bool Measurement::start(float hz, float duty, float tolerance, uint32_t timeMs,
|
||||
if (!expectedHz_ || !timeMs || !repeats || repeats > 10 ||
|
||||
!makePeriodLimits(expectedHz_, duty, tolerance, receiver_.tickHz(), limits_) ||
|
||||
!receiver_.start(expectedHz_)) return false;
|
||||
timeMs_ = timeMs; repeats_ = repeats; settleLeft_ = settleCycles;
|
||||
repeats_ = repeats; 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_));
|
||||
publishStats();
|
||||
measurementStartTick_ = deadlineTick_ = 0; startedMs_ = millis();
|
||||
measurementStartMs_ = lastPeriodMs_ = 0;
|
||||
currentRepeat_ = 0;
|
||||
@@ -34,19 +41,39 @@ void Measurement::taskLoop() {
|
||||
|
||||
void Measurement::fail(FailReason reason) {
|
||||
if (stats_.reason == FailReason::NONE) stats_.reason = reason;
|
||||
publishStats();
|
||||
receiver_.stop(); state_ = MeasureState::FAIL;
|
||||
}
|
||||
|
||||
void Measurement::completeWindow() {
|
||||
void Measurement::completeStep() {
|
||||
receiver_.stop();
|
||||
stats_.droppedItems += receiver_.takeDroppedItems();
|
||||
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return; }
|
||||
++currentStep_;
|
||||
publishStats();
|
||||
if (currentStep_ < totalSteps_) {
|
||||
state_ = MeasureState::STEP_READY;
|
||||
return;
|
||||
}
|
||||
for (uint8_t i = 0; i < repeats_; ++i) if (!repeatPeriods_[i]) {
|
||||
fail(FailReason::TOO_FEW_PERIODS); return;
|
||||
}
|
||||
state_ = MeasureState::PASS;
|
||||
}
|
||||
|
||||
void Measurement::publishStats() {
|
||||
portENTER_CRITICAL(&statsMux_);
|
||||
publishedStats_ = stats_;
|
||||
portEXIT_CRITICAL(&statsMux_);
|
||||
}
|
||||
|
||||
bool Measurement::statsSnapshot(StageStats &out) const {
|
||||
portENTER_CRITICAL(&statsMux_);
|
||||
out = publishedStats_;
|
||||
portEXIT_CRITICAL(&statsMux_);
|
||||
return out.periods && out.periodSum;
|
||||
}
|
||||
|
||||
MeasureState Measurement::processOnce() {
|
||||
if (state_ != MeasureState::SETTLING && state_ != MeasureState::RUNNING) return state_;
|
||||
if (receiver_.overflowed()) { fail(FailReason::GLITCH); return state_; }
|
||||
@@ -63,19 +90,15 @@ MeasureState Measurement::processOnce() {
|
||||
if (settleLeft_) --settleLeft_;
|
||||
if (!settleLeft_) {
|
||||
measurementStartTick_ = period.startTick + period.periodTicks;
|
||||
repeatTicks_ = static_cast<uint64_t>(receiver_.tickHz()) * timeMs_ / 1000ULL;
|
||||
deadlineTick_ = measurementStartTick_ + repeatTicks_ * repeats_;
|
||||
nextRepeatTick_ = measurementStartTick_ + repeatTicks_;
|
||||
stats_.reset(); measurementStartMs_ = lastPeriodMs_ = millis(); state_ = MeasureState::RUNNING;
|
||||
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_) { completeWindow(); return state_; } // trailing incomplete period
|
||||
while (currentRepeat_ + 1U < repeats_ && period.startTick >= nextRepeatTick_) {
|
||||
++currentRepeat_; nextRepeatTick_ += repeatTicks_;
|
||||
}
|
||||
if (endTick > deadlineTick_) { completeStep(); return state_; } // trailing incomplete period
|
||||
++repeatPeriods_[currentRepeat_];
|
||||
const FailReason r = evaluatePeriodFast(period, receiver_.tickHz(), limits_, currentRepeat_ + 1, stats_);
|
||||
if (r != FailReason::NONE) { fail(r); return state_; }
|
||||
@@ -90,16 +113,32 @@ 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 totalMs = timeMs_ * repeats_;
|
||||
const uint32_t edgeTimeoutMs = expectedPeriodMs_ * NO_SIGNAL_TIMEOUT_PERIODS + 2;
|
||||
if (now - measurementStartMs_ < totalMs && now - lastPeriodMs_ > edgeTimeoutMs) {
|
||||
if (now - measurementStartMs_ < stepTimeMs_ && now - lastPeriodMs_ > edgeTimeoutMs) {
|
||||
fail(FailReason::LOST_EDGE); return state_;
|
||||
}
|
||||
if (now - measurementStartMs_ > totalMs + expectedPeriodMs_ + 2) completeWindow();
|
||||
if (now - measurementStartMs_ > stepTimeMs_ + expectedPeriodMs_ + 2) completeStep();
|
||||
}
|
||||
return state_;
|
||||
}
|
||||
|
||||
MeasureState Measurement::update() { return state_; }
|
||||
|
||||
void Measurement::abort() { if (state_ == MeasureState::SETTLING || state_ == MeasureState::RUNNING) fail(FailReason::ABORTED); }
|
||||
bool Measurement::continueAfterDisplay() {
|
||||
if (state_ != MeasureState::STEP_READY) return false;
|
||||
if (!receiver_.start(expectedHz_)) {
|
||||
fail(FailReason::UNSUPPORTED);
|
||||
return false;
|
||||
}
|
||||
settleLeft_ = settleCycles_;
|
||||
measurementStartTick_ = deadlineTick_ = 0;
|
||||
startedMs_ = millis(); measurementStartMs_ = lastPeriodMs_ = 0;
|
||||
state_ = MeasureState::SETTLING;
|
||||
xTaskNotifyGive(task_);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Measurement::abort() {
|
||||
if (state_ == MeasureState::SETTLING || state_ == MeasureState::RUNNING ||
|
||||
state_ == MeasureState::STEP_READY) fail(FailReason::ABORTED);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
#include "Receiver.h"
|
||||
|
||||
enum class MeasureState : uint8_t { IDLE, SETTLING, RUNNING, PASS, FAIL };
|
||||
enum class MeasureState : uint8_t { IDLE, SETTLING, RUNNING, STEP_READY, PASS, FAIL };
|
||||
|
||||
class Measurement {
|
||||
public:
|
||||
@@ -9,27 +9,32 @@ class Measurement {
|
||||
bool start(float expectedHz, float expectedDuty, float tolerancePct,
|
||||
uint32_t testTimeMs, uint8_t repeats, 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_; }
|
||||
bool statsSnapshot(StageStats &out) const;
|
||||
private:
|
||||
static void taskEntry(void *context);
|
||||
void taskLoop();
|
||||
MeasureState processOnce();
|
||||
void fail(FailReason reason);
|
||||
void completeWindow();
|
||||
void completeStep();
|
||||
void publishStats();
|
||||
PulseReceiver &receiver_;
|
||||
volatile MeasureState state_ = MeasureState::IDLE;
|
||||
TaskHandle_t task_ = nullptr;
|
||||
StageStats stats_ = {};
|
||||
StageStats publishedStats_ = {};
|
||||
mutable portMUX_TYPE statsMux_ = portMUX_INITIALIZER_UNLOCKED;
|
||||
PeriodLimits limits_ = {};
|
||||
uint32_t expectedHz_ = 0;
|
||||
uint32_t timeMs_ = 0;
|
||||
uint8_t repeats_ = 0, settleLeft_ = 0, currentRepeat_ = 0;
|
||||
uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, repeatTicks_ = 0, nextRepeatTick_ = 0;
|
||||
uint8_t repeats_ = 0, settleCycles_ = 0, settleLeft_ = 0;
|
||||
uint8_t currentRepeat_ = 0, currentStep_ = 0, totalSteps_ = 0;
|
||||
uint64_t measurementStartTick_ = 0, deadlineTick_ = 0, stepTicks_ = 0;
|
||||
uint32_t startedMs_ = 0, measurementStartMs_ = 0, lastPeriodMs_ = 0;
|
||||
uint32_t expectedPeriodMs_ = 1;
|
||||
uint32_t stepTimeMs_ = 1, expectedPeriodMs_ = 1;
|
||||
uint32_t repeatPeriods_[10] = {};
|
||||
PulsePeriod periodBatch_[PERIOD_BATCH_SIZE] = {};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
const char *messageName(MessageType type) {
|
||||
static const char *names[] = {"DISCOVER", "DISCOVER_ACK", "PREPARE", "READY",
|
||||
"START_STAGE", "RESULT", "ACK", "ABORT", "HEARTBEAT", "HEARTBEAT_ACK"};
|
||||
"START_STAGE", "RESULT", "ACK", "ABORT", "HEARTBEAT", "HEARTBEAT_ACK", "PROGRESS"};
|
||||
const uint8_t index = static_cast<uint8_t>(type);
|
||||
return index < sizeof(names) / sizeof(names[0]) ? names[index] : "UNKNOWN";
|
||||
}
|
||||
@@ -24,5 +24,5 @@ void finalizePacket(ProtocolPacket &p) {
|
||||
|
||||
bool validPacket(const ProtocolPacket &p) {
|
||||
return p.magic == PROTOCOL_MAGIC && p.version == PROTOCOL_VERSION &&
|
||||
p.type <= static_cast<uint8_t>(MessageType::HEARTBEAT_ACK) && p.crc == packetCrc(p);
|
||||
p.type <= static_cast<uint8_t>(MessageType::PROGRESS) && p.crc == packetCrc(p);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
#include "Core.h"
|
||||
|
||||
constexpr uint16_t PROTOCOL_MAGIC = 0x4F43;
|
||||
constexpr uint8_t PROTOCOL_VERSION = 2;
|
||||
constexpr uint8_t PROTOCOL_VERSION = 6;
|
||||
|
||||
enum class MessageType : uint8_t {
|
||||
DISCOVER, DISCOVER_ACK, PREPARE, READY, START_STAGE, RESULT, ACK, ABORT,
|
||||
HEARTBEAT, HEARTBEAT_ACK
|
||||
HEARTBEAT, HEARTBEAT_ACK, PROGRESS
|
||||
};
|
||||
|
||||
const char *messageName(MessageType type);
|
||||
@@ -18,6 +18,7 @@ struct ProtocolPacket {
|
||||
uint8_t type;
|
||||
uint32_t session;
|
||||
uint16_t stage;
|
||||
uint16_t stageCount;
|
||||
uint16_t sequence;
|
||||
uint32_t requestedHz;
|
||||
uint32_t actualHz;
|
||||
@@ -29,13 +30,15 @@ struct ProtocolPacket {
|
||||
uint8_t passed;
|
||||
uint8_t reason;
|
||||
uint32_t periods;
|
||||
uint32_t measuredHzX10;
|
||||
uint16_t measuredDutyX10;
|
||||
uint32_t minPeriodTicks;
|
||||
uint32_t maxPeriodTicks;
|
||||
uint16_t crc;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
static_assert(sizeof(ProtocolPacket) == 46, "Protocol layout changed");
|
||||
static_assert(sizeof(ProtocolPacket) == 54, "Protocol layout changed");
|
||||
|
||||
uint16_t packetCrc(const ProtocolPacket &packet);
|
||||
void finalizePacket(ProtocolPacket &packet);
|
||||
|
||||
@@ -53,7 +53,7 @@ bool PwmGenerator::start(uint32_t hz, uint8_t dutyPct, ActualPwm &a) {
|
||||
if (settleUs > 2000U) settleUs = 2000U;
|
||||
delayMicroseconds(settleUs);
|
||||
const uint32_t actualHz = ledcReadFreq(GPIO_PWM);
|
||||
if (actualHz) {
|
||||
if (actualHz == config.actualHz) {
|
||||
a = {hz, actualHz, 100.0f * duty / levels, bits};
|
||||
running_ = true;
|
||||
return true;
|
||||
|
||||
@@ -23,16 +23,22 @@ bool Radio::begin() {
|
||||
if (esp_wifi_set_channel(ESPNOW_WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
|
||||
Log::event("ESP-NOW", "Wi-Fi channel setup FAILED"); return false;
|
||||
}
|
||||
queue_ = xQueueCreate(8, sizeof(ReceivedPacket));
|
||||
if (!queue_) { Log::event("ESP-NOW", "receive queue creation FAILED"); return false; }
|
||||
if (!queue_) queue_ = xQueueCreate(8, sizeof(ReceivedPacket));
|
||||
if (!heartbeatQueue_) heartbeatQueue_ = xQueueCreate(4, sizeof(ReceivedPacket));
|
||||
if (!heartbeatTask_ && xTaskCreate(heartbeatTaskEntry, "radio-hb", 2048, this, 5, &heartbeatTask_) != pdPASS)
|
||||
heartbeatTask_ = nullptr;
|
||||
if (!queue_ || !heartbeatQueue_ || !heartbeatTask_) {
|
||||
Log::event("ESP-NOW", "receive/heartbeat service creation FAILED"); return false;
|
||||
}
|
||||
if (esp_now_init() != ESP_OK) {
|
||||
vQueueDelete(queue_); queue_ = nullptr;
|
||||
Log::event("ESP-NOW", "initialization FAILED"); return false;
|
||||
}
|
||||
const esp_err_t rateResult = esp_wifi_config_espnow_rate(WIFI_IF_STA, WIFI_PHY_RATE_1M_L);
|
||||
instance_ = this;
|
||||
if (esp_now_register_recv_cb(onReceive) != ESP_OK) { end(); return false; }
|
||||
active_ = true;
|
||||
if (esp_now_register_recv_cb(onReceive) != ESP_OK) {
|
||||
instance_ = nullptr; esp_now_deinit(); return false;
|
||||
}
|
||||
active_ = true; __atomic_store_n(&lastValidRxMs_, millis(), __ATOMIC_RELAXED);
|
||||
const bool ok = ensurePeer(BROADCAST_MAC);
|
||||
uint8_t primaryChannel = 0;
|
||||
wifi_second_chan_t secondaryChannel = WIFI_SECOND_CHAN_NONE;
|
||||
@@ -44,9 +50,11 @@ bool Radio::begin() {
|
||||
}
|
||||
|
||||
void Radio::end() {
|
||||
if (active_) { esp_now_unregister_recv_cb(); esp_now_deinit(); }
|
||||
if (queue_) { vQueueDelete(queue_); queue_ = nullptr; }
|
||||
active_ = false; if (instance_ == this) instance_ = nullptr;
|
||||
const bool wasActive = active_; active_ = false;
|
||||
if (wasActive) { esp_now_unregister_recv_cb(); esp_now_deinit(); }
|
||||
if (queue_) xQueueReset(queue_);
|
||||
if (heartbeatQueue_) xQueueReset(heartbeatQueue_);
|
||||
if (instance_ == this) instance_ = nullptr;
|
||||
Log::event("ESP-NOW", "stopped");
|
||||
}
|
||||
|
||||
@@ -72,7 +80,7 @@ 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)
|
||||
if (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;
|
||||
@@ -97,16 +105,37 @@ void Radio::maintainChannel() {
|
||||
restored ? "RESTORED" : "FAILED");
|
||||
}
|
||||
|
||||
void Radio::flush() { if (queue_) xQueueReset(queue_); }
|
||||
void Radio::flush() {
|
||||
if (queue_) xQueueReset(queue_);
|
||||
if (heartbeatQueue_) xQueueReset(heartbeatQueue_);
|
||||
}
|
||||
|
||||
void Radio::onReceive(const esp_now_recv_info_t *info, const uint8_t *data, int length) {
|
||||
if (!instance_ || !instance_->queue_ || !info || length != sizeof(ProtocolPacket)) return;
|
||||
ReceivedPacket item;
|
||||
memcpy(item.mac, info->src_addr, 6); memcpy(&item.packet, data, sizeof(item.packet));
|
||||
if (!validPacket(item.packet)) return;
|
||||
__atomic_store_n(&instance_->lastValidRxMs_, millis(), __ATOMIC_RELAXED);
|
||||
if (static_cast<MessageType>(item.packet.type) == MessageType::HEARTBEAT && instance_->heartbeatQueue_)
|
||||
xQueueSend(instance_->heartbeatQueue_, &item, 0);
|
||||
xQueueSend(instance_->queue_, &item, 0); // Wi-Fi task callback: copy only, never block
|
||||
}
|
||||
|
||||
void Radio::heartbeatTaskEntry(void *context) {
|
||||
static_cast<Radio *>(context)->heartbeatTaskLoop();
|
||||
}
|
||||
|
||||
void Radio::heartbeatTaskLoop() {
|
||||
ReceivedPacket item;
|
||||
for (;;) {
|
||||
if (xQueueReceive(heartbeatQueue_, &item, portMAX_DELAY) != pdTRUE || !active_) continue;
|
||||
ProtocolPacket ack = item.packet;
|
||||
ack.type = static_cast<uint8_t>(MessageType::HEARTBEAT_ACK);
|
||||
finalizePacket(ack);
|
||||
esp_now_send(item.mac, reinterpret_cast<const uint8_t *>(&ack), sizeof(ack));
|
||||
}
|
||||
}
|
||||
|
||||
void Radio::macText(const uint8_t mac[6], char *out, size_t n) {
|
||||
snprintf(out, n, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
}
|
||||
|
||||
@@ -13,14 +13,20 @@ class Radio {
|
||||
bool sendTo(const uint8_t mac[6], ProtocolPacket packet);
|
||||
bool receive(ReceivedPacket &received);
|
||||
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);
|
||||
private:
|
||||
static void onReceive(const esp_now_recv_info_t *info, const uint8_t *data, int length);
|
||||
static void heartbeatTaskEntry(void *context);
|
||||
void heartbeatTaskLoop();
|
||||
bool ensurePeer(const uint8_t mac[6]);
|
||||
void maintainChannel();
|
||||
static Radio *instance_;
|
||||
QueueHandle_t queue_ = nullptr;
|
||||
bool active_ = false;
|
||||
QueueHandle_t heartbeatQueue_ = nullptr;
|
||||
TaskHandle_t heartbeatTask_ = nullptr;
|
||||
volatile bool active_ = false;
|
||||
volatile uint32_t lastValidRxMs_ = 0;
|
||||
uint32_t lastChannelCheckMs_ = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -54,9 +54,12 @@ 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 = 20;
|
||||
cfg.signal_range_min_ns = 1000000000UL / CAPTURE_RESOLUTION_HZ;
|
||||
const uint64_t maxNs = 4000000000ULL / (expectedHz ? expectedHz : 1);
|
||||
cfg.signal_range_max_ns = maxNs > 100000000ULL ? 100000000UL : static_cast<uint32_t>(maxNs);
|
||||
// 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;
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user