# 📘 Учебное пособие: GPS NEO на ESP32

Это пособие проведёт тебя от "что такое GPS вообще" до "могу объяснить каждую строчку `GpsNeo.cpp`". Читай по порядку — каждый следующий раздел опирается на предыдущий.

## Чему ты научишься

- Как спутники превращаются в координаты (трилатерация, протокол NMEA).
- Как устроена контрольная сумма и зачем она нужна на реальной линии связи с помехами.
- Как прошивка читает GPS побайтово и разбирает строку — с разбором настоящего кода `GpsNeo.cpp`.
- Почему "спутников видно много", а координат всё ещё нет.
- Как читать диагностику в консоли и находить неисправность по её виду.

---

## 1. Как вообще работает GPS

Вокруг Земли летает группировка спутников. Каждый спутник постоянно передаёт два числа: **где он находится** и **который сейчас час** (по очень точным атомным часам).

Приёмник (наш модуль NEO) слушает несколько спутников одновременно и по разнице времени сигнала до каждого высчитывает расстояние до него. Это называется **трилатерацией**:

- 3 спутника → можно вычислить широту и долготу (2D).
- 4 спутника → добавляется высота (3D), а заодно уточняется собственная погрешность часов приёмника.

**Точность.** У обычного модуля без RTK/DGPS типичная погрешность — около 2.5 метра (CEP). Зависит от числа видимых спутников и того, насколько равномерно они разбросаны по небу (это называют DOP — Dilution of Precision). Держи это число в голове: пытаться доехать роботом ровно "в точку" бессмысленно, GPS так не умеет — подробнее в [ROADMAP.md](ROADMAP.md).

## 2. Протокол NMEA 0183 — язык, на котором говорит модуль

Модули NEO-6M/7M/8M по умолчанию непрерывно шлют текстовые строки в формате NMEA. Открой Serial Monitor без разбора — увидишь поток вида:

```
$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
```

Разберём эту строку по кусочкам:

| Кусок | Значение |
|---|---|
| `$` | начало строки |
| `GP` | талкер (какая система: `GP`=GPS, `GL`=GLONASS, `GA`=Galileo, `BD`/`GB`=BeiDou, `GN`=комбинированное сообщение) |
| `GGA` | тип сообщения (что именно передаётся) |
| `,123519,4807.038,N,...` | поля данных через запятую |
| `*47` | контрольная сумма в hex |

Три сообщения важны для нашего проекта:

1. **`$..RMC`** — время, дата, статус (`A`=valid/`V`=void), координаты, скорость.
2. **`$..GSV`** — сколько спутников видно **одной конкретной системе**. Каждая система шлёт свою отдельную группу сообщений.
3. **`$..GGA`** — качество приёма и число спутников, **использованных** в расчёте координат (не просто видимых).

> ⚠️ **Важная ловушка.** "Видно спутников" в нашем коде — это **сумма** по всем `$..GSV` от разных систем, а не значение из одного сообщения. Если взять только последнее пришедшее GSV, получится "видно 2" (BeiDou), хотя GPS-система отдельно видит 17–20. Каждая система считает только свои спутники — поэтому в `GpsNeo` они складываются отдельно (раздел 4.4).

## 2.1 Как `ddmm.mmmm` превращается в десятичные градусы

В строке `$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,...` координаты
записаны не в привычном формате "48.1173°", а как `4807.038` — это **не** число
с 4 знаками после запятой в градусах, а формат `ddmm.mmmm` (широта) /
`dddmm.mmmm` (долгота):

- Первые 2 цифры (широта) или 3 цифры (долгота) — это **целые градусы**.
- Всё, что после них (включая дробную часть) — это **минуты** (1° = 60 минут).

Для `4807.038`: `48` — градусы, `07.038` — минуты. Перевод в десятичные градусы:

```
degrees = 48 + 07.038 / 60 = 48.1173°
```

Именно это делает `GpsNeo::parseNmeaCoordinate()` в `GpsNeo.cpp` — находит
позицию точки, отсчитывает от неё 2 (или 3) цифры назад для целых градусов,
остаток строки — минуты. Знак определяется полушарием: `N`/`E` — положительный,
`S`/`W` — отрицательный (поэтому Австралия и Аргентина получают отрицательную
широту, а США западного побережья — отрицательную долготу).

## 3. Контрольная сумма (checksum)

Провод между модулем и ESP32 — не идеальная среда: наводки, помехи, дребезг контактов могут испортить один бит на лету. Чтобы не доверять испорченным данным, каждая строка NMEA заканчивается контрольной суммой:

