Files
go-service/docs/api.md
2026-07-17 15:57:05 +03:00

252 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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