Доработка документации

This commit is contained in:
Maxim
2026-07-17 15:57:05 +03:00
parent 5b403d7ee4
commit 9fa3d172a8
22 changed files with 1371 additions and 481 deletions

251
docs/api.md Normal file
View File

@@ -0,0 +1,251 @@
# Спецификация HTTP API
Документ описывает HTTP API сервера `gpio-monitor-server`. Источник истины — [cmd/server/main.go](../cmd/server/main.go).
**Аудитория:** разработчики и интеграторы.
## Содержание
- [Общие сведения](#общие-сведения)
- [GET /api/health](#get-apihealth)
- [GET /api/latest](#get-apilatest)
- [GET /api/history](#get-apihistory)
- [GET /api/stream](#get-apistream)
- [GET /api/cam](#get-apicam)
- [GET /api/log/files](#get-apilogfiles)
- [GET /api/log/data](#get-apilogdata)
- [GET /api/log/events](#get-apilogevents)
- [Известные особенности](#известные-особенности)
## Общие сведения
- **Адрес:** порт задаётся флагом `-port` (по умолчанию `:8080`).
- **Метод:** все эндпоинты — только `GET` (плюс `OPTIONS` для CORS preflight).
- **Формат ответов:** JSON (`Content-Type: application/json`), кроме `/api/cam` (MJPEG-поток).
- **CORS:** на всех эндпоинтах, кроме `/api/cam`, стоит обёртка `cors()`: `Access-Control-Allow-Origin: *`, `Access-Control-Allow-Methods: GET, OPTIONS`. Запрос `OPTIONS` возвращает `200` без тела. `/api/cam` выставляет `Access-Control-Allow-Origin: *` самостоятельно, но `OPTIONS` не обрабатывает.
- **Ошибки:** возвращаются как `text/plain` с русскоязычным сообщением и кодом `400`/`404`/`500`/`503`. Неизвестный путь под `/api/``404 not found`.
- **Статика:** все пути вне `/api/` обслуживаются встроенным веб-дашбордом (`go:embed`, см. [architecture.md](architecture.md)).
Расшифровка полей `value`/`amplitude`/`signal` — в [data-formats.md](data-formats.md).
## GET /api/health
Статус сервера, соединения с pipe и сводная статистика. Основной эндпоинт для опроса дашбордом.
Пример ответа:
```json
{
"status": "На связи",
"uptime_sec": 12345.67,
"last_data_ms": 42,
"pipe_alive": true,
"has_data": true,
"latest": 66,
"stats": {
"size": 10240,
"filled": 10240,
"last_write": "2026-07-17T12:34:56.789012345+03:00",
"total_bytes": 1234567,
"total_bits": 9876536,
"bytes_per_sec": 100.5,
"bits_per_sec": 804.0
},
"server_time": "17.07.2026 12:34",
"sound_level": 27
}
```
| Поле | Тип | Описание |
|---|---|---|
| `status` | string | `"На связи"`, если последняя запись в буфер была менее 15 секунд назад, иначе `"Нет связи"` |
| `uptime_sec` | float | Время работы сервера в секундах |
| `last_data_ms` | int | Миллисекунд с момента последней записи данных |
| `pipe_alive` | bool | Была ли запись за последние 3 секунды |
| `has_data` | bool | Есть ли хоть один байт в кольцевом буфере |
| `latest` | int (0255) | Последний сырой байт GPIO; `0`, если данных ещё не было |
| `stats.size` | int | Ёмкость кольцевого буфера (флаг `-buffer-size`) |
| `stats.filled` | int | Сколько ячеек буфера заполнено |
| `stats.last_write` | string (RFC 3339) | Время последней записи |
| `stats.total_bytes` / `total_bits` | int | Всего принято байт/бит с момента запуска |
| `stats.bytes_per_sec` / `bits_per_sec` | float | Текущая скорость приёма |
| `server_time` | string | Серверное время в формате `ДД.ММ.ГГГГ ЧЧ:ММ` (два пробела между датой и временем) |
| `sound_level` | int (0100) | Уровень звука с микрофона; `0`, если аудиомонитор не запущен |
## GET /api/latest
Последний байт и короткая история.
Ответ при наличии данных:
```json
{
"latest": 66,
"history": [64, 65, 66, 66, 65, 64, 66, 67, 66, 66]
}
```
- `history` — до 10 последних байт, **от старых к новым** (последний элемент = `latest`).
- Если буфер пуст, возвращается `200` с телом `{"error": "no data"}` (не HTTP-ошибка).
## GET /api/history
История для графика.
```json
{
"bytes": [64, 65, 66, "...", 66]
}
```
- `bytes` — массив целых (0255), до **300** последних байт, от старых к новым. Если данных меньше — вернётся сколько есть.
## GET /api/stream
Статус видеопотока камеры.
```json
{
"cam": "/api/cam",
"available": true,
"source": "http://192.168.1.10:1984/api/stream.mjpeg?src=cam_mjpeg"
}
```
| Поле | Описание |
|---|---|
| `cam` | Путь к прокси-эндпоинту MJPEG (всегда `/api/cam`) |
| `available` | Доступен ли поток: сервер делает запрос `http://localhost:1984/api/streams?src=cam_mjpeg` и проверяет код 200 |
| `source` | Прямой URL потока go2rtc, построенный из хоста запроса (`r.Host`) и порта 1984 |
⚠️ Проверка `available` захардкожена на `localhost:1984` и **не учитывает флаг `-camera-url`** (main.go:460). См. [tech-debt.md](tech-debt.md).
## GET /api/cam
Прокси MJPEG-потока камеры. Сервер запрашивает URL из флага `-camera-url` (по умолчанию `http://127.0.0.1:1984/api/stream.mjpeg?src=cam_mjpeg`) и ретранслирует поток клиенту с исходными заголовками, сбрасывая буфер после каждого чтения (chunk 32 КБ).
Ошибки:
| Код | Тело | Когда |
|---|---|---|
| `503` | `камера недоступна` | go2rtc не отвечает |
| `500` | `поток не поддерживается` | ResponseWriter не поддерживает Flush |
## GET /api/log/files
Список бинарных лог-файлов `gpio-*.bin` из каталога данных (см. [пути хранения](data-formats.md#пути-хранения)).
```json
[
{
"name": "gpio-2026-07-17-12.bin",
"path": "/var/log/gpio-monitoring/data/gpio-2026-07-17-12.bin",
"size": 34567,
"mod_time": "2026-07-17T12:59:59+03:00",
"time": "17.07.2026 12:00",
"date": "2026-07-17",
"hour": 12,
"is_active": false
}
]
```
- Массив отсортирован по `mod_time`, новые первыми.
- `time`/`date`/`hour` вычисляются из имени файла; если имя не соответствует шаблону `gpio-YYYY-MM-DD-HH.bin` — из `mod_time`.
- `is_active` в текущей реализации всегда `false` (зарезервировано).
- ⚠️ Если файлов нет, возвращается `null`, а не `[]` (сериализация nil-слайса).
## GET /api/log/data
Чтение сэмплов из конкретного `.bin`-файла с пагинацией.
**Параметры запроса:**
| Параметр | Обязательный | По умолчанию | Описание |
|---|---|---|---|
| `file` | да | — | Имя файла (например `gpio-2026-07-17-12.bin`); путь очищается через `filepath.Base` — path traversal невозможен |
| `page` | нет | 1 | Номер страницы (значения < 1 игнорируются; больше максимума прижимается к последней) |
| `page_size` | нет | 100 | Размер страницы |
Пример: `GET /api/log/data?file=gpio-2026-07-17-12.bin&page=1&page_size=50`
```json
{
"filename": "gpio-2026-07-17-12.bin",
"total": 3600,
"page": 1,
"page_size": 50,
"total_pages": 72,
"has_previous": false,
"has_next": true,
"start_index": 3551,
"end_index": 3600,
"samples": [
{
"ts": 1789034096123456,
"time": "17.07.2026 12:34:56",
"value": 66,
"amplitude": 1,
"signal": 2,
"strength_name": "Средний"
}
],
"stats": {
"total_points": 3600,
"amplitude_over_0": 120,
"signal_over_10": 15
},
"order": "newest_first"
}
```
- **Порядок `newest_first`:** страница 1 содержит самые новые сэмплы; внутри страницы сэмплы также идут от новых к старым.
- `ts` метка времени в **микросекундах** Unix; `time` она же в формате `ДД.ММ.ГГГГ ЧЧ:ММ:СС`.
- `value` сырой байт; `amplitude` биты 67 (03); `signal` биты 05 (063); `strength_name` текстовое имя уровня (см. [data-formats.md](data-formats.md#байт-gpio)).
- `stats` считается по **всему файлу**, а не по странице: `amplitude_over_0` сэмплы с амплитудой > 0, `signal_over_10`с сигналом > 10.
- `start_index`/`end_index` — 1-based диапазон в хронологическом порядке файла.
- Для пустого файла возвращается `total: 0` и пустой массив `samples`.
Ошибки: `400 отсутствует параметр file`, `404 файл не найден`, `500` при ошибке чтения.
⚠️ Файл целиком читается в память до пагинации — учитывайте при больших `.bin`-файлах ([tech-debt.md](tech-debt.md)).
## GET /api/log/events
Системные события из `events_human.log` с пагинацией.
**Параметры:** `page` (по умолчанию 1), `page_size` (по умолчанию 100) — семантика как у `/api/log/data`.
```json
{
"events": [
{ "time": "2026-07-17 12:00:00.001", "event": "ПЛАНОВАЯ_РОТАЦИЯ" },
{ "time": "2026-07-17 11:59:12.512", "event": "ОБНАРУЖЕНО_12_ОБЪЕКТОВ_СИЛА_2" }
],
"total": 254,
"page": 1,
"page_size": 100,
"total_pages": 3,
"has_previous": false,
"has_next": true,
"returned": 100,
"start_index": 155,
"end_index": 254,
"order": "newest_first"
}
```
- Парсятся только строки вида `[время] EVENT: имя`; прочие строки учитываются в `total`, но не попадают в `events` (поэтому `returned` может быть меньше размера страницы).
- Словарь имён событий — в [data-formats.md](data-formats.md#события-системы).
Ошибки: `500`, если файл событий недоступен.
## Известные особенности
Зафиксированы также в [tech-debt.md](tech-debt.md):
1. `/api/stream` проверяет доступность камеры по захардкоженному `localhost:1984`, игнорируя `-camera-url`.
2. В `main.go` есть неиспользуемая функция `handleCamProxy` (двойник `handleCamProxyWithURL` с захардкоженным URL) — роутер её не вызывает.
3. `/api/latest` при пустом буфере возвращает `200` с `{"error": "no data"}`, а не код ошибки.
4. `/api/log/files` при отсутствии файлов возвращает `null` вместо пустого массива.
5. CORS открыт для всех источников (`*`) — рассчитано на доверенную локальную сеть.

150
docs/architecture.md Normal file
View File

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

169
docs/data-formats.md Normal file
View File

@@ -0,0 +1,169 @@
# Форматы данных и логов
Документ описывает все форматы данных системы: байт GPIO, протокол FIFO-пайпа, бинарные и текстовые логи, пути хранения и правила очистки.
**Аудитория:** разработчики и интеграторы.
## Содержание
- [Байт GPIO](#байт-gpio)
- [Протокол FIFO-пайпа](#протокол-fifo-пайпа)
- [Бинарные логи (.bin)](#бинарные-логи-bin)
- [Человекочитаемый GPIO-лог](#человекочитаемый-gpio-лог)
- [События системы](#события-системы)
- [Пути хранения](#пути-хранения)
- [Ротация](#ротация)
- [Retention (очистка)](#retention-очистка)
## Байт GPIO
Единица данных системы — один байт, снятый с 8-битной GPIO-шины. Разбор — в [internal/logger/parser.go](../internal/logger/parser.go) (`ParseGPIO`):
```
бит: 7 6 | 5 4 3 2 1 0
└─┬─┘ └────┬─────┘
strength count
(03) (063)
```
| Поле | Биты | Диапазон | Смысл |
|---|---|---|---|
| `count` (в API — `signal`) | 05 | 063 | Количество обнаружений |
| `strength` (в API — `amplitude`) | 67 | 03 | Сила сигнала |
Текстовые имена уровней силы (`GetStrengthName`):
| Значение | Имя |
|---|---|
| 0 | Слабый |
| 1 | Средний |
| 2 | Сильный |
| 3 | Максимальный |
**Порог тревоги:** при `count > 10` (`IsHighCount`) генерируется событие `ОБНАРУЖЕНО_<count>_ОБЪЕКТОВ_СИЛА_<strength>` и строка «ВНИМАНИЕ» в human-логе (анти-спам: не чаще одного алерта на одинаковый `count` за `-alert-cooldown` секунд).
Пример: байт `0x8C` = `1000 1100` → strength = 2 («Сильный»), count = 12 → сработает алерт.
## Протокол FIFO-пайпа
Связь C-программы захвата с Go-сервером — именованный канал (по умолчанию `/tmp/gpio_pipe`, флаг `-pipe`).
- Поток **сырых байт без кадрирования**: каждый байт — одно измерение в формате [байта GPIO](#байт-gpio). Никаких заголовков, разделителей и контрольных сумм.
- Сервер читает блоками до 4096 байт ([internal/pipe/reader.go](../internal/pipe/reader.go)).
- Метка времени присваивается **на стороне сервера** в момент чтения (`time.Now().UnixMicro()`).
- При отсутствии/обрыве пайпа сервер переподключается сам: проверка существования каждые 2 с, повторное открытие через 1 с; смены состояния фиксируются событиями `PIPE_*` (см. [словарь событий](#события-системы)).
## Бинарные логи (.bin)
Основной архив данных. Запись — [internal/logger/data_logger.go](../internal/logger/data_logger.go), чтение — `HandleLogData` в [cmd/server/main.go](../cmd/server/main.go).
**Формат файла:** конкатенация записей по 9 байт, без заголовка и футера:
```
┌────────────────────────────────┬───────────┐
│ timestamp: uint64 LE, 8 байт │ value: 1б
└────────────────────────────────┴───────────┘
```
- `timestamp` — микросекунды Unix (`UnixMicro`), little-endian;
- `value` — сырой [байт GPIO](#байт-gpio).
**Буферизация записи:** сэмплы копятся в буфере 64 КБ и сбрасываются на диск раз в 1 секунду либо при заполнении буфера; после каждого сброса вызывается `fsync` (durability при отключении питания).
**Именование:** `gpio-YYYY-MM-DD-HH.bin`, время **локальное**, один файл на час (см. [Ротация](#ротация)). Пример: `gpio-2026-07-17-12.bin` — данные за 12:0012:59 17 июля 2026.
**Чтение на другой платформе:** каждая запись — `<Q` + `B` в терминах Python `struct`:
```python
import struct
with open("gpio-2026-07-17-12.bin", "rb") as f:
while chunk := f.read(9):
if len(chunk) < 9:
break
ts_us, value = struct.unpack("<QB", chunk)
strength, count = (value >> 6) & 0x3, value & 0x3F
```
## Человекочитаемый GPIO-лог
Файл `gpio_human.log` (см. [пути](#пути-хранения)), пишет [internal/pipe/reader.go](../internal/pipe/reader.go) через `HumanLogger`.
Обычная строка (пишется при изменении `count`/`strength` либо раз в `-human-log-interval` секунд):
```
[2026-07-17 12:34:56.789] Обнаружение: 12 объектов | Сила: Сильный (уровень 2) | Сырое: 0x8C (140)
```
Строка тревоги (при `count > 10`, с учётом анти-спама):
```
[2026-07-17 12:34:56.789] ВНИМАНИЕ: Обнаружено превышение! 12 объектов | Сила: Сильный (уровень 2)
```
## События системы
События пишутся [internal/logger/event_logger.go](../internal/logger/event_logger.go) **сразу в два файла**:
1. `events.log` — JSON-строки (одна на событие):
```json
{"ts": 1789034096, "time": "2026-07-17 12:34:56", "event": "ПЛАНОВАЯ_РОТАЦИЯ"}
```
2. `events_human.log` — текст (этот файл читает `/api/log/events`):
```
[2026-07-17 12:34:56.789] EVENT: ПЛАНОВАЯ_РОТАЦИЯ
```
**Словарь событий** (по вызовам `Event(...)` в коде):
| Событие | Источник | Когда |
|---|---|---|
| `СЕРВЕР_ЗАПУЩЕН` | main.go | Старт сервера |
| `HUMAN_ЛОГЕРОТОВ` | main.go | Инициализация HumanLogger |
| `DATA_ЛОГЕРОТОВ` | main.go | Инициализация RotatingLogger |
| `ОШИБКАНИЦИАЛИЗАЦИИ_DATA_ЛОГЕРА` | main.go | Сбой инициализации (сервер завершается) |
| `RETENTION_ГОТОВ` | main.go | Запуск очистки логов |
| `ПЛАНОВАЯ_РОТАЦИЯ` | rotation.go | Открыт новый почасовой файл |
| `ПЛАНОВАЯ_РОТАЦИЯ_НЕУДАЧНА` | rotation.go | Не удалось открыть новый файл |
| `PIPE_НЕ_НАЙДЕН` | reader.go | FIFO-файл отсутствует |
| `ОШИБКА_ОТКРЫТИЯ_PIPE` | reader.go | FIFO существует, но не открывается |
| `PIPE_ПОДКЛЮЧЕН` | reader.go | Пайп открыт / связь восстановлена |
| `PIPE_ОТКЛЮЧЕН` | reader.go | Ошибка чтения из пайпа |
| `ТИШИНА_5МИН` | monitor.go | Нет данных дольше порога `-silence-5min` |
| `ТИШИНА_10МИН` | monitor.go | Нет данных дольше порога `-silence-10min` |
| `ОБНАРУЖЕНО_<N>_ОБЪЕКТОВ_СИЛА_<M>` | reader.go | Алерт превышения (`count > 10`) |
| `ОЧИСТКАОГОВ_УДАЛЕНО_<N>_ФАЙЛОВ_<M>_MB` | retention.go | Retention удалил файлы |
| `ОШИБКАОЛУЧЕНИЯ_ДИРЕКТОРИИ` | retention.go | Retention не смог получить каталог данных |
События смены состояния пайпа пишутся **только при изменении** состояния (дедупликация в `PipeReader`), тишина — при каждой проверке watchdog (раз в `-watchdog-interval` минут), пока тишина длится.
## Пути хранения
Все пути вычисляет [internal/logger/paths.go](../internal/logger/paths.go). Режим определяется по существованию каталога `/opt/gpio-monitoring`: есть — «пакетный» режим (deb-установка), нет — режим разработки (XDG).
| Что | Пакетный режим | Режим разработки |
|---|---|---|
| Данные приложения | `/var/lib/gpio-monitoring` | `~/.local/share/gpio-monitoring` |
| Каталог логов | `/var/log/gpio-monitoring` | `~/.local/share/gpio-monitoring/logs` |
| Бинарные `.bin` | `/var/log/gpio-monitoring/data/` | `~/.local/share/gpio-monitoring/logs/data/` |
| События (JSON) | `.../events.log` | `.../logs/events.log` |
| События (текст) | `.../events_human.log` | `.../logs/events_human.log` |
| GPIO human-лог | `.../gpio_human.log` | `.../logs/gpio_human.log` |
## Ротация
[internal/logger/rotation.go](../internal/logger/rotation.go): `RotatingLogger` держит текущий `DataLogger` и раз в `-rotation-check-interval` минут (по умолчанию 1) сверяет текущий час с часом открытого файла. При смене часа старый файл закрывается (с финальным flush) и открывается новый `gpio-YYYY-MM-DD-HH.bin`; пишется событие `ПЛАНОВАЯ_РОТАЦИЯ`.
## Retention (очистка)
[internal/logger/retention.go](../internal/logger/retention.go): фоновая FIFO-очистка каталога `data/`. Проверка раз в `-retention-interval` минут (по умолчанию 15), первая — через 1 минуту после старта.
Алгоритм `Cleanup()`:
1. **По возрасту** (`-retention-hours`, 0 = отключено): файлы старше порога удаляются с самого старого края. Файл считается устаревшим, только если он старше порога **и по времени из имени, и по ModTime** — защита от скачка системных часов после NTP-синхронизации на устройстве без RTC.
2. **По размеру** (`-retention-mb`, 0 = отключено): пока суммарный размер `.bin`-файлов превышает лимит, удаляется самый старый файл.
3. В любом случае сохраняются минимум **2 самых свежих файла** (`minKeepFiles`) — защита от полного стирания каталога.
Если имя файла не соответствует шаблону `gpio-YYYY-MM-DD-HH.bin`, вместо времени из имени используется ModTime. Поведение закреплено тестами [internal/logger/retention_test.go](../internal/logger/retention_test.go).

115
docs/development.md Normal file
View File

@@ -0,0 +1,115 @@
# Руководство разработчика
Сборка, запуск без железа, тесты и соглашения проекта.
**Аудитория:** разработчики.
## Содержание
- [Окружение](#окружение)
- [Порядок сборки](#порядок-сборки)
- [Цели Makefile](#цели-makefile)
- [Сборка deb-пакета](#сборка-deb-пакета)
- [Запуск без железа (эмуляция)](#запуск-без-железа-эмуляция)
- [Тесты](#тесты)
- [Соглашения](#соглашения)
## Окружение
- **Go 1.21+** — модуль `gpio-monitor`, **внешних Go-зависимостей нет** (только стандартная библиотека, см. [go.mod](../go.mod));
- **Node.js 20+ и npm** — только для компиляции TypeScript (единственная dev-зависимость — `typescript`);
- `make`, `git`; для deb-пакета — `dpkg-deb`; опционально `golangci-lint` для `make lint`.
Подготовка после клонирования:
```bash
make init # проверит Node.js, скачает Go-модули и npm-пакеты
```
## Порядок сборки
**Критичный нюанс:** сервер встраивает веб-файлы через `go:embed web/dist ...` ([cmd/server/main.go:28](../cmd/server/main.go)). Каталог `dist/` генерируется компилятором TypeScript и в git не хранится, поэтому **на чистом checkout `go build` упадёт с ошибкой embed**. Всегда собирайте фронтенд первым:
```bash
make build # = build-frontend (tsc) + build-backend (go build)
```
По умолчанию бинарник собирается кросс-компиляцией под **linux/arm64** (Raspberry Pi) в `build/gpio-monitor-server`. Для локальной платформы используйте `make build-amd64` / `build-mac` / `build-windows` или `make dev`.
Есть также исторический скрипт [build.sh](../build.sh) — делает то же, что `make build`, но с захардкоженным путём проекта `~/temp/golang`; предпочитайте Makefile.
## Цели Makefile
Справка встроена: `make help`. Основные цели ([Makefile](../Makefile)):
| Цель | Что делает |
|---|---|
| `init` | Проверка Node.js/npm + установка всех зависимостей |
| `build` | Полная сборка: фронтенд + бекенд (GOOS/GOARCH переопределяемы) |
| `build-frontend` | `npm install` (при отсутствии node_modules) + `npm run build` (tsc) |
| `build-backend` | `go build -ldflags="-s -w"` в `build/` |
| `run` | Сборка + запуск бинарника |
| `dev` | `go run ./cmd/server -port :8080` без оптимизаций (dist уже должен существовать) |
| `test` | `go test -v ./...` |
| `fmt` | `go fmt ./...` |
| `lint` | `golangci-lint run ./...` (если установлен) |
| `clean` | Удаляет `build/`, `web/dist`, `web/node_modules`, временные deb-файлы |
| `build-arm64` / `build-amd64` / `build-mac` / `build-windows` / `build-all` | Кросс-сборки |
| `install` / `uninstall` | Копирование бинарника в `/usr/local/bin` |
| `deb` / `deb-info` / `deb-contents` / `deb-clean` / `version` | Работа с deb-пакетом (ниже) |
> Цели `docker-build`/`docker-run` объявлены, но Dockerfile в репозитории отсутствует — они нерабочие ([tech-debt.md](tech-debt.md#сборка-и-инфраструктура)).
## Сборка deb-пакета
```bash
make deb # от обычного пользователя, без sudo
make version # показать вычисленную версию и имя файла
make deb-info # информация о собранном пакете (dpkg-deb -I)
make deb-contents # список файлов пакета (dpkg-deb -c)
make deb-clean # очистка временных файлов сборки пакета
```
**Версионирование через git:** версия пакета = `<база>-build<N>`, где база берётся из `Version:` в [debian/control](../debian/control) (сейчас `1.0.0`), а `N` = `git rev-list --count HEAD` (число коммитов). Итоговый файл: `build/gpio-monitor-server_1.0.0-build<N>_arm64.deb`. Каждый коммит автоматически увеличивает номер сборки.
`make deb` выполняет `clean → build-frontend → build-backend → prepare-debian → dpkg-deb --build`. В пакет попадают: бинарник, веб-файлы (копия для справки — сервер использует встроенные), unit systemd, конфиг, утилита `gpio-logs`, maintainer-скрипты из [debian/](../debian/). Что происходит при установке — в [operations.md](operations.md#установка-из-deb-пакета).
## Запуск без железа (эмуляция)
FIFO и поток данных можно смоделировать на любой Linux-машине:
```bash
mkfifo /tmp/gpio_pipe
python3 scripts/emulator.py scripts/array.txt /tmp/gpio_pipe # генератор данных
make dev # сервер (в другом терминале)
```
Вспомогательные скрипты [scripts/](../scripts/):
| Скрипт | Назначение |
|---|---|
| `emulator.py` | Эмулятор капчера: читает массив байт из файла (формат `[0xE0, 0x01, ...]`) и пишет их в pipe/stdout с заданной задержкой, с переподключением и повтором |
| `array.txt` | Пример массива данных для эмулятора |
| `imi_wire.py` | Имитатор параллельной шины на **реальных GPIO** (gpiod, Raspberry Pi 4): выставляет байт на пины данных и дёргает строб WR — для теста настоящего `gpio-interrupt` |
| `emulatohackrf.sh` | Связка эмулятора с передатчиком HackRF (`hackrf-frame64-tx`): передача байтов GPIO по радиоканалу |
| `vu_meter.py` | Консольный VU-метр микрофона (sounddevice/numpy) — независимая проверка аудиотракта |
| `view_logs.sh` | Сводная статистика dev-логов (`~/.local/share/gpio-monitoring/logs`) |
Пути внутри `emulatohackrf.sh` и systemd-юнитов захардкожены под конкретное устройство (`/home/user/...`) — правьте под своё окружение.
## Тесты
```bash
make test # go test -v ./...
```
Сейчас тестами покрыт только retention: [internal/logger/retention_test.go](../internal/logger/retention_test.go) (очистка по возрасту/размеру, minKeepFiles, устойчивость к скачку часов и битым именам). Остальные пакеты тестов не имеют — см. [tech-debt.md](tech-debt.md#тесты-и-ci). CI в репозитории нет — прогоняйте `make test` и `go vet ./...` перед коммитом вручную.
## Соглашения
- **Язык** — русский: комментарии, логи, сообщения об ошибках, документация.
- **Конфигурация** — только CLI-флаги (без env-переменных и конфиг-файлов); новые параметры добавляются флагом в `main()` и полем в `logger.Config`.
- **Зависимости** — Go-код держится на стандартной библиотеке; прежде чем добавить стороннюю зависимость, убедитесь, что она действительно необходима.
- **Структура**: `cmd/server` — точка входа и HTTP-слой; `internal/adapter` — буфер; `internal/pipe` — приём данных; `internal/logger` — хранение/события; `internal/audio` — микрофон. Фронтенд — [frontend.md](frontend.md).
- **Целевая платформа** — linux/arm64; сборка и на других платформах должна оставаться рабочей (кросс-цели Makefile).
- Форматы данных и API описаны в [data-formats.md](data-formats.md) и [api.md](api.md) — при изменении кода обновляйте эти документы.

79
docs/frontend.md Normal file
View File

@@ -0,0 +1,79 @@
# Фронтенд (веб-дашборд)
Устройство встроенного веб-интерфейса: страницы, модули TypeScript, сборка, взаимодействие с API.
**Аудитория:** разработчики.
## Содержание
- [Обзор](#обзор)
- [Страницы](#страницы)
- [Модули TypeScript](#модули-typescript)
- [Взаимодействие с API](#взаимодействие-с-api)
- [Сборка](#сборка)
- [Цикл разработки](#цикл-разработки)
## Обзор
Фронтенд — SPA на **ванильном TypeScript без фреймворков и рантайм-зависимостей**: ES-модули, Canvas для графиков, Web Audio API для звуковой индикации. Исходники — [cmd/server/web/js/](../cmd/server/web/js/), компилируются `tsc` в `web/dist/` (ES2020, strict), стили — `web/css/`, разметка — `web/*.html`.
Готовые файлы **вшиваются в бинарник сервера** (`go:embed web/dist web/css web/fonts web/*.html web/*.png`) и раздаются с корня `/` — отдельного веб-сервера для фронтенда нет.
## Страницы
| Страница | Назначение | Точка входа JS |
|---|---|---|
| `index.html` | Редирект на дашборд | — |
| `dashboard.html` | Мониторинг в реальном времени: статус, каналы, график, VU-метр звука, камера | `dist/app.js` |
| `logs.html` | Просмотр исторических данных: список `.bin`-файлов, сэмплы с пагинацией, события | `dist/logs.js` |
## Модули TypeScript
Карта модулей `js/` (в скобках — размер на момент написания):
| Модуль | Роль |
|---|---|
| `app.ts` (434 строки) | Оркестратор дашборда: инициализация всех модулей, цикл опроса сервера (каждые 500 мс), раздача данных в state/ui/chart/audio |
| `data.ts` (32) | API-слой дашборда: `fetchHealth()`, `fetchHistory()` + типы ответов |
| `state.ts` (222) | Состояние дашборда: история сигнала, peak-holder'ы амплитуды и уровня сигнала с таймерами удержания |
| `ui.ts` (414) | Каталог DOM-элементов (`DOM`) и все операции обновления интерфейса: статус, бары, каналы, индикаторы |
| `chart.ts` (137) | Отрисовка графика истории на Canvas (сетка, шкала 063) |
| `audio.ts` (306) | `AudioEngine` — звуковая индикация через Web Audio API: тон зависит от уровня силы (600/1000/1200 Гц), громкость регулируется |
| `camera.ts` (164) | Панель камеры: подключение/отключение MJPEG `<img src="/api/cam">`, перекрестие, периодическая проверка потока |
| `layout.ts` (235) | Разделитель панелей (drag-resize), открытие/закрытие правой панели |
| `accordion.ts` (23) | Сворачиваемый блок «Принятые данные» |
| `logs.ts` (747) | Вся страница logs.html: список файлов, таблица сэмплов и событий с пагинацией, график по файлу, автообновление |
Зависимости между модулями — однонаправленные: `app.ts` импортирует остальные; `logs.ts` автономен (использует только `chart.ts`). Типы ответов API объявлены локально в `data.ts` и `logs.ts` (дублирование — см. [tech-debt.md](tech-debt.md#фронтенд-cmdserverwebjs)).
## Взаимодействие с API
Полная спецификация — [api.md](api.md).
| Модуль | Эндпоинты |
|---|---|
| `data.ts` (дашборд) | `GET /api/health`, `GET /api/history` — опрос каждые 500 мс из `app.ts` |
| `camera.ts` | `GET /api/cam` (MJPEG через `<img>`) |
| `logs.ts` | `GET /api/log/files`, `GET /api/log/data`, `GET /api/log/events`, `GET /api/health` (серверное время) |
## Сборка
```bash
cd cmd/server/web
npm install # однократно (единственная dev-зависимость — typescript)
npm run build # tsc: js/*.ts → dist/*.js
npm run watch # tsc --watch
npm run clean # удалить dist/
```
Конфигурация — [tsconfig.json](../cmd/server/web/tsconfig.json): target/module ES2020, `strict: true`, `rootDir: js`, `outDir: dist`. Каталоги `dist/` и `node_modules/` в git не хранятся.
**Важно:** без собранного `dist/` не соберётся и Go-сервер (`go:embed`) — см. [development.md](development.md#порядок-сборки).
## Цикл разработки
1. Терминал 1: `cd cmd/server/web && npm run watch` — пересборка TS при каждом сохранении;
2. Терминал 2: `make dev` — Go-сервер;
3. После изменения `.ts` — обновить страницу; после изменения `.html`/`.css` при работе через встроенные файлы — перезапустить сервер (файлы вшиваются на этапе сборки, `make dev` через `go run` перечитает их при рестарте).
Данные без железа — эмулятор: [development.md](development.md#запуск-без-железа-эмуляция).

255
docs/operations.md Normal file
View File

@@ -0,0 +1,255 @@
# Руководство администратора
Установка, настройка и обслуживание GPIO Monitor на Raspberry Pi.
**Аудитория:** администраторы и операторы.
## Содержание
- [Требования](#требования)
- [Установка из deb-пакета](#установка-из-deb-пакета)
- [Ручная установка](#ручная-установка)
- [Параметры командной строки](#параметры-командной-строки)
- [Firewall (nftables)](#firewall-nftables)
- [Wi-Fi Access Point](#wi-fi-access-point)
- [Камера (go2rtc)](#камера-go2rtc)
- [Микрофон](#микрофон)
- [Логи: где лежат и как смотреть](#логи-где-лежат-и-как-смотреть)
- [Диагностика проблем](#диагностика-проблем)
## Требования
- Raspberry Pi (целевая платформа — ARM64, Raspberry Pi OS Bookworm);
- C-программа захвата `gpio-interrupt` (WiringPi) на устройстве — поставляется отдельно, в репозитории её нет;
- `alsa-utils` (`arecord`) — для монитора уровня звука;
- go2rtc — для видеопотока камеры (бинарник в [scripts/go2rtc](../scripts/go2rtc));
- systemd.
Назначение GPIO-пинов:
```
WR/STROBE = GPIO27
DATA BUS:
D0 = GPIO21 D4 = GPIO25
D1 = GPIO7 D5 = GPIO24
D2 = GPIO6 D6 = GPIO23
D3 = GPIO5 D7 = GPIO22
```
## Установка из deb-пакета
Рекомендуемый способ развёртывания. Сборка пакета — см. [development.md](development.md#сборка-deb-пакета); итоговый файл: `build/gpio-monitor-server_<база>-build<N>_arm64.deb`, где `N` — число git-коммитов (например `gpio-monitor-server_1.0.0-build83_arm64.deb`).
```bash
sudo dpkg -i build/gpio-monitor-server_1.0.0-build83_arm64.deb
```
Пакет при установке ([debian/preinst](../debian/preinst), [debian/postinst](../debian/postinst)):
- создаёт системного пользователя `gpio-monitor` (группы `gpio`, `dialout`) и каталоги `/var/log/gpio-monitoring`, `/var/lib/gpio-monitoring`, `/opt/gpio-monitoring/web`;
- ставит бинарник в `/usr/bin/gpio-monitor-server` и утилиту `gpio-logs` в `/usr/bin/gpio-logs`;
- кладёт unit `gpio-monitor-server.service` в `/etc/systemd/system/` и справочный конфиг в `/etc/gpio-monitoring/config`;
- включает и **сразу запускает** сервис.
Unit [debian/gpio-monitor-server.service](../debian/gpio-monitor-server.service) запускает сервер с параметрами `-retention-hours=72 -retention-mb=2000 -retention-interval=60 -rotation-check-interval=1 -buffer-size=10240 -human-log-interval=5` (обратите внимание: retention здесь 72 часа, а не встроенные по умолчанию 48). Чтобы изменить параметры, отредактируйте `ExecStart` в unit-файле и выполните `sudo systemctl daemon-reload && sudo systemctl restart gpio-monitor-server`.
> Файл `/etc/gpio-monitoring/config` — справочный: сервис его **не читает**, параметры берутся только из `ExecStart` (см. [tech-debt.md](tech-debt.md#пакеты-internal)).
Управление:
```bash
sudo systemctl status gpio-monitor-server
sudo systemctl restart gpio-monitor-server
gpio-logs -f # журнал сервиса
```
Удаление: `sudo apt remove gpio-monitor-server`; полная очистка с логами и пользователем: `sudo apt purge gpio-monitor-server`.
## Ручная установка
### 1. Зависимости и сборка
```bash
sudo apt update
sudo apt install -y golang git build-essential wiringpi nodejs npm alsa-utils
git clone <your-repo> && cd golang
make init && make build # подробности — development.md
```
### 2. Канал данных: systemd socket-activation
FIFO `/tmp/gpio_pipe` и программу захвата запускает systemd ([scripts/monitor-gpio.socket](../scripts/monitor-gpio.socket), [scripts/monitor-gpio.service](../scripts/monitor-gpio.service)): socket-юнит создаёт FIFO, service-юнит направляет stdout `gpio-interrupt` в него.
```bash
sudo cp scripts/monitor-gpio.service scripts/monitor-gpio.socket /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable monitor-gpio.socket monitor-gpio.service
sudo systemctl start monitor-gpio.service
```
> Пути в `monitor-gpio.service` (`/home/user/WiringPi/examples/gpio-interrupt`) и владелец FIFO в `monitor-gpio.socket` (`SocketUser=user`) заданы под конкретное устройство — проверьте их перед установкой.
### 3. Go-сервер как сервис
Шаблон — [scripts/go2monitor.service](../scripts/go2monitor.service) (запуск собранного бинарника от обычного пользователя). Поправьте `WorkingDirectory` и путь в `ExecStart` под своё расположение проекта, затем:
```bash
sudo cp scripts/go2monitor.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now go2monitor.service
```
Либо запустите вручную:
```bash
./build/gpio-monitor-server -pipe /tmp/gpio_pipe -retention-hours=72 -retention-mb=2000
```
### 4. Проверка
```bash
sudo systemctl status monitor-gpio.service
curl http://localhost:8080/api/health
```
Дашборд: `http://<IP>:8080/` (редирект на `dashboard.html`), просмотр истории: `http://<IP>:8080/logs.html`.
## Параметры командной строки
Полный список флагов `gpio-monitor-server` (источник — [cmd/server/main.go](../cmd/server/main.go)):
| Параметр | По умолчанию | Описание |
|---|---|---|
| `-pipe` | `/tmp/gpio_pipe` | Путь к FIFO pipe |
| `-port` | `:8080` | Адрес/порт веб-сервера |
| `-retention-hours` | `48` | Часы хранения `.bin`-логов (0 = отключено) |
| `-retention-mb` | `5000` | Максимальный суммарный размер `.bin`-логов, МБ (0 = отключено) |
| `-retention-interval` | `15` | Интервал проверки retention, мин |
| `-rotation-check-interval` | `1` | Интервал проверки ротации, мин |
| `-buffer-size` | `10240` | Размер кольцевого буфера, элементов |
| `-silence-5min` | `5` | Порог тишины для события `ТИШИНА_5МИН`, мин |
| `-silence-10min` | `10` | Порог тишины для события `ТИШИНА_10МИН`, мин |
| `-watchdog-interval` | `1` | Интервал проверки watchdog, мин |
| `-alert-cooldown` | `10` | Анти-спам между одинаковыми алертами, сек |
| `-human-log-interval` | `5` | Интервал записи human-логов, сек |
| `-camera-url` | `http://127.0.0.1:1984/api/stream.mjpeg?src=cam_mjpeg` | URL MJPEG-потока для прокси `/api/cam` |
## Firewall (nftables)
На Raspberry Pi OS Bookworm по умолчанию активен nftables, блокирующий входящие порты, кроме SSH (22).
Разрешить порт 8080:
```bash
sudo nft add rule inet filter input tcp dport 8080 accept
# Проверить и сохранить
sudo nft list ruleset
sudo nft list ruleset | sudo tee /etc/nftables.conf > /dev/null
sudo systemctl enable nftables
sudo systemctl restart nftables
```
Ограничить доступ:
```bash
# Только локальная сеть
sudo nft add rule inet filter input ip saddr 10.1.1.0/24 tcp dport 8080 accept
# Только конкретный IP
sudo nft add rule inet filter input ip saddr 192.168.1.100 tcp dport 8080 accept
# Только через Wi-Fi AP
sudo nft add rule inet filter input iifname "wlan0" tcp dport 8080 accept
```
## Wi-Fi Access Point
Raspberry Pi может работать точкой доступа:
- SSID: `fix_me`, IP устройства: `192.168.77.1`;
- подключение: `ssh user@192.168.77.1`;
- дашборд: `http://192.168.77.1:8080`.
## Камера (go2rtc)
Видеопоток отдаёт go2rtc (конфиг — [scripts/go2rtc.yaml](../scripts/go2rtc.yaml)): поток `cam_mjpeg` — MJPEG 640×480 15 fps с `/dev/video0` через ffmpeg, API на порту `1984` (также RTSP `:8554`, WebRTC `:8555`).
```bash
./scripts/go2rtc -config scripts/go2rtc.yaml
```
Проверка: `http://<IP>:1984` (встроенный интерфейс go2rtc), `http://<IP>:1984/api/stream.mjpeg?src=cam_mjpeg` (прямой поток). Go-сервер проксирует этот поток на `/api/cam`.
## Микрофон
Уровень звука снимается через `arecord` (пакет `alsa-utils`); USB-микрофон находится автоматически. Проверка:
```bash
arecord -l
# **** List of CAPTURE Hardware Devices ****
# card 3: Device [USB PnP Sound Device], device 0: USB Audio [USB Audio]
```
Если список пуст — микрофон не определился; сервер при этом работает нормально, но `/api/health` отдаёт `sound_level: 0`. Пользователь, от которого запущен сервис, должен состоять в группе `audio` (`sudo usermod -aG audio <user>`). При физическом отвале микрофона сервер сам пытается восстановить захват каждые 5 секунд.
## Логи: где лежат и как смотреть
Пути зависят от режима (подробно — [data-formats.md](data-formats.md#пути-хранения)):
| Файл | Пакетная установка | Ручной запуск (dev) |
|---|---|---|
| Бинарные данные `gpio-*.bin` | `/var/log/gpio-monitoring/data/` | `~/.local/share/gpio-monitoring/logs/data/` |
| События | `/var/log/gpio-monitoring/events.log`, `events_human.log` | `~/.local/share/gpio-monitoring/logs/...` |
| GPIO human-лог | `/var/log/gpio-monitoring/gpio_human.log` | `~/.local/share/gpio-monitoring/logs/gpio_human.log` |
| Журнал сервиса | `journalctl -u gpio-monitor-server` | stdout |
Просмотр:
```bash
# Пакетная установка — утилита gpio-logs:
gpio-logs -f # журнал сервиса (journalctl)
gpio-logs -e # события
gpio-logs -g # GPIO human-лог
gpio-logs -d # сырые бинарные данные (xxd)
# Dev-режим:
./scripts/view_logs.sh # сводная статистика логов
```
Место на диске контролируется retention (см. флаги `-retention-*`); при значениях по умолчанию deb-юнита — не более 2000 МБ и 72 часов бинарных данных.
## Диагностика проблем
### Dashboard не открывается
1. Сервер запущен?
```bash
ps aux | grep gpio-monitor-server
sudo systemctl status gpio-monitor-server # или go2monitor / monitor-gpio при ручной установке
```
2. Firewall: `sudo nft list ruleset | grep 8080`
3. Сервер слушает нужный интерфейс: `sudo ss -tlnp | grep 8080` (должно быть `*:8080` или `0.0.0.0:8080`)
4. Маршрутизация: `ip route show`
5. Логи:
```bash
sudo journalctl -u gpio-monitor-server -f
sudo tail -f /var/log/gpio-monitoring/events_human.log # пакетная установка
tail -f ~/.local/share/gpio-monitoring/logs/events_human.log # dev-режим
```
6. FIFO существует: `ls -la /tmp/gpio_pipe`
### Статус «Нет связи» на дашборде
Данные не поступают дольше 15 секунд. Проверьте цепочку: `monitor-gpio.service` запущен → FIFO существует → в `events_human.log` нет свежих `PIPE_НЕ_НАЙДЕН`/`PIPE_ОТКЛЮЧЕН` → события `ТИШИНА_5МИН` укажут, что pipe жив, но данных нет (проблема на стороне захвата/шины).
### Индикатор «🔊 ЗВУК» не реагирует
1. `which arecord && arecord -l` — arecord установлен и видит микрофон;
2. в журнале сервера при старте нет строки `Аудио-монитор не запущен (микрофон недоступен?)`;
3. пользователь сервиса в группе `audio`: `groups <user>`.
### Камера не показывает
1. go2rtc запущен и отвечает: `curl http://localhost:1984/api/streams`;
2. `/dev/video0` существует;
3. `/api/stream` возвращает `available: true`. Учтите: проверка доступности всегда идёт на `localhost:1984` независимо от `-camera-url` (см. [api.md](api.md#известные-особенности)).

59
docs/tech-debt.md Normal file
View File

@@ -0,0 +1,59 @@
# Реестр технического долга
Известные проблемы и упрощения, принятые в текущей реализации. Реестр ведётся, чтобы долг был видим и осознан; исправления — отдельные задачи. При закрытии пункта удаляйте его отсюда, при появлении нового — добавляйте с указанием места и влияния.
**Аудитория:** разработчики.
Состояние на 2026-07-17. Маркеров `TODO`/`FIXME` в коде нет — этот файл единственный источник.
## Go-сервер (cmd/server/main.go)
| # | Проблема | Где | Влияние | Направление исправления |
|---|---|---|---|---|
| 1 | Монолит: 8 HTTP-хендлеров, разбор флагов, чтение `.bin`, прокси камеры — всё в одном файле на ~750 строк | `main.go` | Затрудняет навигацию и тестирование хендлеров | Вынести HTTP-слой в `internal/api`, чтение `.bin` — в `internal/logger` |
| 2 | Мёртвый код: `handleCamProxy` не вызывается (роутер использует `handleCamProxyWithURL`) | `main.go:474` | Путает при чтении, дублирует логику | Удалить после подтверждения |
| 3 | `HandleStream` проверяет доступность камеры по захардкоженному `localhost:1984`, игнорируя `-camera-url` | `main.go:460` | При нестандартном URL камеры `available` врёт | Строить URL проверки из `-camera-url` |
| 4 | Разбор байта GPIO продублирован: `HandleLogData` сдвигает биты сам вместо `logger.ParseGPIO` | `main.go:297298``internal/logger/parser.go` | Риск рассинхронизации формата | Использовать `ParseGPIO` |
| 5 | `/api/log/data` читает весь `.bin`-файл в память до пагинации | `main.go:278308` | На больших файлах — всплеск памяти на каждый запрос | Читать нужный диапазон по смещению (записи фиксированные, 9 байт) |
| 6 | CORS открыт для всех источников (`*`) | `main.go:522535` | Приемлемо для изолированной сети; риск при выходе наружу | Осознанное решение зафиксировано; при необходимости — allowlist |
| 7 | `/api/log/files` возвращает `null` вместо `[]` при отсутствии файлов; `/api/latest` отдаёт 200 с `{"error":"no data"}` | `main.go:67,105`, `main.go:435` | Клиентам нужны доп. проверки | Инициализировать слайс; вернуть 204/404 либо задокументированную схему |
## Пакеты internal/
| # | Проблема | Где | Влияние | Направление исправления |
|---|---|---|---|---|
| 8 | Два параллельных механизма расчёта скорости пишут в одни атомики: периодический (250 мс) и скользящее окно (1 с); публичный `GetWindowSpeed()` не используется | `internal/adapter/buffer.go:77162` | Значение `bytes_per_sec` зависит от того, кто записал последним; лишний код и память (`recentBytes`) | Оставить один механизм |
| 9 | Конфигурация размазана: значения по умолчанию во флагах (`retention-hours=48`), в deb-юните (`72`), в `scripts/go2monitor.service` (`72`) и в справочном `/etc/gpio-monitoring/config`, который сервис **не читает** | `main.go`, `debian/gpio-monitor-server.service`, `debian/gpio-monitor-server.conf` | Непонятно, что «истина»; правка конфига не влияет на сервис | Либо читать `EnvironmentFile=/etc/gpio-monitoring/config` в юните, либо удалить конфиг-файл |
| 10 | `DataLogger.flush` молча глотает ошибку записи (комментарий «тут должен быть event» в коде) | `internal/logger/data_logger.go:6670` | Потеря данных при полном диске останется незамеченной | Прокинуть EventLogger и писать событие |
## Тесты и CI
| # | Проблема | Где | Влияние | Направление исправления |
|---|---|---|---|---|
| 11 | Единственный тест-файл на проект — retention; без тестов `parser.go`, `buffer.go` (конкурентность), `rotation.go`, `data_logger.go` (бинарный формат), `reader.go` | `internal/logger/retention_test.go` | Регрессии форматов/конкурентности не ловятся | Начать с parser (тривиально) и data_logger (формат 9 байт) |
| 12 | CI отсутствует (нет `.gitea/workflows/`) | — | Сборка и тесты не проверяются автоматически | Gitea Actions: `make build-frontend`, `go vet`, `go test` |
## Сборка и инфраструктура
| # | Проблема | Где | Влияние | Направление исправления |
|---|---|---|---|---|
| 13 | Цели `docker-build`/`docker-run` есть, Dockerfile — нет | `Makefile:249256` | Цели заведомо падают | Удалить цели или добавить Dockerfile |
| 14 | C-программа захвата `gpio-interrupt` — ключевой компонент системы — не версионируется в репозитории (живёт на устройстве в `/home/user/WiringPi/examples`) | `scripts/monitor-gpio.service` | Невоспроизводимость: систему нельзя собрать целиком из репозитория | Добавить исходник в репозиторий (например `capture/gpio-interrupt.c`) |
| 15 | `go build` требует предварительно собранного `web/dist` (`go:embed`) — чистый checkout не собирается командой `go build ./...` | `cmd/server/main.go:28` | Неочевидная ошибка для новичка; ломает go-инструментарий на чистом дереве | Задокументировано ([development.md](development.md#порядок-сборки)); вариант — коммитить заглушку `dist/.keep` с `embed` через `all:` |
| 16 | Захардкоженные пути под конкретное устройство: `build.sh` и `start-server.sh` (`~/temp/golang`), `emulatohackrf.sh`, systemd-юниты в `scripts/` (`/home/user/...`) | `build.sh`, `start-server.sh`, `scripts/*` | Скрипты не переносимы | Параметризовать или пометить как шаблоны |
## Фронтенд (cmd/server/web/js/)
| # | Проблема | Где | Влияние | Направление исправления |
|---|---|---|---|---|
| 17 | Крупные модули без разбивки: `logs.ts` (747 строк), `app.ts` (434), `ui.ts` (414) | `js/` | Сложно поддерживать | Разбить `logs.ts` на api/таблицы/пагинацию |
| 18 | Типы ответов API объявлены дважды: в `data.ts` и `logs.ts` | `js/data.ts`, `js/logs.ts` | Рассинхронизация с сервером ловится только вручную | Общий `js/api-types.ts` |
| 19 | Нет линтера/форматтера (eslint/prettier) и ни одного теста фронтенда; `make fmt` вызывает несуществующий `npm run format` | `package.json`, `Makefile:112` | Стиль и регрессии не контролируются | Добавить prettier + script `format` |
| 20 | `logs.ts:284` передаёт в `/api/log/events` параметр `order=newest_first`, который сервер не читает | `js/logs.ts:284` | Мусорный параметр, вводит в заблуждение | Убрать параметр |
## SDR/
| # | Проблема | Где | Влияние | Направление исправления |
|---|---|---|---|---|
| 21 | makefile ожидает `receiver.c`, реальный файл — `reciever.c` (опечатка): цель сборки приёмника не срабатывает | `SDR/makefile`, `SDR/reciever.c` | `make` собирает только передатчик; приёмник — только вручную (обходная команда в [SDR/README.md](../SDR/README.md)) | Переименовать файл в `receiver.c` (или поправить makefile) |
| 22 | Статус подпроекта не определён: SDR не связан с основной системой ни сборкой, ни CI | `SDR/` | Непонятно, поддерживается ли код | Зафиксировать статус в SDR/README (актуален/эксперимент/заморожен) |