# Архитектура системы Описание устройства 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)).