# Optical Channel Tester — ESP32-C3 / ESP32-S3

Полностью рабочий Arduino IDE-проект для строгой проверки оптического цифрового канала. Устройство формирует PWM аппаратным LEDC, пропускает его через проверяемый канал и проверяет каждый завершённый период отдельно: частоту и заполнение. Поддерживаются роли `SOLO`, `MASTER` и `SLAVE`; роли выбираются только вручную.

## Что реализовано

- неблокирующий конечный автомат без `pulseIn()` и длинных `delay()`;
- аппаратный LEDC с расчётом реально получившихся частоты, разрядности и duty;
- Arduino-ESP32 3.3.10: потоковый аппаратный RMT RX (`partial RX`), ping-pong на C3 и DMA на S3;
- безопасный fallback для старых Arduino-ESP32 3.x через GPIO edge ISR и аппаратный CPU cycle counter (верхний предел при этом автоматически ограничен 100 кГц);
- отбрасывание ровно `PWM_SETTLE_CYCLES` полных периодов;
- потоковая статистика без хранения всех периодов;
- немедленный FAIL по первому плохому периоду;
- десять измерительных участков на каждой частоте с промежуточным отображением результата;
- ESP-NOW discovery, handshake, CRC, session/stage/sequence, ACK, повторы и защита от старых пакетов;
- SSD1306 128×32: каждый экран всегда состоит ровно из двух строк;
- две кнопки, debounce, long press и repeat; после long press ложный short click не создаётся;
- Preferences/NVS с версией структуры, checksum, проверкой индексов и восстановлением defaults;
- работа через Serial при отсутствующем OLED;
- безопасное выключение PWM при PASS, FAIL, ABORT и потере связи.
- полный журнал действий в Serial с временными метками; OLED необязателен.

## Файлы

Arduino sketch находится в каталоге `OpticalChannelTester`:

- `OpticalChannelTester.ino` — стандартная точка входа Arduino IDE;
- `Config.h` — GPIO, тайм-ауты, пределы и все пользовательские массивы;
- `Core.*` — последовательность частот, ALL, допуски, статистика;
- `Buttons.*` — автомат двух кнопок;
- `SettingsStore.*` — NVS;
- `Display.*` — OLED и компактное форматирование;
- `Log.*` — журнал действий и Serial-зеркало интерфейса;
- `Pwm.*` — LEDC;
- `Receiver.*` — RMT RX / совместимый fallback;
- `Measurement.*` — строгая проверка периодов;
- `Protocol.*`, `Radio.*` — ESP-NOW;
- `App.*` — общий конечный автомат SOLO/MASTER/SLAVE.

Каталог `tests` содержит локальные unit-тесты чистой логики и автомата кнопок.

## Требования Arduino IDE

Проверенная конфигурация:

- Arduino IDE 2.x;
- пакет плат **esp32 by Espressif Systems 3.3.10**;
- **Adafruit GFX Library 1.12.1**;
- **Adafruit SSD1306 2.5.15**;
- Adafruit BusIO устанавливается Library Manager как зависимость.

В Arduino IDE откройте `OpticalChannelTester/OpticalChannelTester.ino`. Не переносите один `.ino` отдельно: остальные вкладки являются частью скетча.

Для C3 выберите подходящую плату ESP32-C3, например `ESP32C3 Dev Module`. Для S3 — `ESP32S3 Dev Module`. Затем выберите порт и нажмите Verify/Upload.

## GPIO

Все назначения находятся в начале `Config.h` и могут быть изменены до сборки.

Сейчас в `Config.h` включён `#define MAKETKA`. В этом профиле используются следующие назначения:

| Сигнал | ESP32-C3 | ESP32-S3 |
|---|---:|---:|
| PWM output | GPIO 3 | GPIO 12 |
| Optical RX input | GPIO 4 | GPIO 13 |
| START | GPIO 1 | GPIO 10 |
| MODE | GPIO 0 | GPIO 9 |
| OLED SDA | GPIO 6 | GPIO 44 |
| OLED SCL | GPIO 7 | GPIO 1 |

Если закомментировать `#define MAKETKA`, включается профиль производственной PCB:

| Сигнал | ESP32-C3 | ESP32-S3 |
|---|---:|---:|
| PWM output | GPIO 3 | GPIO 12 |
| Optical RX input | GPIO 4 | GPIO 13 |
| START | GPIO 10 | GPIO 4 |
| MODE | GPIO 20 | GPIO 5 |
| VBAT | GPIO 2 | GPIO 11 |
| Analog RX | GPIO 0 | GPIO 9 |
| OLED SDA | GPIO 6 | GPIO 44 |
| OLED SCL | GPIO 7 | GPIO 1 |

