# Архитектура системы

## Политика управления устройствами

**Ни веб-клиент, ни сами умные устройства не имеют права напрямую управлять другими устройствами через внешний REST API.**

Всё управление устройствами — только через скрипты (`ControlScripts`). Скрипты являются единственной точкой логики автоматизации. У них три механики взаимодействия с системой:

| Механика | Описание | Как запускается |
|----------|---------|----------------|
| **Events** | Реакция на события — в том числе события от устройств (`button_press`, `presence_changed` и т.д.) | Автоматически, при получении события от устройства через `POST /events/new` |
| **Regular** | Периодические задачи | По cron через `GET /cron/regular-scripts` |
| **Actions** | Именованные операции, запускаемые явно | Через `POST /api/v1/scripts/actions/run` из клиента |

Это намеренная политика: бизнес-логика управления устройствами сосредоточена исключительно в `ControlScripts/Scopes/`, а не размазана по клиентскому коду или прошивкам.

---

## Три слоя

```
[ESP8266/ESP32 устройства]
        ↕ HTTP REST (локальная сеть, только сервер инициирует)
[PHP-сервер SHServ]
        ↕ HTTP REST (proxy.php)
[Веб-клиент (JS/SCSS)]
```

---

## Устройства (Firmware)

Каждое устройство — это Arduino-прошивка, собранная поверх библиотеки `sh_core_esp8266`.

### Роль sh_core

`devices/sh_core_esp8266/src/` — разделяемая библиотека для всех устройств. Она отвечает за:
- WiFi подключение и AP-режим при первом запуске
- HTTP-сервер и регистрацию стандартных роутов (`/about`, `/status`, `/action`, `/setup`, `/set_token`, `/reboot`, `/reset`, `/set_device_name`, `/channels_schema`, `/set_channels_schema`)
- EEPROM: хранение SSID/пароля, токена, адреса сервера, имени устройства, схемы каналов
- OTA-обновление
- Универсальный POST-хелпер для отправки событий на сервер: `core_post_json_to_server()`

### Контракт прошивки устройства

Каждый `.ino`-файл обязан реализовать 4 функции (они вызываются из `sh_core`):

```cpp
// Добавить поля в JSON-ответ /status
void appendStatusJsonFields(String &json);

// Добавить поля в JSON-ответ /about
void appendAboutJsonFields(String &json);

// Обработать входящее действие /action
bool deviceHandleAction(const String &action, const String &paramsJson,
                        String &errorCode, String &errorMessage);

// Выполнить сброс к заводским настройкам
void deviceHandleReset();
```

И задать три глобальных константы:
```cpp
const char* DEVICE_TYPE = "relay";   // тип устройства
const char* FW_VERSION  = "1.0";     // версия прошивки
const uint8_t CHANNEL_NUM = 8;       // число каналов (0 если не используются)
```

### EEPROM layout

| Адрес | Длина | Содержимое |
|-------|-------|-----------|
| 0 | 32 | SSID |
| 32 | 64 | WiFi password |
| 96 | 1 | Device mode (0=setup, 1=normal, 2=error, 3=updating) |
| 97 | 64 | Device name |
| 161 | 64 | Auth token |
| 225 | 32 | Server base URL |

### Виртуальный эмулятор (для разработки)

Для локальной разработки и тестирования без реального железа используется виртуальный эмулятор на Python + Flask (`tools/virtual_devices/`).

Эмулятор повторяет полный REST-контракт реальных устройств:
- `GET /about`, `GET /status`, `POST /action`
- `POST /set_token`, `POST /reset`, `POST /reboot`
- `GET /channels_schema`, `POST /set_channels_schema`

Поддерживаемые типы:
- **relay** — управление каналами (`set_state: on/off`)
- **button** — генерация событий (`POST /events/new` на сервер)

Регистрация в сервере происходит через стандартный API `POST /api/v1/devices/setup/new-device`. Сервер устанавливает токен через `/set_token` так же, как и для реального устройства.

Детальное руководство: `docs/virtual-device-emulator.md`.
| 257 | 36 | Channel schema (4 meta + 32 data) |
| 512–1023 | 512 | Свободно для устройства (`DEVICE_EEPROM_START`) |

