151 lines
14 KiB
Markdown
151 lines
14 KiB
Markdown
# Архитектура системы
|
||
|
||
Описание устройства GPIO Monitor: компоненты, поток данных, конкурентность, топологии развёртывания.
|
||
|
||
**Аудитория:** разработчики.
|
||
|
||
## Содержание
|
||
|
||
- [Общая схема](#общая-схема)
|
||
- [Захват GPIO и доставка данных](#захват-gpio-и-доставка-данных)
|
||
- [Компоненты Go-сервера](#компоненты-go-сервера)
|
||
- [Порядок инициализации](#порядок-инициализации)
|
||
- [Конкурентность](#конкурентность)
|
||
- [Встраивание фронтенда](#встраивание-фронтенда)
|
||
- [Топологии развёртывания](#топологии-развёртывания)
|
||
|
||
## Общая схема
|
||
|
||
```
|
||
GPIO-шина (8 бит)
|
||
│ прерывание по стробу WR
|
||
▼
|
||
gpio-interrupt (C + WiringPi) ← отдельная программа, вне этого репозитория
|
||
│ stdout → socket (systemd)
|
||
▼
|
||
FIFO pipe /tmp/gpio_pipe ← создаёт systemd (monitor-gpio.socket)
|
||
│ сырые байты
|
||
▼
|
||
┌──────────────────────── Go-сервер (gpio-monitor-server) ────────────────────────┐
|
||
│ PipeReader (internal/pipe) │
|
||
│ ├─► RingBuffer (internal/adapter) — RAM, для UI │
|
||
│ ├─► RotatingLogger → DataLogger — .bin-архив на диске │
|
||
│ ├─► HumanLogger — gpio_human.log │
|
||
│ ├─► EventLogger (алерты count > 10) — events.log / events_human.log │
|
||
│ └─► Monitor (watchdog тишины) │
|
||
│ │
|
||
│ Retention (internal/logger) — фоновая очистка .bin │
|
||
│ audio.Monitor (internal/audio) — уровень звука через arecord │
|
||
│ │
|
||
│ HTTP :8080 │
|
||
│ ├─ /api/* — JSON API (см. api.md) │
|
||
│ ├─ /api/cam — прокси MJPEG с go2rtc (:1984) │
|
||
│ └─ /* — встроенный веб-дашборд (go:embed) │
|
||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||
▲
|
||
│ HTTP-опрос (~1 раз/с)
|
||
Браузер (dashboard.html / logs.html, TypeScript — см. frontend.md)
|
||
```
|
||
|
||
Форматы данных на каждом участке — в [data-formats.md](data-formats.md), API — в [api.md](api.md).
|
||
|
||
## Захват GPIO и доставка данных
|
||
|
||
Захват выполняет C-программа `gpio-interrupt` (WiringPi), которая по прерыванию строба читает биты шины и пишет байты в stdout. **Её исходников в этом репозитории нет** — она живёт на устройстве в `/home/user/WiringPi/examples` (см. [tech-debt.md](tech-debt.md)).
|
||
|
||
Доставка построена на **systemd socket-activation** ([scripts/monitor-gpio.socket](../scripts/monitor-gpio.socket), [scripts/monitor-gpio.service](../scripts/monitor-gpio.service)):
|
||
|
||
1. `monitor-gpio.socket` создаёт FIFO `/tmp/gpio_pipe` (`ListenFIFO`, права 0666, `RemoveOnStop=yes`);
|
||
2. `monitor-gpio.service` запускает `gpio-interrupt` со `StandardOutput=socket` — stdout программы направляется прямо в FIFO;
|
||
3. Go-сервер открывает FIFO на чтение (флаг `-pipe`).
|
||
|
||
Такая схема развязывает жизненные циклы: капчер и сервер можно перезапускать независимо, pipe создаёт и убирает systemd.
|
||
|
||
## Компоненты Go-сервера
|
||
|
||
### PipeReader — [internal/pipe/reader.go](../internal/pipe/reader.go)
|
||
|
||
Единственный «производитель» данных. В отдельной горутине: следит за существованием FIFO (проверка каждые 2 с), открывает его, читает блоками до 4096 байт и раздаёт каждый байт потребителям (см. схему). Ведёт машину состояний `unknown → found / not_found / error / disconnected` и пишет события `PIPE_*` **только при смене состояния** — защита от спама в лог при флапающем соединении. При обрыве чтения переподключается через 1 с.
|
||
|
||
Дополнительно дедуплицирует human-лог (пишет при изменении `count`/`strength` или раз в `-human-log-interval` с) и алерты (`-alert-cooldown` на одинаковый `count`).
|
||
|
||
### RingBuffer — [internal/adapter/buffer.go](../internal/adapter/buffer.go)
|
||
|
||
Кольцевой буфер последних N байт (флаг `-buffer-size`, по умолчанию 10240) — источник данных для `/api/latest`, `/api/history`, `/api/health`. Потокобезопасен: данные под `sync.RWMutex`, счётчики (`lastWrite`, `totalBytes`) — атомики.
|
||
|
||
Скорость приёма считается **двумя механизмами**, оба пишут в одни и те же атомики `currentBPS`/`currentBPSBits`: периодический пересчёт по дельте счётчика (не чаще раза в 250 мс) и скользящее окно 1 с по временным меткам последних байт. Второй перетирает первый; есть и публичный `GetWindowSpeed()`, который API не использует. Это избыточность — кандидат на упрощение ([tech-debt.md](tech-debt.md)).
|
||
|
||
### Логгеры — [internal/logger/](../internal/logger/)
|
||
|
||
- **RotatingLogger** (`rotation.go`) — фасад над DataLogger: держит файл текущего часа, по таймеру (`-rotation-check-interval`) проверяет смену часа и переоткрывает файл.
|
||
- **DataLogger** (`data_logger.go`) — бинарная запись сэмплов (9 байт) с буфером 64 КБ, flush раз в 1 с или при заполнении, `fsync` после каждого сброса.
|
||
- **HumanLogger** (`human_logger.go`) — построчная запись в `gpio_human.log` с немедленным `Sync`.
|
||
- **EventLogger** (`event_logger.go`) — системные события, двойная запись: JSON (`events.log`) + текст (`events_human.log`).
|
||
- **Monitor** (`monitor.go`) — watchdog: PipeReader отмечает каждую запись (`RecordWrite`), фоновый цикл раз в `-watchdog-interval` минут сравнивает тишину с порогами и пишет `ТИШИНА_5МИН`/`ТИШИНА_10МИН`.
|
||
- **Retention** (`retention.go`) — фоновая FIFO-очистка `.bin`-файлов по возрасту и суммарному размеру, минимум 2 файла всегда сохраняются; устойчива к скачку системных часов (подробности в [data-formats.md](data-formats.md#retention-очистка)).
|
||
|
||
### Аудиомонитор — [internal/audio/monitor.go](../internal/audio/monitor.go)
|
||
|
||
Запускает `arecord` (ALSA: raw, 1 канал, 16 кГц, S16_LE), автоматически выбирая USB-аудиоустройство по выводу `arecord -l` (fallback — первое устройство захвата, затем `default`). Читает PCM блоками 3200 байт (100 мс), считает RMS и публикует уровень 0–100 (`RMS/32768 × 100 × 4`, с отсечкой). При падении `arecord` перезапускает захват через 5 с. Уровень отдаётся в `/api/health` полем `sound_level`. Недоступность микрофона не мешает запуску сервера.
|
||
|
||
### HTTP-слой — [cmd/server/main.go](../cmd/server/main.go)
|
||
|
||
Стандартный `net/http` без внешних роутеров: `/api/` обслуживает switch по пути (8 эндпоинтов, все под CORS-обёрткой, кроме `/api/cam`), остальное — `http.FileServer` поверх встроенных веб-файлов. Прокси камеры ретранслирует MJPEG-поток из go2rtc с flush после каждого чанка 32 КБ.
|
||
|
||
## Порядок инициализации
|
||
|
||
Последовательность в `main()` (важна: каждый следующий компонент получает уже готовые предыдущие):
|
||
|
||
1. Разбор CLI-флагов → `logger.Config`;
|
||
2. **EventLogger** (без него сервер не стартует) → событие `СЕРВЕР_ЗАПУЩЕН`;
|
||
3. **HumanLogger**;
|
||
4. **Monitor** (watchdog) — сразу запускает фоновый цикл;
|
||
5. **RotatingLogger** — открывает файл текущего часа, запускает цикл ротации;
|
||
6. **Retention** — если `-retention-hours > 0` или `-retention-mb > 0`; первая очистка через 1 мин;
|
||
7. **RingBuffer**;
|
||
8. **audio.Monitor** — ошибка запуска не фатальна;
|
||
9. **PipeReader.Start** — горутина чтения FIFO;
|
||
10. Регистрация HTTP-хендлеров и `ListenAndServe` (блокируется навсегда).
|
||
|
||
Ошибка инициализации любого логгера (шаги 2–5) — `log.Fatal`, сервер не запускается.
|
||
|
||
## Конкурентность
|
||
|
||
| Горутина | Кто запускает | Что делает |
|
||
|---|---|---|
|
||
| Чтение FIFO | `PipeReader.Start` | Цикл открытия/чтения пайпа, раздача байт |
|
||
| Flush-цикл DataLogger | `NewDataLogger` (пересоздаётся при ротации) | Сброс буфера раз в 1 с |
|
||
| Цикл ротации | `NewRotatingLogger` | Проверка смены часа |
|
||
| Watchdog | `NewMonitor` | Проверка тишины |
|
||
| Цикл retention | `Retention.StartWithInterval` | Периодическая очистка + разовая через 1 мин |
|
||
| Чтение PCM | `audio.Monitor.startCapture` | RMS-расчёт уровня звука |
|
||
| `cmd.Wait` arecord | `startCapture` | Сбор зомби-процесса |
|
||
| HTTP-хендлеры | `net/http` | По горутине на запрос |
|
||
|
||
Синхронизация: у каждого компонента свой мьютекс (`RingBuffer.mu`+`speedMu`+`recentMu`, `DataLogger.mu`, `RotatingLogger.mu`, `EventLogger.mu`, `HumanLogger.mu`, `Monitor.mu`, `audio.Monitor.mu`); межкомпонентных блокировок нет — данные передаются вызовами методов, владение файлами не разделяется. Завершение — через `context` (PipeReader, audio) и каналы (`DataLogger.closeCh`, `Retention.stopCh`).
|
||
|
||
## Встраивание фронтенда
|
||
|
||
```go
|
||
//go:embed web/dist web/css web/fonts web/*.html web/*.png
|
||
var webFiles embed.FS
|
||
```
|
||
|
||
Скомпилированный дашборд вшивается в бинарник на этапе `go build` — сервер разворачивается одним файлом. Следствие: **`go build` падает, если `web/dist` не существует**, поэтому фронтенд всегда собирается первым (`make build` = `build-frontend` + `build-backend`). Подробнее — [development.md](development.md) и [frontend.md](frontend.md).
|
||
|
||
## Топологии развёртывания
|
||
|
||
Режим определяется наличием каталога `/opt/gpio-monitoring` ([internal/logger/paths.go](../internal/logger/paths.go)):
|
||
|
||
| | Пакетный режим (deb) | Режим разработки |
|
||
|---|---|---|
|
||
| Бинарник | `/usr/bin/gpio-monitor-server` | `./build/gpio-monitor-server` или `go run` |
|
||
| Запуск | systemd `gpio-monitor-server.service`, пользователь `gpio-monitor` | вручную |
|
||
| Логи | `/var/log/gpio-monitoring` | `~/.local/share/gpio-monitoring/logs` |
|
||
| Веб-файлы | вшиты в бинарник (копия в `/opt/gpio-monitoring/web`) | вшиты в бинарник |
|
||
| Источник данных | реальный `gpio-interrupt` через systemd socket | эмулятор `scripts/emulator.py` + `mkfifo` |
|
||
|
||
Внешние сервисы в обоих режимах: **go2rtc** (порт 1984, конфиг [scripts/go2rtc.yaml](../scripts/go2rtc.yaml)) для MJPEG-камеры и **ALSA/arecord** для микрофона. Установка и настройка — в [operations.md](operations.md).
|
||
|
||
Подпроект [SDR/](../SDR/) (OFDM-радиолинк на PlutoSDR) — автономный C-проект со своей сборкой и [README](../SDR/README.md); с Go-сервером не связан: канал HackRF/PlutoSDR используется как альтернативный транспорт байтов GPIO-шины (см. [scripts/emulatohackrf.sh](../scripts/emulatohackrf.sh)).
|