# Теория: как APDS-9960 «видит» приближение

## 1. Инфракрасный дальномер на пальцах

Внутри APDS-9960 спрятаны две вещи:

- **ИК-светодиод** — светит невидимым для человека светом с длиной волны 940 нм
  (для сравнения: видимый нам свет — это 400-700 нм, а 940 нм — уже глубоко
  в инфракрасном диапазоне, том самом, которым «светят» пульты от телевизора).
- **Фотодиод** — ловит отражённый от объекта свет.

Логика простая: чем ближе объект, тем больше ИК-света от него отражается
обратно на фотодиод. Датчик не измеряет расстояние в сантиметрах напрямую —
он измеряет **интенсивность отражённого света** и превращает её в число
0-255 через встроенный АЦП. Это число называется PDATA, и именно его
возвращает `readProximity()` ([Apds9960.cpp:21-23](src/Apds9960.cpp#L21-L23)).

Важное следствие: PDATA — это не линейная шкала сантиметров. Тёмная
матовая ткань отразит меньше света, чем светлая глянцевая поверхность,
даже на одинаковом расстоянии. Поэтому пороги калибруются экспериментально
(см. раздел 4), а не берутся по формуле «сантиметры → PDATA».

## 2. Как ESP32 разговаривает с датчиком: I2C

APDS-9960 подключается всего 4 проводами, из которых два — SDA и SCL —
образуют шину I2C. Это «общая линия связи», на которой может сидеть сразу
несколько устройств — у каждого свой адрес. У APDS-9960 адрес фиксированный:

```cpp
static constexpr uint8_t kAddress = 0x39;
```
([Apds9960.h:29](include/Apds9960.h#L29))

Когда ESP32 хочет что-то прочитать, она сначала «стучится» по этому адресу
и называет номер регистра (ячейки памяти внутри чипа), а затем запрашивает
данные. Это видно в `readRegister()` ([Apds9960.cpp:44-53](src/Apds9960.cpp#L44-L53)):
сначала `beginTransmission` + `write(reg)`, затем `requestFrom`, который
запрашивает ответ.

Каждая I2C-транзакция может провалиться (обрыв провода, дребезг питания,
неверная перемычка) — поэтому в драйвере проверяется код возврата
`endTransmission()` ([Apds9960.cpp:45-56](src/Apds9960.cpp#L45-L56)): если он
не 0, чтение или запись считается неудачной, и наружу возвращается `false`,
а не тихо подставляется старое/нулевое значение.

## 3. Регистры, которые мы используем

Регистр — это именованная ячейка памяти внутри чипа: что-то среднее между
переключателем и почтовым ящиком. Источник — датащит Broadcom/Avago
APDS-9960 (AV02-4191EN), раздел «Register Descriptions».

| Регистр | Адрес | Что делает | Где в коде |
|---------|-------|------------|------------|
| ENABLE  | `0x80` | Бит 0 (PON) включает чип, бит 2 (PEN) — proximity-движок | [Apds9960.cpp:9,18](src/Apds9960.cpp#L18) |
| PPULSE  | `0x8E` | Сколько ИК-импульсов излучить и какой длины — больше импульсов = дальше видит, но больше энергии тратит | [Apds9960.h:46](include/Apds9960.h#L46) |
| CONTROL | `0x8F` | Мощность ИК-светодиода (LDRIVE) и усиление приёмника (PGAIN) | [Apds9960.h:47](include/Apds9960.h#L47) |
| ID      | `0x92` | «Паспорт» чипа — что он сам о себе сообщает | [Apds9960.cpp:12-14](src/Apds9960.cpp#L12-L14) |
| PDATA   | `0x9C` | Итоговое значение приближения, 0-255 | [Apds9960.cpp:24-26](src/Apds9960.cpp#L24-L26) |

Значения `kPPulseDefault = 0x10` и `kControlDefault = 0xD0`
([Apds9960.h:52-53](include/Apds9960.h#L52-L53)) — это не «магия из даташита»,
а рабочая конфигурация (16 импульсов, средняя мощность ИК-диода и усиление x5),
которая на практике даёт стабильные показания у большинства модулей на рынке.
Если ваш датчик видит слишком близко или слишком далеко — это первое место
для эксперимента.

## 4. Проблема клонов: когда чип «врёт» о себе

У каждого чипа есть регистр ID (`0x92`), по которому программа может
проверить: «это точно тот чип, для которого я написана?». По даташиту
оригинальный APDS-9960 должен ответить `0xAB`.

Но на массовом рынке продаются модули с чипом, который отвечает `0xA8` —
это не значит, что модуль сломан или это подделка, которая не работает.
Это другая ревизия/клон того же по сути кристалла, который прекрасно
измеряет приближение, но «представляется» другим именем. Классические
библиотеки для оригинального APDS-9960 при виде `0xA8` обычно отказываются
работать, потому что жёстко проверяют `id == 0xAB`.

Драйвер этого компонента устроен иначе — `begin()` не проваливается из-за
неожиданного ID, а просто запоминает, что реально ответил чип
([Apds9960.cpp:12-14](src/Apds9960.cpp#L12-L14)). `main.cpp` при старте
показывает три возможных статуса: оригинал, известный клон, или
неопознанный чип — и в любом из трёх случаев proximity продолжает работать
([main.cpp:19-29](src/main.cpp#L19-L29)).

## 5. Калибровка: почему пороги — не часть драйвера

Пороги `THRESHOLD_NEAR/CLOSE/VERY_CLOSE` находятся не в `Apds9960.h`, а в
`main.cpp` ([main.cpp:12-14](src/main.cpp#L12-L14)). Это осознанное решение:
разброс сырых значений PDATA между экземплярами датчика (даже одной партии)
достаточно большой, чтобы «зашитые» пороги подошли одному ученику и были
бесполезны для другого. Драйвер честно отдаёт то, что измерил чип, а решение
«что считать близко» — задача калибровки под конкретное железо и условия
освещения в классе.

Числа в Serial Monitor скачут и их трудно оценить на глаз — нагляднее увидеть
это на графике: [tools/proximity_view.py](tools/proximity_view.py) рисует
PDATA во времени вместе с линиями порогов, см. [README.md, раздел «Визуализация»](README.md#визуализация).

## 6. Задержка после включения

После записи PON=1 в ENABLE чипу нужно время, чтобы «прогреться» —
стабилизировать питание ИК-светодиода и АЦП. Даташит описывает только
внутренние тайминги самого цикла измерения, а не время «безопасного» первого
чтения после включения на реальном (не всегда качественном) модуле. Поэтому
в `begin()` используется практический запас `kPowerOnWarmupMs = 200`
([Apds9960.h:58](include/Apds9960.h#L58)) — это эмпирическое значение
с запасом, а не цифра из даташита.