### Каналы (channels schema)

Схема — 8 каналов × 4 байта:
- байт 0: GPIO пин канала (`SH_CH_PIN`)
- байт 1: GPIO пин индикатора или номер LED (`SH_CH_INDICATOR`)
- байт 2: GPIO пин обратной связи / концевика (`SH_CH_FEEDBACK`)
- байт 3: флаги — бит 0 (`SH_CH_FLAG_INVERT`) = инверсия канала

Значение `0xFF` (`SH_PIN_UNUSED`) означает «пин не используется».

### Режимы устройства

| Режим | Описание |
|-------|---------|
| `setup` | Не подключено к серверу. `/setup` и `/set_token` доступны без токена. |
| `normal` | Работает штатно. Все запросы требуют `Authorization: Bearer <token>`. |
| `error` | Ошибка. |
| `updating` | OTA-обновление. |

При переходе `setup → normal`: устройство запоминает IP сервера и принимает авторизованные запросы **только** с этого адреса.

---

## Сервер (SHServ)

PHP-приложение поверх собственного микрофреймворка **Fury**.

### Fury framework

`server/Fury/` содержит минималистичный MVC-фреймворк:
- **Bootstrap** — точка входа, инициализирует приложение через `events()`
- **Router** — маршрутизация через `.uri()`, `.get()`, `.post()`
- **ThinBuilder** — простой SQL query builder поверх PDO
- **Events** — простой pub/sub (`events()->handler(name, cb)`, `events()->app_call(name, data)`)
- **Template** — шаблонизатор (используется мало)

### Жизненный цикл запроса

1. `server/index.php` → `Fury\Kernel\Init` → `Fury\Kernel\Bootstrap`
2. Bootstrap генерирует событие `kernel:Bootstrap.ready_app`
3. `EventsHandlers` ловит событие → вызывает `routes->routes_init()` → `router->start_routing()`
4. Router находит совпадение и вызывает `Controller@method`

### Структура SHServ

```
SHServ/
├── App.php               — точка входа (new App()), инициализация
├── config.php            — DB, IP-диапазон устройств, devmode
├── Routes.php            — объединяет все trait-роуты
├── Routes/               — trait DevicesRESTAPI_v1, ScriptsRESTAPI_v1, AreasRESTAPI_v1, DevMode
├── Controllers/          — по одному контроллеру на группу роутов
├── Models/               — DB-слой (Devices, Areas, Scripts, EventsModel, ...)
├── Entities/             — объекты предметной области (Device, Area, Script, ...)
├── Middleware/           — базовые классы Controller, Model, Entity, ControlScripts
├── Helpers/              — DeviceScriptsHelper, MetaImplementation, Validator, ...
├── Tools/DeviceAPI/      — HTTP-клиенты к устройствам (Base, Relay, Button, Sensor, Hatch)
├── Tools/DeviceScanner.php — параллельное сканирование сети через curl_multi
└── Logs/                 — JSON-логи по дням
```

### Добавление устройства на сервер

1. `GET /api/v1/devices/scanning/setup` — сервер параллельно опрашивает диапазон IP (из `config.php`: `device_ip_range`), возвращает устройства в режиме `setup`
2. `POST /api/v1/devices/setup/new-device` — сервер:
   - Запрашивает `/about` у устройства
   - Создаёт запись в таблице `devices`
   - Генерирует токен, сохраняет в `device_auth`
   - Отправляет токен на устройство через `POST /set_token`
   - Устанавливает имя устройства через `POST /set_device_name`

### DeviceAPI (Tools/DeviceAPI/)

Серверная сторона HTTP-клиентов к устройствам:
- `Base` — базовые методы: `get_about()`, `get_status()`, `post_action()`, `remote_set_token()`, `reboot()`, `reset()`, `set_device_name()`
- `Relay extends Base` — `toggle_channel(ch)`, `set_state(bool)`, `set_channel_state(bool, ch)`
- `Button extends Base` — `get_indicators()`, `get_indicator_state(ch)`, `set_channel_state(mode, ch)`
- `Sensor extends Base`
- `Hatch extends Base`

`Device::device_api()` создаёт нужный экземпляр по `device_type` и автоматически подставляет токен из `device_auth`.

