252 lines
12 KiB
Markdown
252 lines
12 KiB
Markdown
# Спецификация 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 открыт для всех источников (`*`) — рассчитано на доверенную локальную сеть.
|