Files
go-service/docs/architecture.md
2026-07-17 15:57:05 +03:00

151 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Архитектура системы
Описание устройства 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 и публикует уровень 0100 (`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` (блокируется навсегда).
Ошибка инициализации любого логгера (шаги 25) — `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)).