### Cron-маршруты

Два endpoint'а для запуска по cron:

| URL | Действие |
|-----|---------|
| `GET /cron/regular-scripts` | Запускает все зарегистрированные regular-скрипты (с проверкой флага enabled в БД) |
| `GET /cron/status-update-scanning` | Сканирует сеть, обновляет `connection_status` и `device_ip` устройств в БД |

---

## Система событий (Events от устройств)

### Отправка события устройством

Устройство отправляет событие на `POST /events/new`:

```json
{
  "device_id": "ecf0a1b5c9d74f9a8e294c1f67b0a8b9",
  "event_name": "press",
  "data": { "channel": 0 }
}
```

Требует `Authorization: Bearer <device_token>`.

### Жизненный цикл на сервере

```
[EventsController::new_event()]
    1. Найти устройство по device_hard_id
    2. Проверить auth (device_auth)
    3. Залогировать событие
    4. Обновить last_contact и connection_status
    5. Ответить 200 OK немедленно
    6. fastcgi_finish_request() — асинхронное продолжение
        ↓
[EventsModel] — триггерит 5 вариантов через Fury Events
```

**Почему 5 вариантов?** Одно физическое событие триггерит 5 имён — это позволяет подписываться на разном уровне детализации.

### Пять паттернов событий

Для `button`, alias `kitchen_btns`, канал 2, событие `press`:

| № | Паттерн | Имя события | Когда использовать |
|---|---------|-------------|-------------------|
| 1 | `{event_name}` | `press` | Глобально — любое устройство |
| 2 | `{device_type}.{event_name}` | `button.press` | Все устройства типа |
| 3 | `{device_type}@{alias}.{event_name}` | `button@kitchen_btns.press` | Конкретное устройство |
| 4 | `{device_type}({ch}).{event_name}` | `button(2).press` | Все устройства, канал 2 |
| 5 | `{device_type}@{alias}({ch}).{event_name}` | `button@kitchen_btns(2).press` | Точно: устройство + канал |

### EventsModel (server/SHServ/Models/EventsModel.php)

```php
// 1. Канал + alias + channel
$events_model->channel_alias_device_event_call($device, $event_name, $channel, $data);
// → button@kitchen_btns(2).press

// 2. Канал + type + channel
$events_model->channel_device_event_call($device, $event_name, $channel, $data);
// → button(2).press

// 3. Alias (все каналы)
$events_model->alias_device_event_call($device, $event_name, $data);
// → button@kitchen_btns.press

// 4. Тип устройства
$events_model->global_device_event_call($device, $event_name, $data);
// → button.press

// 5. Глобально (любое устройство)
$events_model->global_any_device_event_call($device, $event_name, $data);
// → press
```

Полное описание: `docs/events-from-devices.md`.

---

## Control Scripts (server/SHServ/Middleware/ControlScripts.php)

Скрипты автоматизации — это PHP-классы (Scope), расположенные в `server/ControlScripts/Scopes/` (или другом месте, если настроено иначе).

### Базовый класс

Все Scope-классы:
- Наследуют `\SHServ\Middleware\ControlScripts`
- Реализуют `\SHServ\Implements\ControlScriptsInterface`
- Автоматически загружаются при старте сервера

```php
class MyScope extends \SHServ\Middleware\ControlScripts 
               implements \SHServ\Implements\ControlScriptsInterface {

    public function register_sync_map(): void { ... }
    public function register_events_handlers(): void { ... }
    public function register_actions_scripts(): void { ... }
    public function register_regular_scripts(): void { ... }
}
```

### Четыре регистрационных метода

| Метод | Назначение | Когда вызывается |
|-------|------------|------------------|
| `register_sync_map()` | Связи реле ↔ кнопки для синхронизации индикаторов | При старте сервера |
| `register_events_handlers()` | Подписка на события от устройств | При старте сервера |
| `register_actions_scripts()` | Action-скрипты (ручной запуск) | При старте сервера |
| `register_regular_scripts()` | Regular-скрипты (периодические) | При старте сервера |

### Типы скриптов

