налажена связь между 2 esp

This commit is contained in:
2026-08-07 01:34:18 +03:00
parent 1edc420b77
commit fb40f3ddef
9 changed files with 197 additions and 47 deletions

View File

@@ -47,7 +47,9 @@ void App::finishInitialization(bool factoryReset) {
initialized_ = true; initialized_ = true;
if (!receiver_.begin()) { Log::event("BOOT", "FATAL: capture peripheral init failed"); finish(false, FailReason::UNSUPPORTED); return; } 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"); Log::printf("BOOT", "capture initialized: %s", receiver_.highRateBackend() ? "RMT DMA" : "RMT ping-pong");
printConfiguration(); showIdle(); printConfiguration();
if (static_cast<Role>(settings_.role) == Role::SLAVE) armSlave();
else showIdle();
} }
void App::update() { void App::update() {
@@ -71,11 +73,26 @@ void App::update() {
if (state_ == AppState::IDLE || state_ == AppState::FINISHED) { if (state_ == AppState::IDLE || state_ == AppState::FINISHED) {
if (modeEvent == ButtonEvent::SHORT) { if (modeEvent == ButtonEvent::SHORT) {
settings_.role = (settings_.role + 1U) % 3U; const bool saved = store_.save(settings_); settings_.role = (settings_.role + 1U) % 3U; const bool saved = store_.save(settings_);
params_ = store_.params(settings_); showIdle(); 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"); Log::printf("ACTION", "role changed to %s, NVS=%s", roleName(static_cast<Role>(settings_.role)), saved ? "OK" : "FAILED");
} else if (modeEvent == ButtonEvent::LONG) { } else if (modeEvent == ButtonEvent::LONG) {
state_ = AppState::MENU; menuItem_ = 0; Log::event("ACTION", "settings menu entered"); showMenu(); 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 (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();
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; return;
} }
if (state_ == AppState::MENU) { if (state_ == AppState::MENU) {
@@ -85,7 +102,9 @@ void App::update() {
else if (modeEvent == ButtonEvent::LONG) { else if (modeEvent == ButtonEvent::LONG) {
sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_); sanitizeRange(); const bool saved = store_.save(settings_); params_ = store_.params(settings_);
Log::printf("ACTION", "settings menu saved and closed, NVS=%s", saved ? "OK" : "FAILED"); Log::printf("ACTION", "settings menu saved and closed, NVS=%s", saved ? "OK" : "FAILED");
state_ = AppState::IDLE; printConfiguration(); showIdle(); 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::SHORT) changeMenu(+1);
else if (startEvent == ButtonEvent::LONG || startEvent == ButtonEvent::REPEAT) changeMenu(-1); else if (startEvent == ButtonEvent::LONG || startEvent == ButtonEvent::REPEAT) changeMenu(-1);
return; return;
@@ -156,7 +175,8 @@ void App::showMenu() {
void App::startTest() { void App::startTest() {
params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz); params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz);
stageIndex_ = 0; pendingReason_ = FailReason::NONE; sweepHadDataLoss_ = false; firstDataLossHz_ = 0; stageIndex_ = 0; requestedHz_ = 0; pendingReason_ = FailReason::NONE; sweepHadDataLoss_ = false; firstDataLossHz_ = 0;
havePeer_ = false; lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0;
if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; } if (!stageCount_) { finish(false, FailReason::UNSUPPORTED); return; }
Log::printf("TEST", "starting role=%s stages=%lu", roleName(static_cast<Role>(settings_.role)), stageCount_); Log::printf("TEST", "starting role=%s stages=%lu", roleName(static_cast<Role>(settings_.role)), stageCount_);
if (SERIAL_MINIMAL_LOG) if (SERIAL_MINIMAL_LOG)
@@ -173,6 +193,23 @@ void App::startTest() {
else { state_ = AppState::SLAVE_READY; Log::event("TEST", "Slave armed and waiting for Master"); display_.show("SLAVE READY", "WAIT MASTER"); } else { state_ = AppState::SLAVE_READY; Log::event("TEST", "Slave armed and waiting for Master"); display_.show("SLAVE READY", "WAIT MASTER"); }
} }
bool App::armSlave() {
params_ = store_.params(settings_);
stageIndex_ = 0; stageCount_ = frequencyPointCount(params_.startHz, params_.endHz, params_.stepHz);
requestedHz_ = 0; session_ = 0; sequence_ = 0; havePeer_ = false;
lastHeartbeatMs_ = 0; lastPeerSeenMs_ = 0; retries_ = 0; slaveRearmAtMs_ = 0;
if (!radio_.begin()) {
state_ = AppState::FINISHED; pendingReason_ = FailReason::LINK_LOST;
slaveRearmAtMs_ = millis() + LINK_HEARTBEAT_TIMEOUT_MS;
display_.show("LINK FAILED", "RADIO ERROR");
return false;
}
radio_.flush(); state_ = AppState::SLAVE_READY;
Log::event("TEST", "Slave automatically armed and waiting for Master");
display_.show("SLAVE READY", "WAIT MASTER");
return true;
}
bool App::prepareStage() { bool App::prepareStage() {
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_); requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_);
const uint32_t maxHz = TARGET_IS_C3 ? C3_STRICT_MAX_HZ : const uint32_t maxHz = TARGET_IS_C3 ? C3_STRICT_MAX_HZ :
@@ -228,11 +265,12 @@ void App::stagePassed() {
} }
void App::startMasterDiscovery() { void App::startMasterDiscovery() {
session_ = esp_random(); if (!session_) session_ = 1; sequence_ = 1; havePeer_ = false; radio_.flush(); session_ = esp_random(); if (!session_) session_ = 1;
sequence_ = 1; stageIndex_ = 0; requestedHz_ = 0; havePeer_ = false; radio_.flush();
pendingPacket_ = makePacket(MessageType::DISCOVER); radio_.sendBroadcast(pendingPacket_); pendingPacket_ = makePacket(MessageType::DISCOVER); radio_.sendBroadcast(pendingPacket_);
lastSendMs_ = millis(); deadlineMs_ = millis() + LINK_DISCOVERY_TIMEOUT_MS; retries_ = 0; lastSendMs_ = millis(); retries_ = 0;
state_ = AppState::MASTER_DISCOVER; Log::printf("ESP-NOW", "discovery started session=%08lX", session_); state_ = AppState::MASTER_DISCOVER; Log::printf("ESP-NOW", "discovery started session=%08lX", session_);
display_.show("MASTER SEARCH", "WAIT SLAVE"); display_.show("MASTER SEARCH", "HOLD START=STOP");
} }
ProtocolPacket App::makePacket(MessageType type) const { ProtocolPacket App::makePacket(MessageType type) const {
@@ -247,10 +285,27 @@ ProtocolPacket App::makePacket(MessageType type) const {
void App::sendCurrent(MessageType type) { void App::sendCurrent(MessageType type) {
++sequence_; pendingPacket_ = makePacket(type); ++sequence_; pendingPacket_ = makePacket(type);
const bool ok = radio_.sendTo(peer_, pendingPacket_); lastSendMs_ = millis(); const bool ok = radio_.sendBroadcast(pendingPacket_); lastSendMs_ = millis();
if (!ok) Log::printf("ESP-NOW", "sendCurrent %s FAILED", messageName(type)); if (!ok) Log::printf("ESP-NOW", "sendCurrent %s FAILED", messageName(type));
} }
void App::updateHeartbeat() {
if (!havePeer_ || state_ == AppState::FINISHED) return;
const uint32_t now = millis();
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_;
radio_.sendBroadcast(heartbeat);
lastHeartbeatMs_ = now;
}
}
bool App::packetForCurrent(const ProtocolPacket &p) const { bool App::packetForCurrent(const ProtocolPacket &p) const {
return p.session == session_ && p.stage == stageIndex_; return p.session == session_ && p.stage == stageIndex_;
} }
@@ -259,29 +314,35 @@ void App::handleRadio() {
ReceivedPacket r; ReceivedPacket r;
while (radio_.receive(r)) { while (radio_.receive(r)) {
const MessageType type = static_cast<MessageType>(r.packet.type); const MessageType type = static_cast<MessageType>(r.packet.type);
if (state_ != AppState::SLAVE_MEASURE) if (type != MessageType::HEARTBEAT && type != MessageType::HEARTBEAT_ACK &&
state_ != AppState::SLAVE_MEASURE)
Log::printf("ESP-NOW", "RX %s session=%08lX stage=%u seq=%u", Log::printf("ESP-NOW", "RX %s session=%08lX stage=%u seq=%u",
messageName(type), r.packet.session, r.packet.stage, r.packet.sequence); messageName(type), r.packet.session, r.packet.stage, r.packet.sequence);
if (state_ == AppState::SLAVE_READY && type == MessageType::DISCOVER) { if ((state_ == AppState::SLAVE_READY || state_ == AppState::SLAVE_WAIT_START) &&
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; memcpy(peer_, r.mac, 6); havePeer_ = true; session_ = r.packet.session; stageIndex_ = 0; sequence_ = r.packet.sequence;
ProtocolPacket ack = makePacket(MessageType::DISCOVER_ACK); ack.sequence = r.packet.sequence; radio_.sendTo(peer_, ack); lastPeerSeenMs_ = millis();
state_ = AppState::SLAVE_WAIT_START; display_.show("SLAVE LINKED", "WAIT PREPARE"); continue; ProtocolPacket ack = makePacket(MessageType::DISCOVER_ACK); ack.sequence = r.packet.sequence; radio_.sendBroadcast(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_) { if (state_ == AppState::MASTER_DISCOVER && type == MessageType::DISCOVER_ACK && r.packet.session == session_) {
memcpy(peer_, r.mac, 6); havePeer_ = true; requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_); memcpy(peer_, r.mac, 6); havePeer_ = true; lastPeerSeenMs_ = lastHeartbeatMs_ = millis();
requestedHz_ = frequencyAt(params_.startHz, params_.endHz, params_.stepHz, stageIndex_);
sendCurrent(MessageType::PREPARE); state_ = AppState::MASTER_WAIT_READY; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS; sendCurrent(MessageType::PREPARE); state_ = AppState::MASTER_WAIT_READY; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS;
char mac[20]; Radio::macText(peer_, mac, sizeof(mac)); Log::printf("ESP-NOW", "Slave selected %s", mac); continue; char mac[20]; Radio::macText(peer_, mac, sizeof(mac)); Log::printf("ESP-NOW", "Slave selected %s", mac); continue;
} }
if (state_ == AppState::SLAVE_WAIT_START && type == MessageType::DISCOVER && if (havePeer_ && !memcmp(peer_, r.mac, 6) && r.packet.session == session_) lastPeerSeenMs_ = millis();
r.packet.session == session_ && !memcmp(peer_, r.mac, 6)) { if (havePeer_ && !memcmp(peer_, r.mac, 6) && r.packet.session == session_ &&
ProtocolPacket ack = makePacket(MessageType::DISCOVER_ACK); type == MessageType::HEARTBEAT) {
ack.sequence = r.packet.sequence; radio_.sendTo(peer_, ack); continue; ProtocolPacket ack = makePacket(MessageType::HEARTBEAT_ACK);
ack.sequence = r.packet.sequence; radio_.sendBroadcast(ack); continue;
} }
if (type == MessageType::HEARTBEAT_ACK) continue;
if (havePeer_ && !memcmp(peer_, r.mac, 6) && type == MessageType::RESULT && if (havePeer_ && !memcmp(peer_, r.mac, 6) && type == MessageType::RESULT &&
r.packet.session == session_ && r.packet.stage < stageIndex_) { r.packet.session == session_ && r.packet.stage < stageIndex_) {
ProtocolPacket ack = {}; ack.type = static_cast<uint8_t>(MessageType::ACK); ProtocolPacket ack = {}; ack.type = static_cast<uint8_t>(MessageType::ACK);
ack.session = session_; ack.stage = r.packet.stage; ack.sequence = r.packet.sequence; ack.session = session_; ack.stage = r.packet.stage; ack.sequence = r.packet.sequence;
radio_.sendTo(peer_, ack); continue; // idempotent ACK for a retried old result radio_.sendBroadcast(ack); continue; // idempotent ACK for a retried old result
} }
if (!havePeer_ || memcmp(peer_, r.mac, 6) || !packetForCurrent(r.packet)) 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) { finish(false, FailReason::ABORTED); continue; }
@@ -290,12 +351,13 @@ void App::handleRadio() {
sendCurrent(MessageType::START_STAGE); state_ = AppState::MASTER_WAIT_RESULT; retries_ = 0; deadlineMs_ = millis() + 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; params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS + (1000UL * PWM_SETTLE_CYCLES / actual_.actualHz) + 20;
} else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::RESULT) { } else if (state_ == AppState::MASTER_WAIT_RESULT && type == MessageType::RESULT) {
ProtocolPacket ack = makePacket(MessageType::ACK); ack.sequence = r.packet.sequence; radio_.sendTo(peer_, ack); pwm_.stop(); 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(); if (!r.packet.passed) finish(false, static_cast<FailReason>(r.packet.reason)); else stagePassed();
} else if (state_ == AppState::SLAVE_WAIT_START && type == MessageType::PREPARE) { } else if (state_ == AppState::SLAVE_WAIT_START && type == MessageType::PREPARE) {
params_.testTimeMs = r.packet.testTimeMs; params_.repeats = r.packet.repeats; params_.testTimeMs = r.packet.testTimeMs; params_.repeats = r.packet.repeats;
params_.accuracyPct = r.packet.accuracyX100 / 100.0f; requestedHz_ = r.packet.requestedHz; params_.accuracyPct = r.packet.accuracyX100 / 100.0f; requestedHz_ = r.packet.requestedHz;
ProtocolPacket ready = makePacket(MessageType::READY); ready.sequence = r.packet.sequence; radio_.sendTo(peer_, ready); ProtocolPacket ready = makePacket(MessageType::READY); ready.sequence = r.packet.sequence; radio_.sendBroadcast(ready);
display_.show("SLAVE LINKED", "MASTER ONLINE");
} else if (state_ == AppState::SLAVE_WAIT_START && type == MessageType::START_STAGE) { } else if (state_ == AppState::SLAVE_WAIT_START && type == MessageType::START_STAGE) {
actual_.actualHz = r.packet.actualHz; actual_.actualDutyPct = r.packet.actualDutyX100 / 100.0f; actual_.actualHz = r.packet.actualHz; actual_.actualDutyPct = r.packet.actualDutyX100 / 100.0f;
if (!startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { finish(false, FailReason::UNSUPPORTED); continue; } if (!startLocalMeasurement(actual_.actualHz, actual_.actualDutyPct)) { finish(false, FailReason::UNSUPPORTED); continue; }
@@ -310,22 +372,25 @@ void App::handleRadio() {
void App::updateMaster() { void App::updateMaster() {
const uint32_t now = millis(); const uint32_t now = millis();
if (state_ == AppState::MASTER_DISCOVER) { if (state_ == AppState::MASTER_DISCOVER) {
if (now >= deadlineMs_) { finish(false, FailReason::LINK_LOST); return; }
if (now - lastSendMs_ >= LINK_RETRY_INTERVAL_MS) { if (now - lastSendMs_ >= LINK_RETRY_INTERVAL_MS) {
Log::printf("ESP-NOW", "DISCOVER retry=%u", retries_ + 1); radio_.sendBroadcast(pendingPacket_); Log::event("ESP-NOW", "DISCOVER retry"); radio_.sendBroadcast(pendingPacket_);
lastSendMs_ = now; ++retries_; lastSendMs_ = now;
} }
return; return;
} }
updateHeartbeat();
if (state_ == AppState::FINISHED) return;
if (now < deadlineMs_) return; if (now < deadlineMs_) return;
if (retries_ >= LINK_PACKET_RETRIES) { finish(false, FailReason::LINK_LOST); 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); Log::printf("ESP-NOW", "%s retry=%u", messageName(static_cast<MessageType>(pendingPacket_.type)), retries_ + 1);
radio_.sendTo(peer_, pendingPacket_); ++retries_; radio_.sendBroadcast(pendingPacket_); ++retries_;
deadlineMs_ = now + (state_ == AppState::MASTER_WAIT_RESULT ? deadlineMs_ = now + (state_ == AppState::MASTER_WAIT_RESULT ?
params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS : LINK_REPLY_TIMEOUT_MS); params_.testTimeMs * params_.repeats + LINK_REPLY_TIMEOUT_MS : LINK_REPLY_TIMEOUT_MS);
} }
void App::updateSlave() { void App::updateSlave() {
updateHeartbeat();
if (state_ == AppState::FINISHED) return;
if (state_ == AppState::SLAVE_MEASURE) { if (state_ == AppState::SLAVE_MEASURE) {
const MeasureState ms = measurement_.update(); const MeasureState ms = measurement_.update();
if (ms != MeasureState::PASS && ms != MeasureState::FAIL) return; if (ms != MeasureState::PASS && ms != MeasureState::FAIL) return;
@@ -334,13 +399,13 @@ void App::updateSlave() {
pendingPacket_.passed = ms == MeasureState::PASS && measurement_.reason() == FailReason::NONE; pendingPacket_.passed = ms == MeasureState::PASS && measurement_.reason() == FailReason::NONE;
pendingPacket_.reason = static_cast<uint8_t>(measurement_.reason()); pendingPacket_.periods = measurement_.stats().periods; pendingPacket_.reason = static_cast<uint8_t>(measurement_.reason()); pendingPacket_.periods = measurement_.stats().periods;
pendingPacket_.minPeriodTicks = measurement_.stats().minPeriod; pendingPacket_.maxPeriodTicks = measurement_.stats().maxPeriod; pendingPacket_.minPeriodTicks = measurement_.stats().minPeriod; pendingPacket_.maxPeriodTicks = measurement_.stats().maxPeriod;
pendingPacket_.sequence = ++sequence_; radio_.sendTo(peer_, pendingPacket_); pendingPacket_.sequence = ++sequence_; radio_.sendBroadcast(pendingPacket_);
Log::printf("TEST", "Slave result prepared: %s reason=%s periods=%lu", Log::printf("TEST", "Slave result prepared: %s reason=%s periods=%lu",
pendingPacket_.passed ? "PASS" : "FAIL", failName(static_cast<FailReason>(pendingPacket_.reason)), pendingPacket_.periods); 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; state_ = AppState::SLAVE_WAIT_ACK; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS;
} else if (state_ == AppState::SLAVE_WAIT_ACK && millis() >= deadlineMs_) { } else if (state_ == AppState::SLAVE_WAIT_ACK && millis() >= deadlineMs_) {
if (retries_++ >= LINK_PACKET_RETRIES) finish(false, FailReason::LINK_LOST); if (retries_++ >= LINK_PACKET_RETRIES) finish(false, FailReason::LINK_LOST);
else { Log::printf("ESP-NOW", "RESULT retry=%u", retries_); radio_.sendTo(peer_, pendingPacket_); deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS; } else { Log::printf("ESP-NOW", "RESULT retry=%u", retries_); radio_.sendBroadcast(pendingPacket_); deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS; }
} }
} }
@@ -353,12 +418,33 @@ void App::abortTest() {
void App::finish(bool pass, FailReason reason) { void App::finish(bool pass, FailReason reason) {
Log::printf("TEST", "finishing result=%s reason=%s", pass ? "PASS" : "FAIL", failName(reason)); 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);
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(); pwm_.stop(); receiver_.stop();
if (masterLinkLost) {
Log::event("ESP-NOW", "link lost; returning to continuous discovery");
startMasterDiscovery();
return;
}
if (state_ != AppState::IDLE && state_ != AppState::MENU) radio_.end(); if (state_ != AppState::IDLE && state_ != AppState::MENU) radio_.end();
state_ = AppState::FINISHED; pendingReason_ = reason; state_ = AppState::FINISHED; pendingReason_ = reason;
if (slaveLinkLost) {
if (armSlave()) display_.show("MASTER LOST", "WAIT MASTER");
return;
}
if (static_cast<Role>(settings_.role) == Role::SLAVE) slaveRearmAtMs_ = millis() + 2000;
char one[24]; char one[24];
if (pass) { snprintf(one, sizeof(one), "PASS %luHz-%lu", params_.startHz, params_.endHz); display_.show(one, "START=REPEAT"); } if (pass) { snprintf(one, sizeof(one), "PASS %luHz-%lu", params_.startHz, params_.endHz); display_.show(one, "START=REPEAT"); }
else { snprintf(one, sizeof(one), "FAIL AT %lu", requestedHz_); display_.show(one, failName(reason)); } else if (requestedHz_) {
snprintf(one, sizeof(one), "FAIL AT %lu", requestedHz_); display_.show(one, failName(reason));
} else {
display_.show("TEST FAILED", failName(reason));
}
} }
void App::printConfiguration() { void App::printConfiguration() {

View File

@@ -24,6 +24,7 @@ class App {
void changeMenu(int direction); void changeMenu(int direction);
void sanitizeRange(); void sanitizeRange();
void startTest(); void startTest();
bool armSlave();
bool prepareStage(); bool prepareStage();
bool startLocalMeasurement(float hz, float duty); bool startLocalMeasurement(float hz, float duty);
void startMasterDiscovery(); void startMasterDiscovery();
@@ -39,6 +40,7 @@ class App {
uint64_t actualNominalTotalUs(); uint64_t actualNominalTotalUs();
ProtocolPacket makePacket(MessageType type) const; ProtocolPacket makePacket(MessageType type) const;
void sendCurrent(MessageType type); void sendCurrent(MessageType type);
void updateHeartbeat();
bool packetForCurrent(const ProtocolPacket &p) const; bool packetForCurrent(const ProtocolPacket &p) const;
Button startButton_, modeButton_; Button startButton_, modeButton_;
@@ -61,10 +63,12 @@ class App {
uint8_t peer_[6] = {}; uint8_t peer_[6] = {};
bool havePeer_ = false; bool havePeer_ = false;
uint32_t deadlineMs_ = 0, lastSendMs_ = 0; uint32_t deadlineMs_ = 0, lastSendMs_ = 0;
uint32_t lastHeartbeatMs_ = 0, lastPeerSeenMs_ = 0;
uint8_t retries_ = 0; uint8_t retries_ = 0;
ProtocolPacket pendingPacket_ = {}; ProtocolPacket pendingPacket_ = {};
bool initialized_ = false, bootResetCandidate_ = false; bool initialized_ = false, bootResetCandidate_ = false;
bool sweepHadDataLoss_ = false; bool sweepHadDataLoss_ = false;
uint32_t firstDataLossHz_ = 0; uint32_t firstDataLossHz_ = 0;
uint32_t bootCheckStartedMs_ = 0; uint32_t bootCheckStartedMs_ = 0;
uint32_t slaveRearmAtMs_ = 0;
}; };

View File

@@ -7,22 +7,24 @@
constexpr bool TARGET_IS_C3 = true; constexpr bool TARGET_IS_C3 = true;
constexpr uint8_t GPIO_PWM = 3; constexpr uint8_t GPIO_PWM = 3;
constexpr uint8_t GPIO_RX = 4; constexpr uint8_t GPIO_RX = 4;
constexpr uint8_t GPIO_BUTTON_START = 0; constexpr uint8_t GPIO_BUTTON_MODE = 0;
constexpr uint8_t GPIO_BUTTON_MODE = 1; constexpr uint8_t GPIO_BUTTON_START = 1;
constexpr uint8_t GPIO_SDA = 6; constexpr uint8_t GPIO_SDA = 6;
constexpr uint8_t GPIO_SCL = 7; constexpr uint8_t GPIO_SCL = 7;
#elif CONFIG_IDF_TARGET_ESP32S3 #elif CONFIG_IDF_TARGET_ESP32S3
constexpr bool TARGET_IS_C3 = false; constexpr bool TARGET_IS_C3 = false;
constexpr uint8_t GPIO_PWM = 4; constexpr uint8_t GPIO_PWM = 4;
constexpr uint8_t GPIO_RX = 5; constexpr uint8_t GPIO_RX = 5;
constexpr uint8_t GPIO_BUTTON_START = 6; constexpr uint8_t GPIO_BUTTON_MODE = 6;
constexpr uint8_t GPIO_BUTTON_MODE = 7; constexpr uint8_t GPIO_BUTTON_START = 7;
constexpr uint8_t GPIO_SDA = 8; constexpr uint8_t GPIO_SDA = 8;
constexpr uint8_t GPIO_SCL = 9; constexpr uint8_t GPIO_SCL = 9;
#else #else
#error "Only ESP32-C3 and ESP32-S3 are supported" #error "Only ESP32-C3 and ESP32-S3 are supported"
#endif #endif
constexpr uint8_t OLED_ROTATION = 0;
constexpr uint8_t OLED_ADDRESS = 0x3C; constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr uint8_t ESPNOW_WIFI_CHANNEL = 6; constexpr uint8_t ESPNOW_WIFI_CHANNEL = 6;
constexpr uint32_t SERIAL_BAUD = 115200; constexpr uint32_t SERIAL_BAUD = 115200;
@@ -41,10 +43,11 @@ constexpr uint32_t BUTTON_REPEAT_DELAY_MS = 600;
constexpr uint32_t BUTTON_REPEAT_MS = 180; constexpr uint32_t BUTTON_REPEAT_MS = 180;
constexpr uint32_t FACTORY_RESET_HOLD_MS = 1500; constexpr uint32_t FACTORY_RESET_HOLD_MS = 1500;
constexpr uint32_t LINK_DISCOVERY_TIMEOUT_MS = 3000; constexpr uint32_t LINK_REPLY_TIMEOUT_MS = 1500;
constexpr uint32_t LINK_REPLY_TIMEOUT_MS = 800; constexpr uint8_t LINK_PACKET_RETRIES = 10;
constexpr uint8_t LINK_PACKET_RETRIES = 3; constexpr uint32_t LINK_RETRY_INTERVAL_MS = 1000;
constexpr uint32_t LINK_RETRY_INTERVAL_MS = 100; constexpr uint32_t LINK_HEARTBEAT_INTERVAL_MS = 500;
constexpr uint32_t LINK_HEARTBEAT_TIMEOUT_MS = 2500;
constexpr uint8_t NO_SIGNAL_TIMEOUT_PERIODS = 8; constexpr uint8_t NO_SIGNAL_TIMEOUT_PERIODS = 8;
constexpr uint16_t RMT_MIN_RECEIVE_SYMBOLS = 48; constexpr uint16_t RMT_MIN_RECEIVE_SYMBOLS = 48;
constexpr uint16_t RMT_MAX_RECEIVE_SYMBOLS = 512; constexpr uint16_t RMT_MAX_RECEIVE_SYMBOLS = 512;

View File

@@ -2,14 +2,28 @@
#include "Config.h" #include "Config.h"
#include "Log.h" #include "Log.h"
#include <Wire.h> #include <Wire.h>
#include <esp_log.h>
#include <string.h> #include <string.h>
Display::Display() : oled_(128, 32, &Wire, -1) {} Display::Display() : oled_(128, 32, &Wire, -1) {}
bool Display::begin() { bool Display::begin() {
Wire.begin(GPIO_SDA, GPIO_SCL); Wire.begin(GPIO_SDA, GPIO_SCL);
// An absent optional OLED produces a large burst of ESP-IDF NACK messages.
// Probe it once and keep the I2C driver quiet when no display is connected.
esp_log_level_set("i2c.master", ESP_LOG_NONE);
Wire.beginTransmission(OLED_ADDRESS);
if (Wire.endTransmission() != 0) {
ok_ = false;
Log::printf("OLED", "not detected at I2C address=0x%02X", OLED_ADDRESS);
return false;
}
ok_ = oled_.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS); ok_ = oled_.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS);
if (ok_) { oled_.setTextColor(SSD1306_WHITE); oled_.setTextSize(1); } if (ok_) {
oled_.setRotation(OLED_ROTATION);
oled_.setTextColor(SSD1306_WHITE);
oled_.setTextSize(1);
}
Log::printf("OLED", "initialization %s, I2C address=0x%02X", ok_ ? "OK" : "FAILED", OLED_ADDRESS); Log::printf("OLED", "initialization %s, I2C address=0x%02X", ok_ ? "OK" : "FAILED", OLED_ADDRESS);
return ok_; return ok_;
} }

View File

@@ -7,7 +7,7 @@ namespace Log {
void event(const char *component, const char *message) { void event(const char *component, const char *message) {
if (!SERIAL_ACTION_LOG) return; if (!SERIAL_ACTION_LOG) return;
if (SERIAL_MINIMAL_LOG && strcmp(component, "INPUT") && strcmp(component, "UI") && if (SERIAL_MINIMAL_LOG && strcmp(component, "INPUT") && strcmp(component, "UI") &&
strcmp(component, "CONFIG") && strcmp(component, "RESULT")) return; strcmp(component, "CONFIG") && strcmp(component, "RESULT") && strcmp(component, "ESP-NOW")) return;
if (SERIAL_LOG_TIMESTAMPS) Serial.printf("[%10lu][%-8s] %s\n", millis(), component, message); if (SERIAL_LOG_TIMESTAMPS) Serial.printf("[%10lu][%-8s] %s\n", millis(), component, message);
else Serial.printf("[%-8s] %s\n", component, message); else Serial.printf("[%-8s] %s\n", component, message);
} }

View File

@@ -3,7 +3,7 @@
const char *messageName(MessageType type) { const char *messageName(MessageType type) {
static const char *names[] = {"DISCOVER", "DISCOVER_ACK", "PREPARE", "READY", static const char *names[] = {"DISCOVER", "DISCOVER_ACK", "PREPARE", "READY",
"START_STAGE", "RESULT", "ACK", "ABORT"}; "START_STAGE", "RESULT", "ACK", "ABORT", "HEARTBEAT", "HEARTBEAT_ACK"};
const uint8_t index = static_cast<uint8_t>(type); const uint8_t index = static_cast<uint8_t>(type);
return index < sizeof(names) / sizeof(names[0]) ? names[index] : "UNKNOWN"; return index < sizeof(names) / sizeof(names[0]) ? names[index] : "UNKNOWN";
} }
@@ -24,5 +24,5 @@ void finalizePacket(ProtocolPacket &p) {
bool validPacket(const ProtocolPacket &p) { bool validPacket(const ProtocolPacket &p) {
return p.magic == PROTOCOL_MAGIC && p.version == PROTOCOL_VERSION && return p.magic == PROTOCOL_MAGIC && p.version == PROTOCOL_VERSION &&
p.type <= static_cast<uint8_t>(MessageType::ABORT) && p.crc == packetCrc(p); p.type <= static_cast<uint8_t>(MessageType::HEARTBEAT_ACK) && p.crc == packetCrc(p);
} }

View File

@@ -2,10 +2,11 @@
#include "Core.h" #include "Core.h"
constexpr uint16_t PROTOCOL_MAGIC = 0x4F43; constexpr uint16_t PROTOCOL_MAGIC = 0x4F43;
constexpr uint8_t PROTOCOL_VERSION = 1; constexpr uint8_t PROTOCOL_VERSION = 2;
enum class MessageType : uint8_t { enum class MessageType : uint8_t {
DISCOVER, DISCOVER_ACK, PREPARE, READY, START_STAGE, RESULT, ACK, ABORT DISCOVER, DISCOVER_ACK, PREPARE, READY, START_STAGE, RESULT, ACK, ABORT,
HEARTBEAT, HEARTBEAT_ACK
}; };
const char *messageName(MessageType type); const char *messageName(MessageType type);

View File

@@ -10,17 +10,36 @@ static const uint8_t BROADCAST_MAC[6] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
bool Radio::begin() { bool Radio::begin() {
if (active_) { Log::event("ESP-NOW", "already active"); return true; } if (active_) { Log::event("ESP-NOW", "already active"); return true; }
WiFi.mode(WIFI_STA); WiFi.disconnect(); // Keep both devices on a dedicated, fixed STA channel. Stored access-point
// credentials must not reconnect in the background and move ESP-NOW to the
// AP's channel.
WiFi.persistent(false);
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(false);
WiFi.disconnect(false, false);
WiFi.setSleep(false);
const bool txPowerOk = WiFi.setTxPower(WIFI_POWER_8_5dBm);
delay(10);
if (esp_wifi_set_channel(ESPNOW_WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE) != ESP_OK) { 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; Log::event("ESP-NOW", "Wi-Fi channel setup FAILED"); return false;
} }
queue_ = xQueueCreate(8, sizeof(ReceivedPacket)); queue_ = xQueueCreate(8, sizeof(ReceivedPacket));
if (!queue_ || esp_now_init() != ESP_OK) { Log::event("ESP-NOW", "initialization FAILED"); return false; } if (!queue_) { Log::event("ESP-NOW", "receive queue 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; instance_ = this;
if (esp_now_register_recv_cb(onReceive) != ESP_OK) { end(); return false; } if (esp_now_register_recv_cb(onReceive) != ESP_OK) { end(); return false; }
active_ = true; active_ = true;
const bool ok = ensurePeer(BROADCAST_MAC); const bool ok = ensurePeer(BROADCAST_MAC);
Log::printf("ESP-NOW", "started channel=%u broadcast-peer=%s", ESPNOW_WIFI_CHANNEL, ok ? "OK" : "FAILED"); uint8_t primaryChannel = 0;
wifi_second_chan_t secondaryChannel = WIFI_SECOND_CHAN_NONE;
esp_wifi_get_channel(&primaryChannel, &secondaryChannel);
Log::printf("ESP-NOW", "started channel=%u expected=%u rate=1M%s tx-power=max%s broadcast-peer=%s",
primaryChannel, ESPNOW_WIFI_CHANNEL, rateResult == ESP_OK ? "" : "-FAILED",
txPowerOk ? "" : "-FAILED", ok ? "OK" : "FAILED");
return ok; return ok;
} }
@@ -34,13 +53,17 @@ void Radio::end() {
bool Radio::ensurePeer(const uint8_t mac[6]) { bool Radio::ensurePeer(const uint8_t mac[6]) {
if (esp_now_is_peer_exist(mac)) return true; if (esp_now_is_peer_exist(mac)) return true;
esp_now_peer_info_t peer = {}; esp_now_peer_info_t peer = {};
memcpy(peer.peer_addr, mac, 6); peer.channel = ESPNOW_WIFI_CHANNEL; peer.encrypt = false; memcpy(peer.peer_addr, mac, 6);
peer.channel = ESPNOW_WIFI_CHANNEL;
peer.ifidx = WIFI_IF_STA;
peer.encrypt = false;
return esp_now_add_peer(&peer) == ESP_OK; return esp_now_add_peer(&peer) == ESP_OK;
} }
bool Radio::sendBroadcast(ProtocolPacket p) { return sendTo(BROADCAST_MAC, p); } bool Radio::sendBroadcast(ProtocolPacket p) { return sendTo(BROADCAST_MAC, p); }
bool Radio::sendTo(const uint8_t mac[6], ProtocolPacket p) { bool Radio::sendTo(const uint8_t mac[6], ProtocolPacket p) {
maintainChannel();
char peer[20]; macText(mac, peer, sizeof(peer)); char peer[20]; macText(mac, peer, sizeof(peer));
if (!active_ || !ensurePeer(mac)) { if (!active_ || !ensurePeer(mac)) {
Log::printf("ESP-NOW", "TX %s to %s FAILED: inactive/peer", messageName(static_cast<MessageType>(p.type)), peer); Log::printf("ESP-NOW", "TX %s to %s FAILED: inactive/peer", messageName(static_cast<MessageType>(p.type)), peer);
@@ -48,15 +71,32 @@ bool Radio::sendTo(const uint8_t mac[6], ProtocolPacket p) {
} }
finalizePacket(p); finalizePacket(p);
const bool ok = esp_now_send(mac, reinterpret_cast<const uint8_t *>(&p), sizeof(p)) == ESP_OK; 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)
Log::printf("ESP-NOW", "TX %s to %s session=%08lX stage=%u seq=%u %s", Log::printf("ESP-NOW", "TX %s to %s session=%08lX stage=%u seq=%u %s",
messageName(static_cast<MessageType>(p.type)), peer, p.session, p.stage, p.sequence, ok ? "QUEUED" : "FAILED"); messageName(type), peer, p.session, p.stage, p.sequence, ok ? "QUEUED" : "FAILED");
return ok; return ok;
} }
bool Radio::receive(ReceivedPacket &r) { bool Radio::receive(ReceivedPacket &r) {
maintainChannel();
return queue_ && xQueueReceive(queue_, &r, 0) == pdTRUE; return queue_ && xQueueReceive(queue_, &r, 0) == pdTRUE;
} }
void Radio::maintainChannel() {
if (!active_) return;
const uint32_t now = millis();
if (now - lastChannelCheckMs_ < 250) return;
lastChannelCheckMs_ = now;
uint8_t primaryChannel = 0;
wifi_second_chan_t secondaryChannel = WIFI_SECOND_CHAN_NONE;
if (esp_wifi_get_channel(&primaryChannel, &secondaryChannel) != ESP_OK ||
primaryChannel == ESPNOW_WIFI_CHANNEL) return;
const bool restored = esp_wifi_set_channel(ESPNOW_WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE) == ESP_OK;
Log::printf("ESP-NOW", "channel drift %u->%u %s", primaryChannel, ESPNOW_WIFI_CHANNEL,
restored ? "RESTORED" : "FAILED");
}
void Radio::flush() { if (queue_) xQueueReset(queue_); } void Radio::flush() { if (queue_) xQueueReset(queue_); }
void Radio::onReceive(const esp_now_recv_info_t *info, const uint8_t *data, int length) { void Radio::onReceive(const esp_now_recv_info_t *info, const uint8_t *data, int length) {

View File

@@ -17,8 +17,10 @@ class Radio {
private: private:
static void onReceive(const esp_now_recv_info_t *info, const uint8_t *data, int length); static void onReceive(const esp_now_recv_info_t *info, const uint8_t *data, int length);
bool ensurePeer(const uint8_t mac[6]); bool ensurePeer(const uint8_t mac[6]);
void maintainChannel();
static Radio *instance_; static Radio *instance_;
QueueHandle_t queue_ = nullptr; QueueHandle_t queue_ = nullptr;
bool active_ = false; bool active_ = false;
uint32_t lastChannelCheckMs_ = 0;
}; };