Доработка документации
This commit is contained in:
541
README.md
541
README.md
@@ -1,508 +1,191 @@
|
||||
# GPIO Monitor Dashboard for Raspberry Pi
|
||||
|
||||
Реалтайм мониторинг параллельной GPIO-шины Raspberry Pi через C + Go + Web Dashboard.
|
||||
Реалтайм-мониторинг параллельной GPIO-шины Raspberry Pi: C + Go + Web Dashboard.
|
||||
|
||||
Проект читает входящие сигналы с GPIO-пинов Raspberry Pi по прерыванию, передаёт данные через FIFO pipe в Go-сервер и отображает всё в браузере в реальном времени с возможностью просмотра исторических данных.
|
||||
Проект читает входящие сигналы с GPIO-пинов Raspberry Pi по прерыванию, передаёт данные через FIFO pipe в Go-сервер и отображает их в браузере в реальном времени с возможностью просмотра исторических данных.
|
||||
|
||||
---
|
||||
|
||||
## Возможности
|
||||
|
||||
✅ Захват данных по GPIO interrupt (C + WiringPi)
|
||||
✅ FIFO pipe, а также stdin/stdout между C + WiringPi и Go Service
|
||||
✅ Реализация кольцевого буфера чтения
|
||||
✅ HTTP API + Web Dashboard
|
||||
✅ **Автоматическая ротация** лог-файлов
|
||||
✅ **Retention** (автоматическая очистка старых логов)
|
||||
✅ **Watchdog** мониторинг активности
|
||||
✅ **MJPEG камера** через go2rtc
|
||||
✅ TypeScript фронтенд
|
||||
✅ Захват данных по GPIO interrupt (C + WiringPi)
|
||||
✅ FIFO pipe между капчером и Go-сервером (systemd socket-activation)
|
||||
✅ Кольцевой буфер в RAM + бинарный архив на диске
|
||||
✅ HTTP API + встроенный Web Dashboard (TypeScript)
|
||||
✅ **Автоматическая ротация** лог-файлов (почасовые `.bin`)
|
||||
✅ **Retention** — автоочистка старых логов по возрасту и размеру
|
||||
✅ **Watchdog** — события при пропадании данных
|
||||
✅ **Монитор звука** с микрофона (arecord/ALSA)
|
||||
✅ **MJPEG-камера** через go2rtc
|
||||
✅ Сборка **deb-пакета** с версионированием через git
|
||||
✅ Работа через Wi-Fi Access Point
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
## Документация
|
||||
|
||||
| Документ | Содержание | Для кого |
|
||||
|---|---|---|
|
||||
| [docs/operations.md](docs/operations.md) | Установка (deb и вручную), systemd, firewall, камера, микрофон, диагностика | Администраторы |
|
||||
| [docs/architecture.md](docs/architecture.md) | Устройство системы: компоненты, поток данных, конкурентность | Разработчики |
|
||||
| [docs/api.md](docs/api.md) | Спецификация HTTP API: все эндпоинты с примерами ответов | Разработчики |
|
||||
| [docs/data-formats.md](docs/data-formats.md) | Байт GPIO, формат `.bin`, протокол pipe, события, пути хранения | Разработчики |
|
||||
| [docs/development.md](docs/development.md) | Сборка, Makefile, эмуляторы, тесты, соглашения | Разработчики |
|
||||
| [docs/frontend.md](docs/frontend.md) | Веб-дашборд: модули TypeScript, сборка, dev-цикл | Разработчики |
|
||||
| [docs/tech-debt.md](docs/tech-debt.md) | Реестр технического долга | Разработчики |
|
||||
| [SDR/README.md](SDR/README.md) | Подпроект OFDM-радиолинка на PlutoSDR (автономный) | Разработчики |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | История изменений | Все |
|
||||
|
||||
---
|
||||
|
||||
## Архитектура (кратко)
|
||||
|
||||
```text
|
||||
GPIO BUS (8 bit)
|
||||
↓
|
||||
gpio-interrupt.c (C + WiringPi)
|
||||
↓ stdout
|
||||
↓ прерывание
|
||||
gpio-interrupt (C + WiringPi) ← поставляется отдельно, вне репозитория
|
||||
↓ stdout → systemd socket
|
||||
FIFO pipe (/tmp/gpio_pipe)
|
||||
↓
|
||||
Go Server (gpio-monitor-server)
|
||||
├── RingBuffer (RAM)
|
||||
├── Data Logger (бинарные .bin файлы)
|
||||
├── Human Logger (текстовые логи)
|
||||
├── Event Logger (события системы)
|
||||
├── Watchdog (мониторинг)
|
||||
└── Retention (очистка старых логов)
|
||||
├── Data Logger (почасовые .bin)
|
||||
├── Human / Event Logger (текстовые логи)
|
||||
├── Watchdog + Retention
|
||||
├── Audio Monitor (arecord)
|
||||
└── HTTP API (:8080) + Web Dashboard (embedded)
|
||||
↓
|
||||
HTTP API (:8080)
|
||||
├── /api/health - статус сервера
|
||||
├── /api/latest - последние данные
|
||||
├── /api/history - история (последние 100)
|
||||
├── /api/stream - статус камеры
|
||||
├── /api/cam - MJPEG поток
|
||||
├── /api/log/files - список лог-файлов
|
||||
├── /api/log/data - данные из лог-файла
|
||||
└── /api/log/events - события системы
|
||||
↓
|
||||
Web Dashboard (Embedded)
|
||||
├── index.html
|
||||
├── dashboard.html (главная)
|
||||
├── logs.html (просмотр логов)
|
||||
├── css/style.css
|
||||
├── js/*.ts (TypeScript исходники)
|
||||
└── dist/*.js (скомпилированный JS)
|
||||
Браузер: dashboard.html / logs.html
|
||||
```
|
||||
|
||||
HTTP API: `/api/health`, `/api/latest`, `/api/history` (последние 300), `/api/stream`, `/api/cam` (MJPEG-прокси), `/api/log/files`, `/api/log/data?file=...&page=...`, `/api/log/events?page=...` — подробности в [docs/api.md](docs/api.md).
|
||||
|
||||
Подробная схема — [docs/architecture.md](docs/architecture.md).
|
||||
|
||||
---
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
.
|
||||
├── build/
|
||||
│ └── gpio-monitor-server # Скомпилированный бинарник
|
||||
├── build.sh # Скрипт сборки (Go + TypeScript)
|
||||
├── Makefile # Сборка (основной способ)
|
||||
├── build.sh # Исторический скрипт сборки
|
||||
├── start-server.sh # Скрипт запуска на устройстве
|
||||
├── build/ # Артефакты сборки (генерируется)
|
||||
├── cmd/server/
|
||||
│ ├── main.go # Go HTTP сервер
|
||||
│ ├── main.go # Go HTTP-сервер (точка входа)
|
||||
│ └── web/ # Веб-интерфейс
|
||||
│ ├── css/style.css
|
||||
│ ├── dist/ # Скомпилированный JS
|
||||
│ │ ├── app.js
|
||||
│ │ ├── chart.js
|
||||
│ │ ├── data.js
|
||||
│ │ └── ...
|
||||
│ ├── js/ # TypeScript исходники
|
||||
│ │ ├── app.ts
|
||||
│ │ ├── chart.ts
|
||||
│ │ ├── data.ts
|
||||
│ │ └── ...
|
||||
│ ├── dashboard.html
|
||||
│ ├── index.html
|
||||
│ ├── logs.html
|
||||
│ ├── favicon.png
|
||||
│ ├── package.json
|
||||
│ ├── tsconfig.json
|
||||
│ ├── js/ # TypeScript-исходники (10 модулей)
|
||||
│ ├── dist/ # Скомпилированный JS (генерируется tsc)
|
||||
│ ├── css/, fonts/
|
||||
│ ├── index.html, dashboard.html, logs.html, favicon.png
|
||||
│ ├── package.json, tsconfig.json
|
||||
│ └── README.md
|
||||
├── internal/
|
||||
│ ├── adapter/buffer.go # RingBuffer
|
||||
│ ├── adapter/buffer.go # Кольцевой буфер
|
||||
│ ├── audio/monitor.go # Монитор уровня звука (arecord)
|
||||
│ ├── logger/ # Система логирования
|
||||
│ │ ├── config.go
|
||||
│ │ ├── data_logger.go # Бинарные логи
|
||||
│ │ ├── event_logger.go # События
|
||||
│ │ ├── human_logger.go # Человекочитаемые логи
|
||||
│ │ ├── monitor.go # Watchdog
|
||||
│ │ ├── parser.go # Парсер данных GPIO
|
||||
│ │ ├── paths.go # Пути к файлам
|
||||
│ │ ├── retention.go # Очистка старых логов
|
||||
│ │ └── rotation.go # Ротация файлов
|
||||
│ │ ├── config.go # Конфигурация
|
||||
│ │ ├── data_logger.go # Бинарные логи
|
||||
│ │ ├── event_logger.go # События
|
||||
│ │ ├── human_logger.go # Человекочитаемые логи
|
||||
│ │ ├── monitor.go # Watchdog
|
||||
│ │ ├── parser.go # Разбор байта GPIO
|
||||
│ │ ├── paths.go # Пути хранения
|
||||
│ │ ├── retention.go # Очистка старых логов
|
||||
│ │ ├── retention_test.go # Тесты retention
|
||||
│ │ └── rotation.go # Почасовая ротация
|
||||
│ └── pipe/reader.go # Чтение из FIFO pipe
|
||||
├── scripts/
|
||||
│ ├── monitor-gpio.service # Systemd сервис
|
||||
│ ├── monitor-gpio.socket # Systemd socket
|
||||
│ ├── go2rtc # Бинарник go2rtc
|
||||
│ ├── go2rtc.yaml # Конфиг go2rtc
|
||||
│ ├── view_logs.sh # Скрипт просмотра логов
|
||||
│ ├── emulator.py # Эмулятор GPIO
|
||||
│ └── imi_wire.py # IMI Wire эмулятор
|
||||
├── SDR/ # SDR компоненты
|
||||
│ ├── common.c/h
|
||||
│ ├── reciever.c
|
||||
│ ├── transmitter.c
|
||||
│ └── makefile
|
||||
├── go.mod
|
||||
├── start-server.sh
|
||||
│ ├── monitor-gpio.service # systemd: капчер GPIO
|
||||
│ ├── monitor-gpio.socket # systemd: FIFO pipe
|
||||
│ ├── go2monitor.service # systemd: Go-сервер (ручная установка)
|
||||
│ ├── go2rtc, go2rtc.yaml # Стрим камеры
|
||||
│ ├── emulator.py, array.txt # Эмулятор потока GPIO
|
||||
│ ├── imi_wire.py # Имитатор шины на реальных GPIO
|
||||
│ ├── emulatohackrf.sh # Передача данных через HackRF
|
||||
│ ├── vu_meter.py # Консольный VU-метр микрофона
|
||||
│ └── view_logs.sh # Статистика dev-логов
|
||||
├── debian/ # Сборка deb-пакета (control, unit, скрипты)
|
||||
├── SDR/ # Подпроект: OFDM-радиолинк PlutoSDR (C)
|
||||
│ ├── common.c/h, transmitter.c, reciever.c
|
||||
│ ├── makefile
|
||||
│ └── README.md
|
||||
├── docs/ # Документация (см. таблицу выше)
|
||||
├── go.mod # module gpio-monitor, Go 1.21, без зависимостей
|
||||
├── CHANGELOG.md
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## GPIO пины
|
||||
Каталоги `build/`, `cmd/server/web/dist/` и `cmd/server/web/node_modules/` генерируются при сборке и в git не хранятся.
|
||||
|
||||
## GPIO-пины
|
||||
|
||||
```
|
||||
WR/STROBE = GPIO27
|
||||
|
||||
DATA BUS:
|
||||
D0 = GPIO21
|
||||
D1 = GPIO7
|
||||
D2 = GPIO6
|
||||
D3 = GPIO5
|
||||
D4 = GPIO25
|
||||
D5 = GPIO24
|
||||
D6 = GPIO23
|
||||
D7 = GPIO22
|
||||
D0 = GPIO21 D4 = GPIO25
|
||||
D1 = GPIO7 D5 = GPIO24
|
||||
D2 = GPIO6 D6 = GPIO23
|
||||
D3 = GPIO5 D7 = GPIO22
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Установка
|
||||
## Быстрый старт
|
||||
|
||||
### 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 # подготовка окружения (Node.js, Go-модули, npm-пакеты)
|
||||
make build # TypeScript-фронтенд + Go-сервер → build/gpio-monitor-server
|
||||
```
|
||||
|
||||
> `alsa-utils` нужен для монитора уровня звука (использует `arecord`). Проверить, что микрофон виден системе:
|
||||
> ```bash
|
||||
> arecord -l
|
||||
> # **** List of CAPTURE Hardware Devices ****
|
||||
> # card 3: Device [USB PnP Sound Device], device 0: USB Audio [USB Audio]
|
||||
> ```
|
||||
> Если список пуст — микрофон не подключён или не определился, монитор звука будет отдавать `sound_level: 0`.
|
||||
Подробности, кросс-компиляция, эмуляция без железа — [docs/development.md](docs/development.md).
|
||||
|
||||
### 2. Клонирование и сборка
|
||||
|
||||
```bash
|
||||
git clone <your-repo>
|
||||
cd ~/work/golang
|
||||
|
||||
# Полная подготовка окружения (проверка Node.js, скачивание Go-модулей и npm-пакетов)
|
||||
make init
|
||||
```
|
||||
|
||||
### 3. Сборка проекта
|
||||
``` Bash
|
||||
# Скомпилируйте TypeScript-фронтенд и Go-сервер одной командой:
|
||||
make build
|
||||
```
|
||||
|
||||
### 4. Настройка systemd сервиса
|
||||
|
||||
Для постоянной работы сервера в фоновом режиме настройте систему инициализации. Поскольку сервер пишет логи в защищенную директорию /var/log/, его запуск и управление осуществляются через systemctl с правами администратора
|
||||
|
||||
```bash
|
||||
# Копировать сервисные файлы
|
||||
sudo cp scripts/monitor-gpio.service /etc/systemd/system/
|
||||
sudo cp scripts/monitor-gpio.socket /etc/systemd/system/
|
||||
|
||||
# Перезагрузить systemd
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Включить автозапуск
|
||||
sudo systemctl enable monitor-gpio.socket
|
||||
sudo systemctl enable monitor-gpio.service
|
||||
|
||||
# Запустить сервис
|
||||
sudo systemctl start monitor-gpio.service
|
||||
```
|
||||
|
||||
### 4. Проверка статуса
|
||||
|
||||
```bash
|
||||
sudo systemctl status monitor-gpio.service
|
||||
journalctl -u monitor-gpio.service -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Настройка firewall (nftables)
|
||||
|
||||
**Важно:** На Raspberry Pi OS Bookworm по умолчанию активен nftables, который блокирует все входящие порты, кроме SSH (22).
|
||||
|
||||
### Разрешить порт 8080:
|
||||
|
||||
```bash
|
||||
# Добавить правило для порта 8080
|
||||
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
|
||||
# Разрешить только с сети 10.1.1.0/24
|
||||
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:
|
||||
|
||||
```bash
|
||||
# Разрешить с Wi-Fi сети (192.168.77.0/24)
|
||||
sudo nft add rule inet filter input iifname "wlan0" tcp dport 8080 accept
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Запуск
|
||||
|
||||
### Ручной запуск
|
||||
### Запуск
|
||||
|
||||
```bash
|
||||
./build/gpio-monitor-server \
|
||||
-pipe /tmp/gpio_pipe \
|
||||
-retention-hours=72 \
|
||||
-retention-mb=2000 \
|
||||
-retention-interval=60 \
|
||||
-rotation-check-interval=1 \
|
||||
-buffer-size=10240 \
|
||||
-human-log-interval=5
|
||||
-retention-mb=2000
|
||||
```
|
||||
|
||||
### Параметры командной строки
|
||||
Дашборд: `http://<IP>:8080` (через Wi-Fi AP — `http://192.168.77.1:8080`, локально — `http://localhost:8080`). Страницы: `/dashboard.html` — мониторинг, `/logs.html` — история.
|
||||
|
||||
| Параметр | По умолчанию | Описание |
|
||||
|----------|------------------|--------------------|
|
||||
| `-pipe` | `/tmp/gpio_pipe` | Путь к FIFO pipe |
|
||||
| `-port` | `:8080` | Порт веб-сервера |
|
||||
| `-retention-hours` | `48` | Часы хранения логов (0 = отключено) |
|
||||
| `-retention-mb` | `5000` | Максимальный размер логов в MB |
|
||||
| `-retention-interval` | `15` | Интервал проверки retention (мин) |
|
||||
| `-rotation-check-interval` | `1` | Интервал проверки ротации (мин)|
|
||||
| `-buffer-size` | `10240`| Размер кольцевого буфера |
|
||||
| `-human-log-interval` | `5` | Интервал записи human-логов (сек) |
|
||||
| `-alert-cooldown` | `10` | Задержка между алертами (сек) |
|
||||
| `-camera-url` | `http://127.0.0.1:1984/api/stream.mjpeg?src=cam_mjpeg` | URL MJPEG камеры |
|
||||
Полная таблица из 13 CLI-флагов, настройка systemd, firewall (nftables), камеры и микрофона — [docs/operations.md](docs/operations.md).
|
||||
|
||||
---
|
||||
|
||||
## Ротация и хранение логов (Retention)
|
||||
|
||||
Бинарные данные GPIO пишутся в почасовые файлы вида `gpio-YYYY-MM-DD-HH.bin`
|
||||
в директории `internal/logger` (`GetDataLogsDir()`). За это отвечают два
|
||||
независимых механизма:
|
||||
|
||||
- **Ротация** (`internal/logger/rotation.go`) — каждую минуту (`-rotation-check-interval`)
|
||||
проверяет текущий час и, если он изменился, закрывает старый `.bin`-файл и
|
||||
открывает новый. Ротация только создаёт новые файлы, старые она не трогает.
|
||||
- **Retention** (`internal/logger/retention.go`) — раз в `-retention-interval`
|
||||
минут (плюс один прогон через минуту после старта) удаляет лишние файлы по
|
||||
двум независимым лимитам: возрасту (`-retention-hours`) и суммарному размеру
|
||||
(`-retention-mb`). Любой из лимитов можно отключить, выставив `0`.
|
||||
|
||||
### Как работает удаление (FIFO)
|
||||
|
||||
Очистка идёт по правилам:
|
||||
|
||||
1. Все `gpio-*.bin` сортируются от самого старого к самому новому (FIFO-порядок).
|
||||
2. Файлы удаляются строго с "старого" конца списка, по одному, пока выполняются
|
||||
оба условия:
|
||||
- файл старше порога **и по времени в имени файла, и по реальному времени
|
||||
последней записи (ModTime)** — если файл был записан недавно, но у него
|
||||
"старое" имя (или наоборот), он не считается кандидатом на удаление;
|
||||
- файлов в директории больше `minKeepFiles` (сейчас — 2).
|
||||
3. Как только встречается первый файл, который не удовлетворяет условиям,
|
||||
очистка останавливается — все файлы правее (более новые) заведомо тоже
|
||||
не подходят под удаление.
|
||||
|
||||
Итог: Очистка по размеру (`cleanBySize`): удаляет самые старые файлы,
|
||||
пока суммарный объём превышает `-retention-mb`, но тоже не опускается ниже
|
||||
`minKeepFiles`.
|
||||
|
||||
Дополнительная защита — при разборе имени файла (`parseFilenameTime`) любая
|
||||
ошибка парсинга (битое или неожиданное имя) не считается "нулевым/древним"
|
||||
временем, а приводит к откату на реальный `ModTime` файла с диска, чтобы
|
||||
повреждённое или нестандартное имя не привело к ошибочному удалению.
|
||||
|
||||
---
|
||||
|
||||
## Dashboard
|
||||
|
||||
Открывайте в браузере:
|
||||
|
||||
- **Через Ethernet:** `http://10.1.1.33:8080` (или ваш IP)
|
||||
- **Через Wi-Fi AP:** `http://192.168.77.1:8080`
|
||||
- **Локально:** `http://localhost:8080`
|
||||
|
||||
### Страницы
|
||||
|
||||
- `http://<IP>:8080/` - Перенаправление на dashboard
|
||||
- `http://<IP>:8080/dashboard.html` - Главная панель мониторинга
|
||||
- `http://<IP>:8080/logs.html` - Просмотр исторических логов
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### Основные эндпоинты
|
||||
### Deb-пакет
|
||||
|
||||
```bash
|
||||
GET /api/health
|
||||
# Статус сервера, uptime, статистика, sound_level (0-100, уровень с микрофона)
|
||||
|
||||
GET /api/latest
|
||||
# Последние данные и история (10 записей)
|
||||
|
||||
GET /api/history
|
||||
# Последние 100 записей
|
||||
|
||||
GET /api/stream
|
||||
# Статус камеры
|
||||
|
||||
GET /api/cam
|
||||
# MJPEG поток камеры (прокси на go2rtc)
|
||||
make deb # от обычного пользователя, без sudo
|
||||
sudo dpkg -i build/gpio-monitor-server_1.0.0-build<N>_arm64.deb
|
||||
```
|
||||
|
||||
### Работа с логами
|
||||
|
||||
```bash
|
||||
GET /api/log/files
|
||||
# Список доступных лог-файлов
|
||||
|
||||
GET /api/log/data?file=gpio-2026-06-17-11.bin
|
||||
# Данные из конкретного бинарного лога
|
||||
|
||||
GET /api/log/events?limit=100
|
||||
# Последние события системы
|
||||
```
|
||||
Версия пакета формируется автоматически: `<база из debian/control>-build<число git-коммитов>`. Полезные цели: `make version` (показать версию), `make deb-info` (информация о пакете), `make deb-contents` (список файлов), `make deb-clean` (очистка). Подробнее — [docs/development.md](docs/development.md#сборка-deb-пакета); что делает пакет при установке — [docs/operations.md](docs/operations.md#установка-из-deb-пакета).
|
||||
|
||||
---
|
||||
|
||||
## Диагностика проблем
|
||||
## Хранение данных (кратко)
|
||||
|
||||
### Если Dashboard не открывается:
|
||||
Бинарные данные пишутся в почасовые файлы `gpio-YYYY-MM-DD-HH.bin` в каталоге данных (`/var/log/gpio-monitoring/data/` при deb-установке, `~/.local/share/gpio-monitoring/logs/data/` в dev-режиме). Ротация открывает новый файл каждый час; retention удаляет старые файлы по возрасту (`-retention-hours`) и суммарному размеру (`-retention-mb`), всегда сохраняя минимум 2 свежих файла.
|
||||
|
||||
1. **Проверьте, что сервер запущен:**
|
||||
```bash
|
||||
ps aux | grep gpio-monitor-server
|
||||
sudo systemctl status monitor-gpio.service
|
||||
```
|
||||
|
||||
2. **Проверьте firewall:**
|
||||
```bash
|
||||
sudo nft list ruleset | grep 8080
|
||||
```
|
||||
|
||||
3. **Проверьте, на каком интерфейсе слушает сервер:**
|
||||
```bash
|
||||
sudo ss -tlnp | grep 8080
|
||||
# Должно быть *:8080 или 0.0.0.0:8080
|
||||
```
|
||||
|
||||
4. **Проверьте маршрутизацию:**
|
||||
```bash
|
||||
ip route show
|
||||
```
|
||||
|
||||
5. **Посмотрите логи:**
|
||||
```bash
|
||||
sudo journalctl -u monitor-gpio.service -f
|
||||
tail -f /home/user/logs/gpio-events.log
|
||||
tail -f /home/user/logs/gpio-human.log
|
||||
```
|
||||
|
||||
6. **Проверьте наличие pipe:**
|
||||
```bash
|
||||
ls -la /tmp/gpio_pipe
|
||||
```
|
||||
|
||||
### Если индикатор "🔊 ЗВУК" на dashboard не реагирует на микрофон:
|
||||
|
||||
1. **Проверьте, что arecord установлен и видит микрофон:**
|
||||
```bash
|
||||
which arecord
|
||||
arecord -l
|
||||
```
|
||||
|
||||
2. **Проверьте лог сервера при запуске** — если микрофон не найден, будет строка:
|
||||
```
|
||||
Аудио-монитор не запущен (микрофон недоступен?): ...
|
||||
```
|
||||
|
||||
3. **Проверьте права доступа к аудио-устройству** (сервис должен работать от пользователя из группы `audio`):
|
||||
```bash
|
||||
groups <user>
|
||||
sudo usermod -aG audio <user>
|
||||
```
|
||||
Форматы файлов, словарь событий и алгоритм очистки — [docs/data-formats.md](docs/data-formats.md).
|
||||
|
||||
---
|
||||
|
||||
## Wi-Fi режим Raspberry Pi
|
||||
|
||||
SSID: `fix_me`
|
||||
IP: `192.168.77.1`
|
||||
|
||||
Подключение: `ssh user@192.168.77.1`
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Запуск Go сервера в режиме разработки:
|
||||
## Диагностика (кратко)
|
||||
|
||||
```bash
|
||||
go run cmd/server/main.go \
|
||||
-pipe /tmp/gpio_pipe \
|
||||
-retention-hours=1 \
|
||||
-human-log-interval=1
|
||||
sudo systemctl status gpio-monitor-server # или monitor-gpio / go2monitor
|
||||
sudo ss -tlnp | grep 8080 # сервер слушает порт?
|
||||
sudo nft list ruleset | grep 8080 # firewall пропускает?
|
||||
ls -la /tmp/gpio_pipe # FIFO существует?
|
||||
gpio-logs -f # журнал сервиса (deb-установка)
|
||||
```
|
||||
|
||||
### Эмуляция GPIO сигналов:
|
||||
|
||||
```bash
|
||||
python3 scripts/emulator.py
|
||||
```
|
||||
|
||||
### Просмотр логов:
|
||||
|
||||
```bash
|
||||
./scripts/view_logs.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Камера (go2rtc)
|
||||
|
||||
Для работы камеры установите go2rtc:
|
||||
|
||||
```bash
|
||||
# Запуск go2rtc
|
||||
./scripts/go2rtc -config scripts/go2rtc.yaml
|
||||
```
|
||||
|
||||
Камера будет доступна по адресу:
|
||||
- MJPEG поток: `http://<IP>:1984/api/stream.mjpeg?src=cam_mjpeg`
|
||||
- Встроенный интерфейс: `http://<IP>:1984`
|
||||
|
||||
---
|
||||
|
||||
## Сборка и управление DEB-пакетом
|
||||
|
||||
В проект добавлена возможность сборки нативного `.deb` пакета для Raspberry Pi OS. Пакет автоматически упаковывает скомпилированный Go-сервер, собранный TypeScript-фронтенд, скрипты логов, конфигурационные файлы и systemd-сервисы.
|
||||
|
||||
|
||||
Сборка пакета выполняется одной командой. **Запуск от обычного пользователя (без sudo):**
|
||||
|
||||
```bash
|
||||
make deb
|
||||
```
|
||||
|
||||
После завершения файл пакета будет доступен по пути: ./build/gpio-monitor-server.deb
|
||||
|
||||
Установка собранного пакета
|
||||
После того как пакет собран, его можно установить в систему с помощью менеджера пакетов apt (он автоматически подтянет системные зависимости, если они требуются):
|
||||
|
||||
```Bash
|
||||
cd ./build
|
||||
sudo dpkg -i gpio-monitor-server_1.0.0-1_arm64.deb
|
||||
```
|
||||
|
||||
Полезные команды для работы с пакетом
|
||||
В Makefile предусмотрены дополнительные команды для проверки и очистки пакета перед деплоем:
|
||||
|
||||
Просмотр информации о пакете (версия, архитектура, зависимости, описание):
|
||||
```Bash
|
||||
# Просмотр содержимого пакета, список файлов и папок:
|
||||
make deb-info
|
||||
```
|
||||
```Bash
|
||||
# Очистка временных файлов сборки пакета:
|
||||
make deb-contents
|
||||
```
|
||||
```Bash
|
||||
make deb-clean
|
||||
sudo apt purge gpio-monitor-server
|
||||
```
|
||||
|
||||
---
|
||||
Полный чек-лист (дашборд, звук, камера, «Нет связи») — [docs/operations.md](docs/operations.md#диагностика-проблем).
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
- [Формат кадра](#формат-кадра)
|
||||
- [Диагностика](#диагностика)
|
||||
- [Устранение неполадок](#устранение-неполадок)
|
||||
- [Лицензия](#лицензия)
|
||||
- [Статус подпроекта](#статус-подпроекта)
|
||||
|
||||
## Возможности
|
||||
|
||||
@@ -55,16 +55,24 @@ sudo ldconfig
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
pluto/
|
||||
SDR/
|
||||
├── common.h # Общие определения и прототипы
|
||||
├── common.c # Реализация общих функций
|
||||
├── transmitter.c # Передатчик
|
||||
├── receiver.c # Приёмник с частотной коррекцией
|
||||
├── Makefile # Система сборки
|
||||
├── reciever.c # Приёмник с частотной коррекцией (имя файла с опечаткой — см. ниже)
|
||||
├── makefile # Система сборки
|
||||
├── README.md # Этот файл
|
||||
└── test_input.bin # Тестовый файл (создаётся make test)
|
||||
```
|
||||
|
||||
> ⚠️ **Известная проблема сборки приёмника.** `makefile` ожидает файл `receiver.c`, но в репозитории он называется `reciever.c` (опечатка в имени). Из-за проверки существования файла цель `receiver` не попадает в `make all`, и `make` собирает **только передатчик**. Пока имя не исправлено, приёмник собирается вручную:
|
||||
>
|
||||
> ```bash
|
||||
> gcc -Wall -O2 -o receiver reciever.c common.c -liio -lliquid -lfftw3f -lm -lpthread
|
||||
> ```
|
||||
>
|
||||
> Долг зафиксирован в [docs/tech-debt.md](../docs/tech-debt.md#sdr) основного проекта.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
@@ -90,11 +98,11 @@ md5sum test_input.bin output.bin
|
||||
# Сборка всего проекта
|
||||
make
|
||||
|
||||
# Сборка только передатчика (если есть transmitter.c)
|
||||
# Сборка только передатчика
|
||||
make transmitter
|
||||
|
||||
# Сборка только приёмника (если есть receiver.c)
|
||||
make receiver
|
||||
# Приёмник — вручную, пока файл называется reciever.c (см. «Известная проблема» выше)
|
||||
gcc -Wall -O2 -o receiver reciever.c common.c -liio -lliquid -lfftw3f -lm -lpthread
|
||||
|
||||
# Создание тестового файла 1 МБ
|
||||
make test
|
||||
@@ -266,7 +274,7 @@ ifconfig eth1
|
||||
### Высокий EVM / много ошибок
|
||||
|
||||
1. **Уменьшите полосу** — `-b 1500000` вместо `-b 3000000`
|
||||
2. **Увеличьте усиление RX** — отредактируйте `RX_GAIN` в `receiver.c`
|
||||
2. **Увеличьте усиление RX** — отредактируйте `RX_GAIN` в `reciever.c`
|
||||
3. **Добавьте паузы** — `-p 5000` для стабилизации
|
||||
4. **Уменьшите амплитуду TX** — `-a 0.15` для снижения искажений
|
||||
5. **Проверьте CFO** — значение > 0.1 требует настройки частоты
|
||||
@@ -276,7 +284,7 @@ ifconfig eth1
|
||||
1. **Проверьте версию liquid-dsp** — требуется >= 1.3.0
|
||||
2. **Пересоберите с отладкой**:
|
||||
```bash
|
||||
gcc -g -O0 -o receiver_debug receiver.c common.c -liio -lliquid -lfftw3f -lm -lpthread
|
||||
gcc -g -O0 -o receiver_debug reciever.c common.c -liio -lliquid -lfftw3f -lm -lpthread
|
||||
gdb ./receiver_debug
|
||||
run -f 1265000000 -r 3840000 -b 2000000 -c rs8
|
||||
bt
|
||||
@@ -314,7 +322,7 @@ cat data.bin | ./transmitter -f 1265000000 -r 3840000 -b 1500000 -g -10 -a 0.4 -
|
||||
```
|
||||
|
||||
|
||||
## Лицензия
|
||||
## Статус подпроекта
|
||||
|
||||
Данный проект распространяется под лицензией MIT. Используйте на свой страх и риск.
|
||||
SDR — автономный подпроект в репозитории GPIO Monitor: со своей сборкой, без связи с Go-сервером. Радиоканал используется как альтернативный транспорт байтов GPIO-шины (см. [scripts/emulatohackrf.sh](../scripts/emulatohackrf.sh) — передача потока эмулятора через HackRF). Общая документация основного проекта — в [docs/](../docs/).
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// gpio-monitor-server — HTTP-сервер мониторинга GPIO-шины Raspberry Pi.
|
||||
// Читает поток байт из FIFO pipe, хранит их в кольцевом буфере и бинарных
|
||||
// почасовых логах, отдаёт JSON API и встроенный веб-дашборд на :8080.
|
||||
// Конфигурация — только флагами командной строки (см. main или docs/operations.md).
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -28,6 +32,8 @@ import (
|
||||
//go:embed web/dist web/css web/fonts web/*.html web/*.png
|
||||
var webFiles embed.FS
|
||||
|
||||
// API — состояние HTTP-хендлеров: кольцевой буфер, время старта сервера
|
||||
// и монитор звука. Спецификация ответов — docs/api.md.
|
||||
type API struct {
|
||||
buf *adapter.RingBuffer
|
||||
startTime time.Time
|
||||
@@ -39,7 +45,8 @@ func writeJSON(w http.ResponseWriter, v any) {
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
// HandleLogFiles - список доступных лог-файлов
|
||||
// HandleLogFiles отдаёт список бинарных лог-файлов gpio-*.bin,
|
||||
// отсортированный по времени изменения (новые первыми).
|
||||
func (a *API) HandleLogFiles(w http.ResponseWriter, r *http.Request) {
|
||||
dataDir, err := logger.GetDataLogsDir()
|
||||
if err != nil {
|
||||
@@ -105,7 +112,8 @@ func (a *API) HandleLogFiles(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, result)
|
||||
}
|
||||
|
||||
// HandleLogEvents - чтение событий с пагинацией
|
||||
// HandleLogEvents отдаёт события из events_human.log с пагинацией
|
||||
// (page, page_size; страница 1 — самые новые).
|
||||
func (a *API) HandleLogEvents(w http.ResponseWriter, r *http.Request) {
|
||||
// Параметры пагинации
|
||||
page := 1
|
||||
@@ -220,7 +228,9 @@ func (a *API) HandleLogEvents(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
|
||||
// HandleLogData - чтение данных из конкретного bin файла с пагинацией
|
||||
// HandleLogData отдаёт сэмплы одного .bin-файла (параметр file) с пагинацией
|
||||
// (page, page_size; страница 1 — самые новые). Формат записи — 9 байт,
|
||||
// см. docs/data-formats.md. Файл читается в память целиком.
|
||||
func (a *API) HandleLogData(w http.ResponseWriter, r *http.Request) {
|
||||
filename := r.URL.Query().Get("file")
|
||||
if filename == "" {
|
||||
@@ -400,6 +410,8 @@ func (a *API) HandleLogData(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// HandleHealth отдаёт статус сервера: связь с pipe (пороги 15 с и 3 с),
|
||||
// uptime, статистику буфера и уровень звука.
|
||||
func (a *API) HandleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
latest, ok := a.buf.GetLatest()
|
||||
idle := time.Since(a.buf.LastWriteTime())
|
||||
@@ -429,6 +441,7 @@ func (a *API) HandleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// HandleLatest отдаёт последний байт и до 10 предыдущих (от старых к новым).
|
||||
func (a *API) HandleLatest(w http.ResponseWriter, r *http.Request) {
|
||||
latest, ok := a.buf.GetLatest()
|
||||
if !ok {
|
||||
@@ -442,6 +455,7 @@ func (a *API) HandleLatest(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// HandleHistory отдаёт до 300 последних байт для графика (от старых к новым).
|
||||
func (a *API) HandleHistory(w http.ResponseWriter, r *http.Request) {
|
||||
raw := a.buf.GetLast(300)
|
||||
|
||||
@@ -455,6 +469,9 @@ func (a *API) HandleHistory(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// HandleStream отдаёт статус видеопотока камеры. Внимание: доступность
|
||||
// проверяется по захардкоженному localhost:1984 без учёта флага -camera-url
|
||||
// (известное поведение, docs/tech-debt.md).
|
||||
func (a *API) HandleStream(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверяем доступность MJPEG потока в go2rtc
|
||||
resp, err := http.Get("http://localhost:1984/api/streams?src=cam_mjpeg")
|
||||
@@ -470,7 +487,8 @@ func (a *API) HandleStream(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// Прокси для MJPEG потока — просто ретранслирует готовый поток из go2rtc
|
||||
// handleCamProxy — НЕ ИСПОЛЬЗУЕТСЯ: роутер вызывает handleCamProxyWithURL,
|
||||
// этот вариант с захардкоженным URL остался как мёртвый код (docs/tech-debt.md).
|
||||
func handleCamProxy(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[cam] запрос от %s", r.RemoteAddr)
|
||||
|
||||
@@ -519,6 +537,8 @@ func handleCamProxy(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// cors разрешает кросс-доменные GET-запросы с любых источников
|
||||
// (рассчитано на доверенную локальную сеть).
|
||||
func cors(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
@@ -703,7 +723,8 @@ func main() {
|
||||
log.Fatal(http.ListenAndServe(*serverPort, nil))
|
||||
}
|
||||
|
||||
// handleCamProxyWithURL проксирует MJPEG поток с указанным URL
|
||||
// handleCamProxyWithURL проксирует MJPEG-поток с cameraURL (флаг -camera-url),
|
||||
// ретранслируя заголовки и сбрасывая буфер после каждого чанка 32 КБ.
|
||||
func handleCamProxyWithURL(w http.ResponseWriter, r *http.Request, cameraURL string) {
|
||||
log.Printf("[cam] запрос от %s к %s", r.RemoteAddr, cameraURL)
|
||||
|
||||
|
||||
@@ -1,37 +1,56 @@
|
||||
# GPIO Monitor Web Interface
|
||||
|
||||
Веб-интерфейс для мониторинга GPIO и видеопотока с камеры.
|
||||
Веб-интерфейс дашборда GPIO Monitor: мониторинг в реальном времени, просмотр исторических логов, видеопоток камеры. Подробная документация фронтенда — [docs/frontend.md](../../../docs/frontend.md).
|
||||
|
||||
## Требования
|
||||
|
||||
- Node.js 20.x или выше
|
||||
- npm 10.x или выше
|
||||
- Go 1.21
|
||||
|
||||
Node.js нужен **только для компиляции TypeScript** — в рантайме фронтенд не имеет зависимостей и раздаётся Go-сервером из встроенных файлов.
|
||||
|
||||
### 1. Установка Node.js и npm на Raspberry Pi
|
||||
|
||||
#### Добавление официального репозитория NodeSource
|
||||
```curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -```
|
||||
|
||||
#### Установка Node.js и npm
|
||||
```sudo apt install -y nodejs```
|
||||
|
||||
#### Проверка установки
|
||||
```node --version # v20.20.2```
|
||||
```npm --version # 10.8.2```
|
||||
|
||||
### 2.Инициализация npm проекта
|
||||
|
||||
#### Переход в директорию с веб-файлами
|
||||
```cd ~/temp/golang/cmd/server/web```
|
||||
|
||||
#### Инициализация package.json
|
||||
```npm init -y```
|
||||
|
||||
#### Установка TypeScript и типов Node.js
|
||||
```npm install -D typescript @types/node```
|
||||
## Установка Node.js на Raspberry Pi
|
||||
|
||||
```bash
|
||||
chmod +x node_modules/.bin/tsc
|
||||
```
|
||||
# Официальный репозиторий NodeSource
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
|
||||
sudo apt install -y nodejs
|
||||
|
||||
# Проверка
|
||||
node --version # v20.x
|
||||
npm --version # 10.x
|
||||
```
|
||||
|
||||
## Сборка
|
||||
|
||||
`package.json` и `tsconfig.json` уже в репозитории — инициализировать проект не нужно, только установить зависимости:
|
||||
|
||||
```bash
|
||||
cd cmd/server/web
|
||||
npm install # однократно
|
||||
npm run build # tsc: js/*.ts → dist/*.js
|
||||
npm run watch # пересборка при изменениях
|
||||
npm run clean # удалить dist/
|
||||
```
|
||||
|
||||
Проще из корня проекта: `make build-frontend` (сам выполнит `npm install` при необходимости).
|
||||
|
||||
> **Важно:** каталог `dist/` не хранится в git, но обязателен для сборки Go-сервера (`go:embed`). Всегда собирайте фронтенд до `go build` — подробности в [docs/development.md](../../../docs/development.md#порядок-сборки).
|
||||
|
||||
## Структура
|
||||
|
||||
```
|
||||
web/
|
||||
├── js/ # TypeScript-исходники (10 модулей, см. docs/frontend.md)
|
||||
├── dist/ # Скомпилированный JS (генерируется tsc)
|
||||
├── css/ # Стили
|
||||
├── fonts/ # Шрифты
|
||||
├── index.html # Редирект на дашборд
|
||||
├── dashboard.html # Мониторинг в реальном времени
|
||||
├── logs.html # Просмотр исторических логов
|
||||
├── favicon.png
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Карта модулей `js/`, взаимодействие с API и цикл разработки — [docs/frontend.md](../../../docs/frontend.md). Спецификация API сервера — [docs/api.md](../../../docs/api.md).
|
||||
|
||||
251
docs/api.md
Normal file
251
docs/api.md
Normal 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 (0–255) | Последний сырой байт 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 (0–100) | Уровень звука с микрофона; `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` — массив целых (0–255), до **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` — биты 6–7 (0–3); `signal` — биты 0–5 (0–63); `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
150
docs/architecture.md
Normal 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 и публикует уровень 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)).
|
||||
169
docs/data-formats.md
Normal file
169
docs/data-formats.md
Normal 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
|
||||
(0–3) (0–63)
|
||||
```
|
||||
|
||||
| Поле | Биты | Диапазон | Смысл |
|
||||
|---|---|---|---|
|
||||
| `count` (в API — `signal`) | 0–5 | 0–63 | Количество обнаружений |
|
||||
| `strength` (в API — `amplitude`) | 6–7 | 0–3 | Сила сигнала |
|
||||
|
||||
Текстовые имена уровней силы (`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:00–12: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
115
docs/development.md
Normal 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
79
docs/frontend.md
Normal 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 (сетка, шкала 0–63) |
|
||||
| `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
255
docs/operations.md
Normal 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
59
docs/tech-debt.md
Normal 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:297–298` ↔ `internal/logger/parser.go` | Риск рассинхронизации формата | Использовать `ParseGPIO` |
|
||||
| 5 | `/api/log/data` читает весь `.bin`-файл в память до пагинации | `main.go:278–308` | На больших файлах — всплеск памяти на каждый запрос | Читать нужный диапазон по смещению (записи фиксированные, 9 байт) |
|
||||
| 6 | CORS открыт для всех источников (`*`) | `main.go:522–535` | Приемлемо для изолированной сети; риск при выходе наружу | Осознанное решение зафиксировано; при необходимости — 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:77–162` | Значение `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:66–70` | Потеря данных при полном диске останется незамеченной | Прокинуть 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:249–256` | Цели заведомо падают | Удалить цели или добавить 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 (актуален/эксперимент/заморожен) |
|
||||
@@ -1,3 +1,5 @@
|
||||
// Package adapter содержит кольцевой буфер последних принятых байт GPIO —
|
||||
// источник данных для HTTP API (/api/latest, /api/history, /api/health).
|
||||
package adapter
|
||||
|
||||
import (
|
||||
@@ -6,6 +8,9 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// RingBuffer — потокобезопасный кольцевой буфер байт фиксированного размера.
|
||||
// Помимо самих данных ведёт статистику приёма: время последней записи,
|
||||
// суммарные счётчики и текущую скорость (байт/с и бит/с).
|
||||
type RingBuffer struct {
|
||||
mu sync.RWMutex
|
||||
data []byte
|
||||
@@ -34,7 +39,7 @@ type timestampedByte struct {
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
// =====================
|
||||
// NewRingBuffer создаёт буфер на size элементов.
|
||||
func NewRingBuffer(size int) *RingBuffer {
|
||||
rb := &RingBuffer{
|
||||
data: make([]byte, size),
|
||||
@@ -48,7 +53,8 @@ func NewRingBuffer(size int) *RingBuffer {
|
||||
return rb
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Write добавляет байт в буфер (при переполнении затирается самый старый)
|
||||
// и обновляет статистику: счётчики, время последней записи, скорость.
|
||||
func (rb *RingBuffer) Write(b byte) {
|
||||
rb.mu.Lock()
|
||||
|
||||
@@ -98,7 +104,8 @@ func (rb *RingBuffer) updateSpeedPeriodically(now time.Time) {
|
||||
rb.lastCalcBytes = currentTotal
|
||||
}
|
||||
|
||||
// Скользящее окно
|
||||
// addTimestampedByte ведёт скользящее окно временных меток за последнюю секунду
|
||||
// и пересчитывает по нему скорость (перетирая значение периодического механизма).
|
||||
func (rb *RingBuffer) addTimestampedByte(now time.Time) {
|
||||
rb.recentMu.Lock()
|
||||
defer rb.recentMu.Unlock()
|
||||
@@ -131,7 +138,8 @@ func (rb *RingBuffer) addTimestampedByte(now time.Time) {
|
||||
}
|
||||
}
|
||||
|
||||
// Получить скорость на основе скользящего окна (альтернативный метод)
|
||||
// GetWindowSpeed возвращает скорость приёма, рассчитанную по скользящему окну
|
||||
// в 1 секунду. Альтернатива значениям из Stats; текущим API не используется.
|
||||
func (rb *RingBuffer) GetWindowSpeed() (bytesPerSec, bitsPerSec float64) {
|
||||
rb.recentMu.Lock()
|
||||
defer rb.recentMu.Unlock()
|
||||
@@ -161,12 +169,12 @@ func (rb *RingBuffer) GetWindowSpeed() (bytesPerSec, bitsPerSec float64) {
|
||||
return
|
||||
}
|
||||
|
||||
// =====================
|
||||
// LastWriteTime возвращает время последней записи в буфер.
|
||||
func (rb *RingBuffer) LastWriteTime() time.Time {
|
||||
return time.Unix(0, rb.lastWrite.Load())
|
||||
}
|
||||
|
||||
// =====================
|
||||
// GetLatest возвращает последний записанный байт; false — если буфер пуст.
|
||||
func (rb *RingBuffer) GetLatest() (byte, bool) {
|
||||
rb.mu.RLock()
|
||||
defer rb.mu.RUnlock()
|
||||
@@ -183,7 +191,7 @@ func (rb *RingBuffer) GetLatest() (byte, bool) {
|
||||
return rb.data[idx], true
|
||||
}
|
||||
|
||||
// =====================
|
||||
// GetLast возвращает до n последних байт в порядке от старых к новым.
|
||||
func (rb *RingBuffer) GetLast(n int) []byte {
|
||||
rb.mu.RLock()
|
||||
defer rb.mu.RUnlock()
|
||||
@@ -207,12 +215,13 @@ func (rb *RingBuffer) GetLast(n int) []byte {
|
||||
return res
|
||||
}
|
||||
|
||||
// =====================
|
||||
// IsAlive сообщает, была ли запись в буфер за последние timeout.
|
||||
func (rb *RingBuffer) IsAlive(timeout time.Duration) bool {
|
||||
return time.Since(time.Unix(0, rb.lastWrite.Load())) < timeout
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Stats возвращает статистику буфера для /api/health. Ключи: size, filled,
|
||||
// last_write, total_bytes, total_bits, bytes_per_sec, bits_per_sec.
|
||||
func (rb *RingBuffer) Stats() map[string]interface{} {
|
||||
rb.mu.RLock()
|
||||
filled := rb.count
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Package audio измеряет уровень звука с микрофона через arecord (ALSA):
|
||||
// USB-устройство находится автоматически, PCM-поток читается блоками по 100 мс,
|
||||
// уровень считается как RMS и публикуется по шкале 0–100 (поле sound_level
|
||||
// в /api/health). Падение arecord не фатально — захват перезапускается.
|
||||
package audio
|
||||
|
||||
import (
|
||||
@@ -15,6 +19,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Monitor — фоновый измеритель уровня звука с микрофона.
|
||||
type Monitor struct {
|
||||
mu sync.RWMutex
|
||||
currentLevel int
|
||||
@@ -22,16 +27,21 @@ type Monitor struct {
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewMonitor создаёт монитор; захват запускается отдельным вызовом Start.
|
||||
func NewMonitor() *Monitor {
|
||||
return &Monitor{}
|
||||
}
|
||||
|
||||
// GetLevel возвращает текущий уровень звука по шкале 0–100
|
||||
// (RMS/32768 × 100 × 4 с отсечкой сверху); 0 — если захват не работает.
|
||||
func (m *Monitor) GetLevel() int {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.currentLevel
|
||||
}
|
||||
|
||||
// Start запускает захват звука в фоне. Ошибка (микрофон недоступен) не
|
||||
// критична для вызывающего кода — сервер продолжает работать без звука.
|
||||
func (m *Monitor) Start() error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m.cancel = cancel
|
||||
@@ -156,6 +166,8 @@ func (m *Monitor) handleCrash(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// findUSBDevice выбирает ALSA-устройство по выводу arecord -l: сначала USB-аудио,
|
||||
// затем любое устройство захвата, иначе "default".
|
||||
func (m *Monitor) findUSBDevice() string {
|
||||
cmd := exec.Command("arecord", "-l")
|
||||
output, err := cmd.Output()
|
||||
@@ -201,6 +213,7 @@ func (m *Monitor) findUSBDevice() string {
|
||||
return "default"
|
||||
}
|
||||
|
||||
// Stop останавливает захват и дожидается завершения горутины чтения.
|
||||
func (m *Monitor) Stop() {
|
||||
if m.cancel != nil {
|
||||
m.cancel()
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Package logger реализует хранение данных GPIO: разбор байта, бинарные
|
||||
// почасовые логи с ротацией, человекочитаемые и событийные логи, watchdog
|
||||
// тишины и retention (автоочистку старых файлов). Пути хранения зависят от
|
||||
// режима работы (deb-пакет или разработка) и вычисляются в paths.go.
|
||||
package logger
|
||||
|
||||
import "time"
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// DataLogger пишет сэмплы в бинарный файл с буферизацией: буфер 64 КБ
|
||||
// сбрасывается раз в секунду либо при заполнении, после сброса — fsync.
|
||||
type DataLogger struct {
|
||||
file *os.File
|
||||
mu sync.Mutex
|
||||
@@ -16,11 +18,14 @@ type DataLogger struct {
|
||||
closeCh chan struct{}
|
||||
}
|
||||
|
||||
// Sample — одно измерение: метка времени в микросекундах Unix и сырой байт GPIO.
|
||||
type Sample struct {
|
||||
Timestamp int64 // microseconds
|
||||
Value byte
|
||||
}
|
||||
|
||||
// NewDataLogger открывает файл basePath на дозапись и запускает фоновый
|
||||
// цикл сброса буфера. Именование файлов и смену часа обеспечивает RotatingLogger.
|
||||
func NewDataLogger(basePath string) (*DataLogger, error) {
|
||||
// путь будет формироваться через rotation
|
||||
f, err := os.OpenFile(basePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
@@ -40,6 +45,8 @@ func NewDataLogger(basePath string) (*DataLogger, error) {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Write добавляет сэмпл в буфер в формате 9 байт:
|
||||
// uint64 little-endian (микросекунды Unix) + 1 байт значения.
|
||||
func (d *DataLogger) Write(s Sample) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
@@ -91,6 +98,7 @@ func (d *DataLogger) flushLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
// Close останавливает цикл сброса, дописывает остаток буфера и закрывает файл.
|
||||
func (d *DataLogger) Close() error {
|
||||
close(d.closeCh)
|
||||
d.flushTick.Stop()
|
||||
|
||||
@@ -8,12 +8,16 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// EventLogger пишет системные события параллельно в два файла: events.log
|
||||
// (JSON-строки для машинной обработки) и events_human.log (текст; его читает
|
||||
// /api/log/events). Словарь событий — docs/data-formats.md.
|
||||
type EventLogger struct {
|
||||
file *os.File
|
||||
humanFile *os.File
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewEventLogger открывает оба файла событий на дозапись.
|
||||
func NewEventLogger() (*EventLogger, error) {
|
||||
// Получаем пути к файлам
|
||||
jsonPath, err := GetEventLogPath()
|
||||
@@ -45,6 +49,7 @@ func NewEventLogger() (*EventLogger, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Event записывает событие name в оба файла с текущим временем и sync.
|
||||
func (l *EventLogger) Event(name string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
@@ -74,6 +79,7 @@ func (l *EventLogger) Event(name string) {
|
||||
l.humanFile.Sync()
|
||||
}
|
||||
|
||||
// Close закрывает оба файла событий.
|
||||
func (l *EventLogger) Close() error {
|
||||
l.file.Close()
|
||||
l.humanFile.Close()
|
||||
|
||||
@@ -5,11 +5,14 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// HumanLogger пишет человекочитаемый лог GPIO (gpio_human.log) с немедленным
|
||||
// sync после каждой строки. Частоту записей регулирует вызывающий код (PipeReader).
|
||||
type HumanLogger struct {
|
||||
file *os.File
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewHumanLogger открывает gpio_human.log на дозапись.
|
||||
func NewHumanLogger() (*HumanLogger, error) {
|
||||
path, err := GetGPIOLogPath()
|
||||
if err != nil {
|
||||
@@ -24,6 +27,7 @@ func NewHumanLogger() (*HumanLogger, error) {
|
||||
return &HumanLogger{file: f}, nil
|
||||
}
|
||||
|
||||
// Write дописывает готовую строку в лог.
|
||||
func (l *HumanLogger) Write(data string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
@@ -32,6 +36,7 @@ func (l *HumanLogger) Write(data string) {
|
||||
l.file.Sync()
|
||||
}
|
||||
|
||||
// Close закрывает файл лога.
|
||||
func (l *HumanLogger) Close() error {
|
||||
return l.file.Close()
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Monitor — watchdog потока данных: PipeReader отмечает каждую запись через
|
||||
// RecordWrite, фоновый цикл сравнивает длительность тишины с порогами и пишет
|
||||
// события ТИШИНА_5МИН/ТИШИНА_10МИН.
|
||||
type Monitor struct {
|
||||
eventLogger *EventLogger
|
||||
lastWrite time.Time
|
||||
@@ -14,6 +17,7 @@ type Monitor struct {
|
||||
watchdogInterval time.Duration
|
||||
}
|
||||
|
||||
// NewMonitor создаёт watchdog с порогами из config и сразу запускает фоновый цикл.
|
||||
func NewMonitor(eventLogger *EventLogger, config *Config) *Monitor {
|
||||
m := &Monitor{
|
||||
eventLogger: eventLogger,
|
||||
@@ -34,6 +38,7 @@ func (m *Monitor) SetSilenceThresholds(silence5Min, silence10Min time.Duration)
|
||||
m.silence10Min = silence10Min
|
||||
}
|
||||
|
||||
// RecordWrite отмечает факт приёма данных (сбрасывает отсчёт тишины).
|
||||
func (m *Monitor) RecordWrite() {
|
||||
m.mu.Lock()
|
||||
m.lastWrite = time.Now()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package logger
|
||||
|
||||
// GPIOData — разобранный байт GPIO-шины: количество обнаружений (биты 0–5),
|
||||
// сила сигнала (биты 6–7) и признак превышения порога тревоги.
|
||||
type GPIOData struct {
|
||||
RawValue byte // сырое значение
|
||||
Count byte // количество обнаружений (биты 0-5)
|
||||
|
||||
@@ -14,6 +14,9 @@ import (
|
||||
// возраст или размер говорят удалить. Защита от полного стирания директории.
|
||||
const minKeepFiles = 2
|
||||
|
||||
// Retention — фоновая FIFO-очистка бинарных логов по двум независимым лимитам:
|
||||
// возрасту (maxAgeHours) и суммарному размеру (maxSizeBytes). Любой лимит
|
||||
// отключается нулём; минимум minKeepFiles свежих файлов сохраняется всегда.
|
||||
type Retention struct {
|
||||
maxAgeHours int
|
||||
maxSizeBytes int64
|
||||
@@ -21,6 +24,7 @@ type Retention struct {
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
// NewRetention создаёт очистку с лимитами; запуск — Start или StartWithInterval.
|
||||
func NewRetention(maxAgeHours int, maxSizeMB int, eventLogger *EventLogger) *Retention {
|
||||
return &Retention{
|
||||
maxAgeHours: maxAgeHours,
|
||||
@@ -30,11 +34,13 @@ func NewRetention(maxAgeHours int, maxSizeMB int, eventLogger *EventLogger) *Ret
|
||||
}
|
||||
}
|
||||
|
||||
// Start запускает очистку со стандартным интервалом 15 минут.
|
||||
func (r *Retention) Start() {
|
||||
// Запускаем проверку каждые 15 минут
|
||||
r.StartWithInterval(15 * time.Minute)
|
||||
}
|
||||
|
||||
// StartWithInterval запускает фоновый цикл очистки с заданным интервалом;
|
||||
// первая очистка выполняется через 1 минуту после запуска.
|
||||
func (r *Retention) StartWithInterval(interval time.Duration) {
|
||||
// Запускаем проверку с указанным интервалом
|
||||
ticker := time.NewTicker(interval)
|
||||
@@ -56,10 +62,13 @@ func (r *Retention) StartWithInterval(interval time.Duration) {
|
||||
})
|
||||
}
|
||||
|
||||
// Stop останавливает фоновый цикл очистки.
|
||||
func (r *Retention) Stop() {
|
||||
close(r.stopCh)
|
||||
}
|
||||
|
||||
// Cleanup выполняет один проход очистки: сначала по возрасту, затем по размеру.
|
||||
// Удаления фиксируются суммарным событием ОЧИСТКА_ЛОГОВ_УДАЛЕНО_N_ФАЙЛОВ_M_MB.
|
||||
func (r *Retention) Cleanup() {
|
||||
// Если оба лимита отключены, ничего не делаем
|
||||
if r.maxAgeHours <= 0 && r.maxSizeBytes <= 0 {
|
||||
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// RotatingLogger — обёртка над DataLogger с почасовой ротацией: держит файл
|
||||
// текущего часа (gpio-YYYY-MM-DD-HH.bin) и по таймеру переоткрывает его при
|
||||
// смене часа, фиксируя событие ПЛАНОВАЯ_РОТАЦИЯ.
|
||||
type RotatingLogger struct {
|
||||
dataLogger *DataLogger
|
||||
currentHour int
|
||||
@@ -16,6 +19,8 @@ type RotatingLogger struct {
|
||||
checkInterval time.Duration
|
||||
}
|
||||
|
||||
// NewRotatingLogger открывает файл текущего часа в каталоге бинарных данных
|
||||
// и запускает цикл проверки ротации (checkInterval; 0 — раз в минуту).
|
||||
func NewRotatingLogger(eventLogger *EventLogger, checkInterval time.Duration) (*RotatingLogger, error) {
|
||||
// Получаем директорию для бинарных данных
|
||||
dataDir, err := GetDataLogsDir()
|
||||
@@ -83,6 +88,7 @@ func (r *RotatingLogger) rotate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write передаёт сэмпл текущему DataLogger.
|
||||
func (r *RotatingLogger) Write(s Sample) {
|
||||
r.mu.Lock()
|
||||
logger := r.dataLogger
|
||||
@@ -102,6 +108,7 @@ func (r *RotatingLogger) rotationLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
// Close закрывает текущий DataLogger (с финальным сбросом буфера).
|
||||
func (r *RotatingLogger) Close() error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Package pipe читает поток байт GPIO из именованного канала (FIFO) и раздаёт
|
||||
// их потребителям: кольцевому буферу, бинарному и текстовым логгерам, watchdog.
|
||||
package pipe
|
||||
|
||||
import (
|
||||
@@ -10,6 +12,10 @@ import (
|
||||
"gpio-monitor/internal/logger"
|
||||
)
|
||||
|
||||
// PipeReader — единственный «производитель» данных в системе: следит за FIFO,
|
||||
// переподключается при обрывах и раздаёт каждый принятый байт потребителям.
|
||||
// Ведёт машину состояний пайпа (unknown/found/not_found/error/disconnected)
|
||||
// и пишет события смены состояния без дублей.
|
||||
type PipeReader struct {
|
||||
buf *adapter.RingBuffer
|
||||
dataLogger *logger.RotatingLogger
|
||||
@@ -31,6 +37,8 @@ type PipeReader struct {
|
||||
lastLoggedState string // для отслеживания изменений
|
||||
}
|
||||
|
||||
// NewPipeReader создаёт читателя с настройками анти-спама из config.
|
||||
// Любой из потребителей (dataLogger, humanLogger, monitor, eventLog) может быть nil.
|
||||
func NewPipeReader(
|
||||
buf *adapter.RingBuffer,
|
||||
dataLogger *logger.RotatingLogger,
|
||||
@@ -63,6 +71,9 @@ func (pr *PipeReader) logStateChange(newState string, eventName string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Start запускает чтение pipePath в отдельной горутине и сразу возвращается.
|
||||
// Если FIFO отсутствует — ждёт его появления (проверка каждые 2 с); при обрыве
|
||||
// чтения переоткрывает файл через 1 с. Останавливается по отмене ctx.
|
||||
func (pr *PipeReader) Start(ctx context.Context, pipePath string) {
|
||||
go func() {
|
||||
buffer := make([]byte, 4096)
|
||||
@@ -184,6 +195,8 @@ func (pr *PipeReader) Start(ctx context.Context, pipePath string) {
|
||||
}()
|
||||
}
|
||||
|
||||
// handleAlert пишет алерт превышения (count > 10) в event- и human-логи,
|
||||
// не чаще одного раза в alertCooldownSec для одинакового значения count.
|
||||
func (pr *PipeReader) handleAlert(data logger.GPIOData, timestamp time.Time) {
|
||||
// Anti-spam: не чаще 1 алерта в N секунд для одинакового количества
|
||||
key := data.Count
|
||||
|
||||
Reference in New Issue
Block a user