**Action-скрипты** — ручной запуск через `POST /api/v1/scripts/actions/run`:
```php
$this->add_action_script([
    "alias"  => "kitchen_light_toggle",
    "name"   => "Свет на кухне",
    "icon"   => '<i class="ph ph-lightbulb"></i>',
], function($params) {
    $relay = $this->devices()->by_alias("kitchen_relay");
    $relay->device_api()->toggle_channel(0);
    return ["result" => true];
});
```

**Regular-скрипты** — периодический запуск через `GET /cron/regular-scripts`:
```php
$this->add_regular_script([
    "alias" => "check_door_sensor",
    "name"  => "Проверка датчика двери",
], function() {
    // периодическая логика
});
```

**Event-хендлеры** — реакция на события от устройств:
```php
// Конкретная кнопка, канал 1
$this->add_event_handler("button@kitchen_btns(1).press", function(Device $device, array $data) {
    $relay = $this->devices()->by_alias("kitchen_relay");
    $relay->device_api()->toggle_channel(0);
    $this->helper()->sync_relay_to_btns($this->sync_map(), "kitchen_relay");
});

// Устройство онлайн — синхронизировать индикаторы
$this->add_event_handler("button@kitchen_btns.online", function(Device $device, array $data) {
    $this->helper()->sync_btn_channels($this->sync_map(), $device->alias);
});
```

### Sync map

Декларативное описание связей «реле ↔ кнопки» для синхронизации индикаторов:

```php
public function register_sync_map(): void {
    $this->add_sync_connection([
        ["type" => "relay",  "alias" => "kitchen_relay", "channel" => 0],
        ["type" => "button", "alias" => "kitchen_btns",  "channel" => 1],
    ]);
}
```

**Хелперы синхронизации** (`DeviceScriptsHelper`):
- `sync_relay_to_btns($sync_map, $relay_alias)` — синхронизировать кнопки с реле
- `sync_btn_channels($sync_map, $btn_alias)` — синхронизировать индикаторы кнопки с реле

Полное описание: `docs/control-scripts-guide.md`, `docs/events-from-devices.md`.

---

## Веб-клиент

### Vue Client (webclient/) — основной

Современный SPA на Vue 3 + Pinia + Vite + vue-router (hash mode).

```
webclient/
├── index.php                 — entry point (serve dist/index.html)
├── vite.config.js            — Vite конфиг (dev server + build)
├── src/
│   ├── app/main.js           — Vue app entry (createApp + Pinia + router)
│   ├── router/routes.js      — hash-роуты
│   ├── router/index.js       — настройки роутера + навигация логи
│   ├── api/
│   │   ├── client.js         — базовый API клиент
│   │   ├── http.js           — HTTP слой (fetch, логирование)
│   │   ├── mappers.js        — маппинг DTO → domain
│   │   └── modules/          — devices, areas, scripts, scanning
│   ├── stores/               — Pinia stores (Composition API)
│   │   ├── devices.js
│   │   ├── areas.js
│   │   ├── scripts.js
│   │   └── favorites.js
│   ├── features/             — feature-модули (pages, components)
│   │   ├── devices/pages/
│   │   ├── areas/pages/
│   │   └── scripts/pages/
│   ├── components/           — общие UI компоненты
│   └── utils/logger.js       — централизованное логирование
└── dist/                     — production build
```

**Команды:**
```bash
cd webclient
npm run dev       # dev server с прокси на PHP
npm run build     # production build → dist/
npm test          # Vitest тесты
```

**Стилевые правила:**
- Использовать семантические классы `gnexus-ui-kit` (`.text-success`, `.badge-warning`, и т.д.)
- Не использовать CSS custom properties (`--color-*`) напрямую

### Legacy Client (webclient_legacy/) — архив

Vanilla JS + esbuild + SCSS. Поддерживается для обратной совместимости.

```
webclient_legacy/
├── src/js/
│   ├── index.js              — entry point
│   ├── sh/SmartHomeApi.js    — callback-style API клиент
│   ├── routes.js             — hash роутинг
│   └── components/           — UI компоненты (Screens, modals, toasts)
└── dist/                     — сборка через gulp
```

**Команды:**
```bash
cd webclient_legacy
npm start         # gulp: SCSS → CSS, JS bundle, live-reload
```