Оба профиля рассчитаны на установку S3 с совмещением контактов `5V` и `GND` с модулем C3. Номера GPIO S3 выбраны по тому же физическому ряду разъёма, а не по совпадению номера GPIO.

На ESP32-C3 PWM формируется через LEDC с целым делителем. На ESP32-S3 используется отдельный аппаратный MCPWM со счётчиком 40 МГц и произвольным целым периодом.

### OLED

Подключите SSD1306 128×32: `VCC → 3.3 V`, `GND → GND`, `SDA/SCL` по таблице. Адрес по умолчанию `0x3C`. Если OLED не отвечает, тест продолжает работать и пишет диагностику в Serial 115200.

### Работа вообще без OLED

OLED можно не подключать. Откройте Serial Monitor на **115200 baud**: каждый экран всегда дублируется одной строкой вида:

```text
[      1250][UI      ] MODE: SOLO | START=RUN
```

Полоса прогресса учитывает номер частоты и десять измерительных участков внутри неё. Например, диапазон из 11 частот даёт 110 последовательных позиций прогресса.

В Serial также выводятся:

- каждое распознанное нажатие START/MODE и текущее состояние автомата;
- вход, изменение и сохранение каждого пункта меню;
- загрузка, проверка и сохранение NVS;
- запуск/остановка PWM и реальные параметры LEDC;
- начало этапа, число отбрасываемых периодов и параметры измерительного окна;
- все действия ESP-NOW, peer, session/stage/sequence, ACK и повторы;
- статистика этапа, первый плохой период и итоговая причина завершения;
- все строки, которые были бы показаны на OLED.

Во время строгого измерительного окна отдельные импульсы намеренно не печатаются: они проверяются потоково, а итоговая статистика выводится после окна. Это предотвращает влияние Serial на точность и переполнение очереди RMT. Журнал действий включён параметрами в `Config.h`:

```cpp
constexpr bool SERIAL_ACTION_LOG = true;
constexpr bool SERIAL_LOG_TIMESTAMPS = true;
```

### Кнопки

По умолчанию задано:

```cpp
#define BUTTON_ACTIVE_LEVEL LOW
```

Поэтому каждая кнопка подключается между своим GPIO и GND, а прошивка автоматически включает `INPUT_PULLUP`.

Если установить `BUTTON_ACTIVE_LEVEL HIGH`, подключайте кнопку между GPIO и 3.3 V; автоматически будет использован `INPUT_PULLDOWN`. Других изменений логики не требуется.

## Подключение сигнала

### SOLO

```text
ESP GPIO_PWM -> вход передатчика оптического канала
выход приёмника оптического канала -> ESP GPIO_RX
GND ESP -> GND входной/выходной электроники (если канал не гальванически развязан)
```

### MASTER / SLAVE

```text
MASTER GPIO_PWM -> вход передатчика проверяемого канала
выход приёмника проверяемого канала -> SLAVE GPIO_RX
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`.

**GPIO ESP32 допускают только логические уровни 0…3.3 В.** Не подавайте 5 В на `GPIO_RX`; применяйте согласование уровня. Если оптический приёмник инвертирует сигнал, установите:

```cpp
#define RX_SIGNAL_INVERTED true
```

## Управление

В ожидании:

- MODE short: `SOLO → MASTER → SLAVE → SOLO`, выбранная роль сохраняется;
- MODE long: открыть настройки;
- START short: начать тест;
- START long во время теста: ABORT, PWM немедленно выключается.

В настройках:

- MODE short: следующий параметр;
- MODE long: проверить диапазон, сохранить один раз в NVS и выйти;
- START short: следующее значение;
- START long: предыдущее значение, затем autorepeat при удержании.

Удержание обеих кнопок минимум 1.5 с при включении восстанавливает defaults.

### Энергосбережение

В ожидании процессор работает на 80 МГц, во время теста и связанной MASTER/SLAVE-сессии — на 160 МГц. После тайм-аута бездействия в `IDLE`, `MENU`, `FINISHED` или `SLAVE READY` OLED выключается, а ESP переходит в короткие циклы light sleep. Экран и текущее состояние сохраняются. При использовании встроенного USB Serial/JTAG для следующей прошивки может потребоваться ручной вход в загрузчик кнопкой BOOT.

START и MODE будят устройство. Пробуждающее нажатие намеренно не выполняет действие и полностью игнорируется до отпускания кнопки; действие выполняет только следующее нажатие. Принятый `DISCOVER` также немедленно выводит Slave из энергосбережения. Тайм-аут задаётся `IDLE_POWER_SAVE_TIMEOUT_MS` в `Config.h`.

## Настройка диапазона

