14 KiB
Архитектура системы
Описание устройства GPIO Monitor: компоненты, поток данных, конкурентность, топологии развёртывания.
Аудитория: разработчики.
Содержание
- Общая схема
- Захват GPIO и доставка данных
- Компоненты 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, API — в api.md.
Захват GPIO и доставка данных
Захват выполняет C-программа gpio-interrupt (WiringPi), которая по прерыванию строба читает биты шины и пишет байты в stdout. Её исходников в этом репозитории нет — она живёт на устройстве в /home/user/WiringPi/examples (см. tech-debt.md).
Доставка построена на systemd socket-activation (scripts/monitor-gpio.socket, scripts/monitor-gpio.service):
monitor-gpio.socketсоздаёт FIFO/tmp/gpio_pipe(ListenFIFO, права 0666,RemoveOnStop=yes);monitor-gpio.serviceзапускаетgpio-interruptсоStandardOutput=socket— stdout программы направляется прямо в FIFO;- Go-сервер открывает FIFO на чтение (флаг
-pipe).
Такая схема развязывает жизненные циклы: капчер и сервер можно перезапускать независимо, pipe создаёт и убирает systemd.
Компоненты Go-сервера
PipeReader — 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
Кольцевой буфер последних N байт (флаг -buffer-size, по умолчанию 10240) — источник данных для /api/latest, /api/history, /api/health. Потокобезопасен: данные под sync.RWMutex, счётчики (lastWrite, totalBytes) — атомики.
Скорость приёма считается двумя механизмами, оба пишут в одни и те же атомики currentBPS/currentBPSBits: периодический пересчёт по дельте счётчика (не чаще раза в 250 мс) и скользящее окно 1 с по временным меткам последних байт. Второй перетирает первый; есть и публичный GetWindowSpeed(), который API не использует. Это избыточность — кандидат на упрощение (tech-debt.md).
Логгеры — 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).
Аудиомонитор — 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
Стандартный net/http без внешних роутеров: /api/ обслуживает switch по пути (8 эндпоинтов, все под CORS-обёрткой, кроме /api/cam), остальное — http.FileServer поверх встроенных веб-файлов. Прокси камеры ретранслирует MJPEG-поток из go2rtc с flush после каждого чанка 32 КБ.
Порядок инициализации
Последовательность в main() (важна: каждый следующий компонент получает уже готовые предыдущие):
- Разбор CLI-флагов →
logger.Config; - EventLogger (без него сервер не стартует) → событие
СЕРВЕР_ЗАПУЩЕН; - HumanLogger;
- Monitor (watchdog) — сразу запускает фоновый цикл;
- RotatingLogger — открывает файл текущего часа, запускает цикл ротации;
- Retention — если
-retention-hours > 0или-retention-mb > 0; первая очистка через 1 мин; - RingBuffer;
- audio.Monitor — ошибка запуска не фатальна;
- PipeReader.Start — горутина чтения FIFO;
- Регистрация 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: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 и frontend.md.
Топологии развёртывания
Режим определяется наличием каталога /opt/gpio-monitoring (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) для MJPEG-камеры и ALSA/arecord для микрофона. Установка и настройка — в operations.md.
Подпроект SDR/ (OFDM-радиолинк на PlutoSDR) — автономный C-проект со своей сборкой и README; с Go-сервером не связан: канал HackRF/PlutoSDR используется как альтернативный транспорт байтов GPIO-шины (см. scripts/emulatohackrf.sh).