```
$GPGGA,...,*47
        └────┘
        XOR всех байт между '$' и '*', записанный как hex
```

Если пересчитанная сумма не совпадает с той, что указана в строке — строка отбрасывается целиком, её данные не используются. Это ровно то, что делает `GpsNeo::validateChecksum()`.

## 4. Как код разбирает строку

Открой [`src/GpsNeo.cpp`](src/GpsNeo.cpp) — дальше идём по нему сверху вниз.

### 4.1 Побайтовое чтение — `process()`

GPS-модуль не присылает строку целиком одним куском — он шлёт байты по одному, как будто печатает по букве в секунду (на самом деле быстрее, но принцип тот же). `process()` вызывается на каждой итерации `loop()` и:

1. Проверяет, есть ли новый байт (`_serial->available()`).
2. Читает байт, копит его в `_buffer`.
3. Как только пришёл символ `'\n'` (конец строки) — строка готова, можно её разбирать.

```cpp
bool GpsNeo::process(Stream &target) {
    if (!_serial->available()) return false;
    char c = _serial->read();
    ...
    if (c != '\n') return false;   // строка ещё не закончена — выходим
    ...                             // а вот тут строка уже полная
}
```

Это классический паттерн для встраиваемых систем: **никогда не блокировать `loop()`** ожиданием данных. Функция либо мгновенно обрабатывает один байт, либо мгновенно выходит — контроллер всегда остаётся отзывчивым для остального кода (мигание светодиода, чтение других датчиков и т.д.).

### 4.2 Проверка суммы — `validateChecksum()`

```cpp
for (int i = 1; i < starIndex; i++) {
    calculated ^= (uint8_t)line[i];
}
```

Именно тот XOR из раздела 3, реализованный в цикле: `1` — потому что байт `$` в сумму не входит, `starIndex` — позиция символа `*`.

### 4.3 Извлечение поля — `getField()`

NMEA-строка — это данные через запятую. `getField(line, len, fieldIndex, ...)` возвращает N-е поле по счёту (поле `0` — это тип сообщения после талкера). Функция идёт по строке и считает запятые, пока не насчитает нужный номер поля.

Пример на нашей строке `$GPGGA,123519,4807.038,N,...,08,...`:

| fieldIndex | значение |
|---|---|
| 0 | `GGA` |
| 1 | `123519` (время) |
| 6 | качество фикса |
| 7 | `08` (спутников использовано) |

Именно эти номера — `6` и `7` — не magic numbers в коде, а именованные константы в начале файла:

```cpp
constexpr uint8_t kGgaFieldFixQuality = 6;
constexpr uint8_t kGgaFieldSatellitesUsed = 7;
```

### 4.4 Спутники "видно" по каждой системе отдельно — `updateSatellitesInView()`

Как отмечено в разделе 2, каждая система (`GP`, `GL`, `GA`, `BD`) шлёт свой собственный `$..GSV`. Код хранит счётчик отдельно для каждого талкера в массиве `_talkerSats[kMaxTalkers]`, и когда нужно узнать общее число (`getSatellitesInView()`), просто складывает все активные слоты. Так GPS + GLONASS + Galileo + BeiDou не перезатирают друг друга, а суммируются.

### 4.5 Качество фикса — `FixQuality`

```cpp
enum class FixQuality : uint8_t {
    NoFix = 0,
    GpsFix = 1,
    DgpsFix = 2,
};
```

Вместо "голого" числа `0`/`1`/`2`, разбросанного по коду, используется именованный тип. `getFixQuality() == FixQuality::NoFix` читается однозначно, а `getFixQuality() == 0` пришлось бы каждый раз держать в голове как расшифровку.

---

## 5. Почему "видно 17 спутников", а Fix всё ещё нет

Видеть спутник (поймать несущую частоту, получить уровень сигнала) и иметь Fix (декодировать навигационное сообщение и решить систему уравнений) — **разные этапы**:

- **Эфемериды** (точные координаты спутника на орбите) передаются медленно — полный кадр данных занимает до 30 секунд *на каждый спутник отдельно*, и нужно успешно принять кадр хотя бы от 4 спутников **одновременно**.
- **Холодный старт** (нет сохранённого альманаха) — может потребоваться от нескольких минут до 10 на открытом небе.
- **Тёплый/горячий старт** — секунды, если на плате есть батарейка RTC, сохраняющая альманах между включениями.
- **Слабый сигнал** (в помещении, под крышей, за стеклом) — кадр эфемерид чаще обрывается из-за помех, декодирование "не успевает" — спутник виден, но бесполезен для Fix.