START и END выбираются из отдельных массивов `START_FREQ_OPTIONS_HZ` и `END_FREQ_OPTIONS_HZ` в `Config.h` и зацикливаются независимо. Полный список точных частот заранее рассчитан для XTAL 40 МГц, целого делителя LEDC и таймера 1…14 бит и записан в `TEST_FREQUENCIES_HZ`. Сохранённые индексы всегда проверяются; после изменения массивов повреждённая/несовместимая настройка не приводит к выходу за границы.

Отдельной настройки STEP нет: тест начинается с выбранной START, проходит все соседние достижимые точки из полного списка и заканчивается на выбранной END.

`ALL` пересчитывается после изменения START, END или TEST TIME и отображается с точностью до целой секунды. Для каждой точки учитывается:

```text
max(TEST_TIME, periods / RX_PROCESSING_PERIODS_PER_SECOND)
+ ожидание заполнения RMT-пакета × 10
+ PWM_SETTLE_CYCLES / actualFrequency × 10
+ OLED_PROGRESS_UPDATE_MS × 11
```

`TEST TIME` — это чистое время выборки сигнала, а не полная длительность этапа. На высоких частотах полная длительность заметно возрастает из-за проверки каждого периода. Расчёт использует консервативную производительность 300 тысяч периодов в секунду, полученную из реального журнала ESP32-C3.

## Строгая проверка

После каждой перенастройки PWM приёмник:

1. выбирает максимальную допустимую частоту RMT 20, 40 или 80 МГц с учётом частоты и duty;
2. отбрасывает ровно `PWM_SETTLE_CYCLES` полных периодов;
3. очищает статистику;
4. проверяет все полные периоды десяти участков TEST TIME и между ними показывает промежуточный результат.

Настройка точности `1%` использует фактический допуск `1,25%`, соответствующий одному такту RMT 80 МГц на частоте сигнала 1 МГц.

Незавершённый период в начале и конце окна не учитывается. Период, пересекающий границу повторов, не теряется. Для каждого периода отдельно вычисляются частота и duty; средние используются только для диагностики. Любой один выход за допуск немедленно завершает всю проверку.

Причины: `NO SIGNAL`, `PERIOD OUT`, `DUTY OUT`, `EXTRA EDGE`, `GLITCH`, `LOST EDGE`, `DATA LOSS ERROR`, `LINK LOST`, `UNSUPPORTED`, `RESOLUTION`, `ABORTED`.

### Пределы

- C3: гарантированный проектный диапазон до 10 кГц; настраиваемый строгий предел по умолчанию 100 кГц;
- S3: строгий предел до 1 МГц только при рабочем RMT DMA;
- выше строгого аппаратного предела выдаётся `UNSUPPORTED`, ослабленной проверки нет;
- комбинации, для которых LEDC или RX timer не дают выбранную точность, завершаются `RESOLUTION` до запуска заведомо неверного теста.

Реальный предел зависит от конкретной платы, разводки, формы фронтов, Wi-Fi нагрузки и оптического оборудования. Начинайте проверку с 100 Гц…10 кГц.

## Первый запуск SOLO

1. Соберите схему SOLO и ещё раз убедитесь, что на `GPIO_RX` не бывает напряжения выше 3.3 В.
2. Откройте Serial Monitor на 115200 baud.
3. Включите плату. На OLED должно быть `MODE: SOLO` / `START=RUN`.
4. Если выбран другой режим, коротко нажимайте MODE до SOLO.
5. Для первого опыта оставьте defaults: 100 Гц…10 кГц, duty 50%, accuracy 5%.
6. Коротко нажмите START.
7. Serial покажет запрошенные и фактические параметры LEDC, список частот, ALL и статистику каждой точки.
8. Успех: `PASS ...` / `START=AGAIN`. Ошибка: `FAIL AT ...` и точная причина.

## Проверка проекта

Целевая версия проекта — Arduino-ESP32 3.3.10. В доступной среде исходный код дополнительно собран Arduino CLI с установленным Arduino-ESP32 3.3.0:

| Target | Arduino-ESP32 | Flash | RAM | Результат |
|---|---:|---:|---:|---|
| ESP32-C3 | 3.3.0 | 1,049,316 B (80%) | 44,312 B (13%) | PASS |
| ESP32-S3 | 3.3.0 | 980,407 B (74%) | 53,456 B (16%) | PASS |

Локальные unit-тесты: `core tests: PASS`, `button tests: PASS`. Они покрывают неделимый диапазон, END без дубля, ALL, границы допусков, немедленный FAIL, resolution, checksum настроек, CRC протокола и отсутствие short после long.

Физическое оборудование в этой среде недоступно, поэтому реальные оптические фронты, RMT DMA под длительной нагрузкой, дальность ESP-NOW и электрическая совместимость должны быть проверены на ваших платах. Сборка и программные тесты не выдаются за аппаратный тест.
