From 862781fa6a3628008fd37195438177429cce61c8 Mon Sep 17 00:00:00 2001 From: Razvalyaev Date: Wed, 12 Aug 2026 08:25:56 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=83=D1=80=D0=BE=D0=B2=D0=BD=D1=8F=20=D0=BE?= =?UTF-8?q?=D0=BF=D1=82=D0=B8=D0=BA=D0=B8,=20=D0=B2=D0=BA=D0=BB=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87=D0=B0=D0=BD=D0=B8=D1=8E?= =?UTF-8?q?,=20=D0=B2=D1=8B=D0=BA=D0=BB=20=D0=B2=20=D1=81=D0=BD=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit плюс коррекции по отображению --- OpticalChannelTester/App.cpp | 87 ++++++++++++++++++++++++++----- OpticalChannelTester/App.h | 4 ++ OpticalChannelTester/Config.h | 12 ++++- OpticalChannelTester/Display.cpp | 12 +++-- OpticalChannelTester/Display.h | 5 +- OpticalChannelTester/Pwm.cpp | 15 ++++++ OpticalChannelTester/Pwm.h | 1 + OpticalChannelTester/Radio.cpp | 1 + OpticalChannelTester/Receiver.cpp | 5 +- README.md | 8 +-- 10 files changed, 124 insertions(+), 26 deletions(-) diff --git a/OpticalChannelTester/App.cpp b/OpticalChannelTester/App.cpp index 3752112..6b4f026 100644 --- a/OpticalChannelTester/App.cpp +++ b/OpticalChannelTester/App.cpp @@ -53,6 +53,12 @@ const char *uiFailName(FailReason reason) { ? UiText::FAIL_NAMES[index] : "UNKNOWN"; } +const char *roleCorner(Role role) { + static const char *markers[] = {"O", "M", "S"}; + const uint8_t index = static_cast(role); + return index < sizeof(markers) / sizeof(markers[0]) ? markers[index] : "?"; +} + void formatMenuLine(const char *label, const char *value, char *out, size_t size) { constexpr size_t OLED_TEXT_COLUMNS = 21; const size_t labelLength = utf8CharacterCount(label); @@ -198,6 +204,7 @@ void App::update() { void App::showIdle() { setActivePerformance(false); + setStandbyOpticalOutput(); lastUserActivityMs_ = millis(); char one[64]; snprintf(one, sizeof(one), "%s%s", UiText::MODE_PREFIX, uiRoleName(static_cast(settings_.role))); @@ -272,6 +279,7 @@ void App::showMenu() { void App::startTest() { leaveIdlePowerSave(); + pwm_.stop(); setActivePerformance(true); params_ = store_.params(settings_); stageCount_ = frequencyPointCount(params_.startHz, params_.endHz); stageIndex_ = 0; requestedHz_ = 0; pendingReason_ = FailReason::NONE; @@ -298,6 +306,7 @@ void App::startTest() { bool App::armSlave(bool preserveDisplay) { setActivePerformance(false); + pwm_.stop(); lastUserActivityMs_ = millis(); params_ = store_.params(settings_); stageIndex_ = 0; stageCount_ = frequencyPointCount(params_.startHz, params_.endHz); @@ -306,7 +315,8 @@ bool App::armSlave(bool preserveDisplay) { if (!radio_.begin()) { state_ = AppState::FINISHED; pendingReason_ = FailReason::LINK_LOST; slaveRearmAtMs_ = millis() + LINK_HEARTBEAT_TIMEOUT_MS; - display_.show(UiText::LINK_FAILED, UiText::RADIO_ERROR); + display_.show(UiText::LINK_FAILED, UiText::RADIO_ERROR, + 0, 0, roleCorner(Role::SLAVE)); return false; } radio_.setWindowedReceive(true); @@ -373,6 +383,9 @@ void App::stagePassed() { void App::startMasterDiscovery() { session_ = esp_random(); if (!session_) session_ = 1; sequence_ = 1; stageIndex_ = 0; requestedHz_ = 0; havePeer_ = false; radio_.flush(); + opticalWakeActive_ = true; + lastOpticalWakeToggleMs_ = millis(); + pwm_.active(); pendingPacket_ = makePacket(MessageType::DISCOVER); radio_.sendBroadcast(pendingPacket_); lastSendMs_ = millis(); retries_ = 0; state_ = AppState::MASTER_DISCOVER; Log::printf("ESP-NOW", "discovery started session=%08lX", session_); @@ -435,6 +448,7 @@ void App::handleRadio() { if ((state_ == AppState::SLAVE_READY || state_ == AppState::SLAVE_WAIT_START) && type == MessageType::DISCOVER && (!havePeer_ || !memcmp(peer_, r.mac, 6))) { leaveIdlePowerSave(); + pwm_.stop(); setActivePerformance(true); radio_.setWindowedReceive(false); memcpy(peer_, r.mac, 6); havePeer_ = true; session_ = r.packet.session; stageIndex_ = 0; sequence_ = r.packet.sequence; @@ -443,6 +457,8 @@ void App::handleRadio() { state_ = AppState::SLAVE_WAIT_START; display_.show(UiText::MASTER_SEEN, UiText::ACK_SENT); continue; } if (state_ == AppState::MASTER_DISCOVER && type == MessageType::DISCOVER_ACK && r.packet.session == session_) { + opticalWakeActive_ = false; + pwm_.stop(); memcpy(peer_, r.mac, 6); havePeer_ = true; lastPeerSeenMs_ = lastHeartbeatMs_ = millis(); requestedHz_ = frequencyAt(params_.startHz, params_.endHz, stageIndex_); sendCurrent(MessageType::PREPARE); state_ = AppState::MASTER_WAIT_READY; retries_ = 0; deadlineMs_ = millis() + LINK_REPLY_TIMEOUT_MS; @@ -551,6 +567,12 @@ void App::handleRadio() { void App::updateMaster() { const uint32_t now = millis(); if (state_ == AppState::MASTER_DISCOVER) { + if (now - lastOpticalWakeToggleMs_ >= OPTICAL_WAKE_HALF_PERIOD_MS) { + opticalWakeActive_ = !opticalWakeActive_; + if (opticalWakeActive_) pwm_.active(); + else pwm_.stop(); + lastOpticalWakeToggleMs_ = now; + } if (now - lastSendMs_ >= DISCOVERY_RETRY_INTERVAL_MS) { if (++retries_ % 50U == 0U) Log::event("ESP-NOW", "DISCOVER burst continues"); radio_.sendBroadcast(pendingPacket_); @@ -651,6 +673,7 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) { if (!pass && masterActive && havePeer_ && reason != FailReason::ABORTED) sendAbort(reason); if (state_ != AppState::IDLE && state_ != AppState::MENU) radio_.end(); state_ = AppState::FINISHED; pendingReason_ = reason; + setStandbyOpticalOutput(); setActivePerformance(false); lastUserActivityMs_ = millis(); if (slaveLinkLost) { @@ -658,7 +681,8 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) { Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, target, sizeof(target)); snprintf(one, sizeof(one), UiText::FAIL_FORMAT, target, actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct); - display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_); + display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_, + roleCorner(Role::SLAVE)); armSlave(true); return; } @@ -675,9 +699,11 @@ void App::finish(bool pass, FailReason reason, bool preserveDisplay) { Display::formatTestFrequency(actual_.actualHz ? actual_.actualHz : requestedHz_, frequency, sizeof(frequency)); snprintf(one, sizeof(one), UiText::FAIL_FORMAT, frequency, actual_.actualDutyPct > 0.0f ? actual_.actualDutyPct : params_.dutyPct); - display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_); + display_.show(one, uiFailName(reason), stageIndex_ + 1, stageCount_, + roleCorner(static_cast(settings_.role))); } else { - display_.show(UiText::TEST_FAILED, uiFailName(reason)); + display_.show(UiText::TEST_FAILED, uiFailName(reason), 0, 0, + roleCorner(static_cast(settings_.role))); } } @@ -686,6 +712,11 @@ bool App::idlePowerSaveAllowed() const { state_ == AppState::FINISHED || state_ == AppState::SLAVE_READY); } +void App::setStandbyOpticalOutput() { + if (static_cast(settings_.role) == Role::SLAVE) pwm_.stop(); + else pwm_.active(); +} + void App::setActivePerformance(bool active) { const uint32_t targetMhz = active ? 160U : 80U; if (getCpuFrequencyMhz() != targetMhz && !setCpuFrequencyMhz(targetMhz)) @@ -699,6 +730,15 @@ void App::leaveIdlePowerSave(bool wakeDisplay) { } idlePowerSave_ = false; lastUserActivityMs_ = millis(); + setStandbyOpticalOutput(); + if (idleSleepRadioStopped_) { + idleSleepRadioStopped_ = false; + if (radio_.begin()) { + radio_.setWindowedReceive(true); + radio_.flush(); + Log::event("POWER", "Slave ESP-NOW restored after external wake"); + } else Log::event("POWER", "Slave ESP-NOW restore FAILED after external wake"); + } if (wakeDisplay) display_.setPower(true); Log::event("POWER", "idle light sleep ended"); } @@ -716,6 +756,11 @@ void App::serviceIdlePowerSave() { return; } idlePowerSave_ = true; + pwm_.stop(); + if (state_ == AppState::SLAVE_READY) { + radio_.end(); + idleSleepRadioStopped_ = true; + } display_.setPower(false); Log::event("POWER", "idle timeout; OLED off and light sleep started"); } @@ -724,8 +769,16 @@ void App::serviceIdlePowerSave() { BUTTON_ACTIVE_LEVEL == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL); gpio_wakeup_enable(static_cast(GPIO_BUTTON_MODE), BUTTON_ACTIVE_LEVEL == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL); + if (static_cast(settings_.role) == Role::SLAVE) { + // Light-sleep GPIO wake is level-triggered in ESP-IDF. Arm the level + // opposite to the one sampled immediately before sleep, which makes a + // transition (either edge) necessary and prevents a steady RX level from + // waking Slave continuously. + const bool currentRxHigh = gpio_get_level(static_cast(GPIO_RX)) != 0; + gpio_wakeup_enable(static_cast(GPIO_RX), + currentRxHigh ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL); + } else gpio_wakeup_disable(static_cast(GPIO_RX)); esp_sleep_enable_gpio_wakeup(); - esp_sleep_enable_timer_wakeup(IDLE_LIGHT_SLEEP_SLICE_US); const esp_err_t result = esp_light_sleep_start(); if (result != ESP_OK) { delay(1); @@ -733,12 +786,19 @@ void App::serviceIdlePowerSave() { } if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO) { - // The wake-up press is deliberately consumed. Holding or releasing it - // must not later turn into a SHORT, LONG, or REPEAT event. - startButton_.suppressUntilRelease(); - modeButton_.suppressUntilRelease(); - leaveIdlePowerSave(); - Log::event("POWER", "button wake consumed; next press will perform the action"); + const bool buttonWake = digitalRead(GPIO_BUTTON_START) == BUTTON_ACTIVE_LEVEL || + digitalRead(GPIO_BUTTON_MODE) == BUTTON_ACTIVE_LEVEL; + if (buttonWake) { + // The wake-up press is deliberately consumed. Holding or releasing it + // must not later turn into a SHORT, LONG, or REPEAT event. + startButton_.suppressUntilRelease(); + modeButton_.suppressUntilRelease(); + leaveIdlePowerSave(); + Log::event("POWER", "button wake consumed; next press will perform the action"); + } else if (static_cast(settings_.role) == Role::SLAVE) { + leaveIdlePowerSave(); + Log::event("POWER", "optical input woke Slave"); + } } } @@ -794,7 +854,7 @@ void App::showStageResult(const StageStats &s) { snprintf(two, sizeof(two), "%s", uiFailName(s.reason)); } display_.show(one, two, overallProgress(stageIndex_, measurement_.progressStep()), - overallProgressTotal(stageCount_)); + overallProgressTotal(stageCount_), roleCorner(static_cast(settings_.role))); return; } char stage[12]; snprintf(stage, sizeof(stage), "%lu/%lu", stageIndex_ + 1, stageCount_); @@ -841,7 +901,8 @@ void App::showRemoteResult(const ProtocolPacket &packet) { snprintf(two, sizeof(two), "%s", uiFailName(reason)); } display_.show(one, two, overallProgress(stageIndex_, packet.progressStep), - overallProgressTotal(stageCount_)); + overallProgressTotal(stageCount_), reason == FailReason::NONE ? nullptr : + roleCorner(static_cast(settings_.role))); } void App::fillMeasuredResult(ProtocolPacket &packet, const StageStats &stats) const { diff --git a/OpticalChannelTester/App.h b/OpticalChannelTester/App.h index 76f2c9a..21f5712 100644 --- a/OpticalChannelTester/App.h +++ b/OpticalChannelTester/App.h @@ -51,6 +51,7 @@ class App { void serviceRxPinStateLog(); void leaveIdlePowerSave(bool wakeDisplay = true); bool idlePowerSaveAllowed() const; + void setStandbyOpticalOutput(); void setActivePerformance(bool active); Button startButton_, modeButton_; @@ -82,5 +83,8 @@ class App { bool stageStartConfirmed_ = false; uint32_t lastUserActivityMs_ = 0; bool idlePowerSave_ = false; + bool idleSleepRadioStopped_ = false; + uint32_t lastOpticalWakeToggleMs_ = 0; + bool opticalWakeActive_ = false; bool rxPinStateKnown_ = false, rxPinState_ = false; }; diff --git a/OpticalChannelTester/Config.h b/OpticalChannelTester/Config.h index 4d25217..d3107f3 100644 --- a/OpticalChannelTester/Config.h +++ b/OpticalChannelTester/Config.h @@ -71,8 +71,14 @@ constexpr bool SERIAL_LOG_TIMESTAMPS = true; constexpr bool SERIAL_MINIMAL_LOG = true; #define BUTTON_ACTIVE_LEVEL LOW -#define RX_SIGNAL_INVERTED false +// Raw GPIO_RX level that means the optical receiver is active. +#define RX_ACTIVE_LEVEL LOW +// PWM_SAFE_LEVEL must switch the optical transmitter fully off and is used +// during tests whenever PWM is stopped, and while the controller sleeps. +// PWM_ACTIVE_LEVEL intentionally keeps the transmitter active while the +// controller is awake and no test is in progress. #define PWM_SAFE_LEVEL HIGH +#define PWM_ACTIVE_LEVEL LOW #define PWM_SETTLE_CYCLES 5U constexpr uint32_t BUTTON_DEBOUNCE_MS = 30; @@ -85,6 +91,9 @@ constexpr uint32_t LINK_REPLY_TIMEOUT_MS = 1500; constexpr uint8_t LINK_PACKET_RETRIES = 10; constexpr uint32_t LINK_RETRY_INTERVAL_MS = 1000; constexpr uint32_t DISCOVERY_RETRY_INTERVAL_MS = 20; +// During discovery Master alternates PWM_ACTIVE_LEVEL and PWM_SAFE_LEVEL to +// wake a sleeping Slave through the optical channel. +constexpr uint32_t OPTICAL_WAKE_HALF_PERIOD_MS = 50; 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; @@ -99,7 +108,6 @@ constexpr uint8_t MEASUREMENT_PROGRESS_STEPS = 10; constexpr uint32_t OLED_PROGRESS_UPDATE_MS = 15; constexpr uint32_t IDLE_POWER_SAVE_TIMEOUT_MS = 60000; -constexpr uint32_t IDLE_LIGHT_SLEEP_SLICE_US = 10000; constexpr uint16_t SLAVE_LISTEN_INTERVAL_MS = 100; constexpr uint16_t SLAVE_LISTEN_WINDOW_MS = 20; static_assert(SLAVE_LISTEN_WINDOW_MS < SLAVE_LISTEN_INTERVAL_MS, diff --git a/OpticalChannelTester/Display.cpp b/OpticalChannelTester/Display.cpp index 755917e..3712363 100644 --- a/OpticalChannelTester/Display.cpp +++ b/OpticalChannelTester/Display.cpp @@ -53,8 +53,8 @@ void Display::setPower(bool enabled) { Log::printf("OLED", "display power %s", enabled ? "ON" : "OFF"); } -void Display::drawTextLine(const char *text, int16_t y) { - int16_t x = 0; +void Display::drawTextLine(const char *text, int16_t y, int16_t startX) { + int16_t x = startX; while (text && *text && x + CyrillicFont::WIDTH <= oled_.width()) { const uint32_t codepoint = nextUtf8Codepoint(text); const uint8_t *glyph = CyrillicFont::glyph(codepoint); @@ -75,7 +75,8 @@ void Display::drawTextLine(const char *text, int16_t y) { } } -void Display::show(const char *a, const char *b, uint32_t progress, uint32_t progressTotal) { +void Display::show(const char *a, const char *b, uint32_t progress, + uint32_t progressTotal, const char *topRight) { char one[64], two[64]; snprintf(one, sizeof(one), "%s", a ? a : ""); snprintf(two, sizeof(two), "%s", b ? b : ""); // Serial is the primary UI mirror and remains available when OLED is absent. @@ -85,6 +86,11 @@ void Display::show(const char *a, const char *b, uint32_t progress, uint32_t pro oled_.clearDisplay(); drawTextLine(one, 3); drawTextLine(two, 19); + if (topRight && *topRight) { + oled_.fillRect(oled_.width() - CyrillicFont::ADVANCE, 0, + CyrillicFont::ADVANCE, CyrillicFont::HEIGHT + 3, SSD1306_BLACK); + drawTextLine(topRight, 3, oled_.width() - CyrillicFont::ADVANCE); + } if (progressTotal) { if (progress > progressTotal) progress = progressTotal; const uint16_t width = static_cast( diff --git a/OpticalChannelTester/Display.h b/OpticalChannelTester/Display.h index a3cdecd..969e962 100644 --- a/OpticalChannelTester/Display.h +++ b/OpticalChannelTester/Display.h @@ -8,7 +8,8 @@ class Display { Display(); bool begin(); void show(const char *line1, const char *line2, - uint32_t progress = 0, uint32_t progressTotal = 0); + uint32_t progress = 0, uint32_t progressTotal = 0, + const char *topRight = nullptr); void setPower(bool enabled); bool available() const { return ok_; } bool powered() const { return powered_; } @@ -16,7 +17,7 @@ class Display { static void formatTestFrequency(uint32_t hz, char *out, size_t size); static void formatDuration(uint64_t us, char *out, size_t size); private: - void drawTextLine(const char *text, int16_t y); + void drawTextLine(const char *text, int16_t y, int16_t startX = 0); Adafruit_SSD1306 oled_; bool ok_ = false; bool powered_ = false; diff --git a/OpticalChannelTester/Pwm.cpp b/OpticalChannelTester/Pwm.cpp index 530110c..9931a01 100644 --- a/OpticalChannelTester/Pwm.cpp +++ b/OpticalChannelTester/Pwm.cpp @@ -187,3 +187,18 @@ void PwmGenerator::stop() { #endif running_ = false; } + +void PwmGenerator::active() { + // First detach/stop the PWM peripheral, then select the independently + // configured active level. The active and safe levels may be equal. + stop(); +#if CONFIG_IDF_TARGET_ESP32C3 + digitalWrite(GPIO_PWM, PWM_ACTIVE_LEVEL); +#elif CONFIG_IDF_TARGET_ESP32S3 + if (mcpwmGenerator) mcpwm_generator_set_force_level(mcpwmGenerator, PWM_ACTIVE_LEVEL, true); + else { + pinMode(GPIO_PWM, OUTPUT); + digitalWrite(GPIO_PWM, PWM_ACTIVE_LEVEL); + } +#endif +} diff --git a/OpticalChannelTester/Pwm.h b/OpticalChannelTester/Pwm.h index 4a757ee..40d94c7 100644 --- a/OpticalChannelTester/Pwm.h +++ b/OpticalChannelTester/Pwm.h @@ -7,6 +7,7 @@ class PwmGenerator { public: void begin(); bool start(uint32_t frequencyHz, uint8_t dutyPct, ActualPwm &actual); + void active(); void stop(); bool running() const { return running_; } private: diff --git a/OpticalChannelTester/Radio.cpp b/OpticalChannelTester/Radio.cpp index df63653..2b2753f 100644 --- a/OpticalChannelTester/Radio.cpp +++ b/OpticalChannelTester/Radio.cpp @@ -56,6 +56,7 @@ void Radio::end() { if (heartbeatQueue_) xQueueReset(heartbeatQueue_); if (instance_ == this) instance_ = nullptr; windowedReceive_ = false; + if (wasActive) WiFi.mode(WIFI_OFF); Log::event("ESP-NOW", "stopped"); } diff --git a/OpticalChannelTester/Receiver.cpp b/OpticalChannelTester/Receiver.cpp index fcdefd3..3fb679a 100644 --- a/OpticalChannelTester/Receiver.cpp +++ b/OpticalChannelTester/Receiver.cpp @@ -58,7 +58,8 @@ bool PulseReceiver::configureRmt(uint32_t resolutionHz) { rmt_rx_channel_config_t cfg = {}; cfg.clk_src = RMT_CLK_SRC_DEFAULT; cfg.resolution_hz = resolutionHz; cfg.gpio_num = static_cast(GPIO_RX); - cfg.flags.invert_in = RX_SIGNAL_INVERTED; + // Internally the measurement code always treats HIGH as the active phase. + cfg.flags.invert_in = RX_ACTIVE_LEVEL == LOW; #if CONFIG_IDF_TARGET_ESP32S3 cfg.mem_block_symbols = 512; cfg.flags.with_dma = true; @@ -205,7 +206,7 @@ size_t PulseReceiver::readPeriods(PulsePeriod *periods, size_t capacity, TickTyp void IRAM_ATTR PulseReceiver::onGpio(void *ctx) { PulseReceiver *self = static_cast(ctx); bool level = gpio_get_level(static_cast(GPIO_RX)); - if (RX_SIGNAL_INVERTED) level = !level; + if (RX_ACTIVE_LEVEL == LOW) level = !level; Edge e = {esp_cpu_get_cycle_count(), static_cast(level)}; BaseType_t wake = pdFALSE; if (xQueueSendFromISR(self->queue_, &e, &wake) != pdTRUE) diff --git a/README.md b/README.md index b1ad999..43d9d97 100644 --- a/README.md +++ b/README.md @@ -149,12 +149,12 @@ MASTER <~~~~ ESP-NOW Wi-Fi channel 6 ~~~~> SLAVE На обеих ESP должны совпадать `ESPNOW_WIFI_CHANNEL` и версия прошивки. Slave сначала переводится в `SLAVE READY` кнопкой START, затем START нажимается на Master. -В `SLAVE READY` радиоприём работает периодически: окно 20 мс каждые 100 мс. Master во время поиска посылает серию `DISCOVER` каждые 20 мс. После обнаружения Master Slave автоматически включает непрерывный приём на всю тестовую сессию, а после завершения возвращается к периодическому режиму. Параметры задаются `SLAVE_LISTEN_WINDOW_MS`, `SLAVE_LISTEN_INTERVAL_MS` и `DISCOVERY_RETRY_INTERVAL_MS` в `Config.h`. +Пока бодрствующий Slave находится в `SLAVE READY`, радиоприём работает периодически: окно 20 мс каждые 100 мс, а собственный оптический передатчик Slave отключён уровнем `PWM_SAFE_LEVEL`. При переходе Slave в light sleep ESP-NOW и Wi-Fi полностью отключаются, поэтому радиопакет не может его разбудить. Master во время поиска посылает серию `DISCOVER` каждые 20 мс и одновременно переключает оптический выход между `PWM_ACTIVE_LEVEL` и `PWM_SAFE_LEVEL` каждые `OPTICAL_WAKE_HALF_PERIOD_MS`. Изменение уровня на оптическом входе пробуждает Slave через GPIO; после этого Slave запускает ESP-NOW и принимает повторяемый `DISCOVER`. Постоянный HIGH или LOW сам по себе повторных пробуждений не вызывает. После обнаружения Master прекращает wake-сигнал, а Slave включает непрерывный радиоприём на всю тестовую сессию. Параметры задаются в `Config.h`. -**GPIO ESP32 допускают только логические уровни 0…3.3 В.** Не подавайте 5 В на `GPIO_RX`; применяйте согласование уровня. Если оптический приёмник инвертирует сигнал, установите: +**GPIO ESP32 допускают только логические уровни 0…3.3 В.** Не подавайте 5 В на `GPIO_RX`; применяйте согласование уровня. Полярность выхода оптического приёмника задаётся однозначно его активным уровнем: ```cpp -#define RX_SIGNAL_INVERTED true +#define RX_ACTIVE_LEVEL HIGH // либо LOW ``` ## Управление @@ -177,7 +177,7 @@ MASTER <~~~~ ESP-NOW Wi-Fi channel 6 ~~~~> SLAVE ### Энергосбережение -В ожидании процессор работает на 80 МГц, во время теста и связанной MASTER/SLAVE-сессии — на 160 МГц. После тайм-аута бездействия в `IDLE`, `MENU`, `FINISHED` или `SLAVE READY` OLED выключается, а ESP переходит в короткие циклы light sleep. Экран и текущее состояние сохраняются. При использовании встроенного USB Serial/JTAG для следующей прошивки может потребоваться ручной вход в загрузчик кнопкой BOOT. +В ожидании процессор работает на 80 МГц, во время теста и связанной MASTER/SLAVE-сессии — на 160 МГц. Пока Solo или Master бодрствует и тест не идёт, выход передатчика удерживается на `PWM_ACTIVE_LEVEL`; в режиме Slave передатчик всегда отключён уровнем `PWM_SAFE_LEVEL`. При запуске теста, остановленном PWM и после тайм-аута сна также используется `PWM_SAFE_LEVEL` — уровень полного выключения с минимальным потреблением тока. После тайм-аута бездействия в `IDLE`, `MENU`, `FINISHED` или `SLAVE READY` OLED выключается, а ESP переходит в непрерывный light sleep без периодических таймерных пробуждений. Экран и текущее состояние сохраняются. START и MODE будят любую роль, а фронт на оптическом входе дополнительно будит Slave. При использовании встроенного USB Serial/JTAG для следующей прошивки может потребоваться ручной вход в загрузчик кнопкой BOOT. START и MODE будят устройство. Пробуждающее нажатие намеренно не выполняет действие и полностью игнорируется до отпускания кнопки; действие выполняет только следующее нажатие. Принятый `DISCOVER` также немедленно выводит Slave из энергосбережения. Тайм-аут задаётся `IDLE_POWER_SAVE_TIMEOUT_MS` в `Config.h`.