**Практический вывод:** если модуль долго не даёт Fix — не спеши искать баг в коде. Вынеси модуль к окну с прямым видом на небо и подожди 1–2 минуты.

---

## 6. Как читать диагностику в консоли

Прошивка выводит статус раз в 2 секунды:

```
[Связь: OK] [Строк: 1942, валидных: 1942] [Видно спутников: 17] [Fix: ДА, исп. в fix: 8]
Lat:48.117300,Lon:11.516700,SpeedKmh:12.3,Course:84.4,Alt:545.4,HDOP:0.9
```

| Что смотреть | Что значит | Если плохо |
|---|---|---|
| `Связь: OK` / `!!! GPS НЕ ОТВЕЧАЕТ !!!` | Идут ли байты по UART вообще | Проверь TX↔RX (см. [WIRING.md](WIRING.md)), питание, пины |
| `Строк` vs `валидных` | Сколько строк прошли проверку checksum | Если валидных меньше — помехи на линии |
| `Видно спутников` | Сумма по ВСЕМ созвездиям (GPS+BeiDou+...) | 0 при "Связь: OK" — антенна/обзор неба |
| `Fix` / `исп. в fix` | Декодированы ли координаты | Долго "нет" при многих видимых — см. раздел 5 |
| `Lat`/`Lon`/`SpeedKmh`/`Course`/`Alt`/`HDOP` | Координаты и кинематика из `getPosition()` — печатаются только когда есть fix | `HDOP` большой (>2-3) — плохая геометрия спутников, точность ниже |

Строка `Lat:.../Lon:...` — машиночитаемый формат специально для
[tools/gps_track_view.py](tools/gps_track_view.py), который рисует живой трек
движения (см. [README.md, раздел «Визуализация»](README.md#-визуализация)).

Светодиод дублирует уровень 1 без чтения консоли: **горит ровно** = связь стабильна, **мигает быстро** = обрыв связи с модулем.

---

## 7. Проверь себя

Постарайся ответить, не подглядывая в код, потом сверься.

1. Строка `$GPGGA,...,08,...*47` пришла, но одна цифра в середине строки исказилась помехой. Что произойдёт в коде? Какая функция это поймает и что с строкой сделает дальше `process()`?
   <details><summary>Ответ</summary>Пересчитанный XOR в <code>validateChecksum()</code> не совпадёт с <code>47</code>. <code>_lastLineValid</code> станет <code>false</code>, <code>parseSentence()</code> для этой строки не вызовется — данные из неё нигде не используются, счётчик <code>_validLineCount</code> не увеличится.</details>

2. Консоль десять минут подряд показывает `Видно спутников: 19`, `Fix: нет`. GPS сломан?
   <details><summary>Ответ</summary>Скорее всего нет — см. раздел 5. Скорее всего декодирование эфемерид не успевает завершиться (слабый сигнал/холодный старт). Проверь, что антенна смотрит в открытое небо, и подожди ещё.</details>

3. Почему `getSatellitesInView()` не может просто "взять последнее значение из последнего `$..GSV`"?
   <details><summary>Ответ</summary>Потому что разные системы (GPS/GLONASS/Galileo/BeiDou) шлют отдельные наборы <code>$..GSV</code>, и последний пришедший — это только одна система. Нужно суммировать по всем активным талкерам, что и делает <code>_talkerSats[]</code>.</details>

4. Зачем `process()` выходит из функции сразу после чтения одного байта, а не читает строку целиком в цикле `while`?
   <details><summary>Ответ</summary>Чтобы не блокировать <code>loop()</code> в ожидании следующего байта — иначе вся остальная логика прошивки (диагностика, светодиод, будущий контроль моторов) встанет в ожидании GPS.</details>

5. Строка NMEA содержит широту `01131.000` (это долгота, три цифры градусов). Почему `parseNmeaCoordinate()` не может использовать те же первые 2 цифры, что и для широты?
   <details><summary>Ответ</summary>Потому что долгота измеряется от −180° до +180°, а широта только от −90° до +90° — долготе нужна третья цифра для трёхзначных градусов (100-180°). Функция вычисляет число цифр градусов по позиции десятичной точки (<code>dot - field</code> минус 2 цифры минут), поэтому одинаково работает и для 2-значной широты, и для 3-значной долготы.</details>

---

## Куда дальше

Разобрался с базовым чтением GPS? Следующий уровень — научить ровер ехать к точке по этим координатам: [ROADMAP.md](ROADMAP.md).
