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

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

**Ни веб-клиент, ни сами умные устройства не имеют права напрямую управлять другими устройствами через внешний 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 |
| 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": "<device_hard_id>",
  "event_name": "button_press",
  "data": { "channel": 0 }
}
```

Сервер (`EventsController`) ищет устройство по `device_hard_id`, проверяет авторизацию, немедленно отвечает `200 OK`, а затем (после `fastcgi_finish_request`) вызывает `EventsModel`, который триггерит события через Fury Events в нескольких форматах:

| Паттерн события | Пример | Описание |
|----------------|---------|---------|
| `{event_name}` | `button_press` | Глобальный — любое устройство |
| `{device_type}.{event_name}` | `button.button_press` | По типу устройства |
| `{device_type}@{alias}.{event_name}` | `button@kitchen_btns.button_press` | По alias устройства |
| `{device_type}({channel}).{event_name}` | `button(2).button_press` | По типу + каналу |
| `{device_type}@{alias}({channel}).{event_name}` | `button@kitchen_btns(2).button_press` | По alias + каналу (самый точный) |

---

## Control Scripts (ControlScripts/Scopes/)

Скрипты автоматизации — это PHP-классы в `server/ControlScripts/Scopes/`, наследующие `ControlScripts` и реализующие `ControlScriptsInterface`. Все Scope-файлы загружаются автоматически при старте приложения.

### Структура Scope-класса

```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 { ... }
}
```

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

**Action-скрипты** — вызываются вручную через API (`POST /api/v1/scripts/actions/run`):
```php
$this->add_action_script([
    "alias"  => "my_action",
    "name"   => "Имя",
    "icon"   => '<i class="ph ph-lightbulb"></i>',
    "author" => "Name"
], function($params) {
    // логика
    return ["result" => ...];
});
```

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

**Event-хендлеры** — реакция на события от устройств:
```php
$this->add_event_handler("button@{$alias}({$channel}).press", function(Device $device, array $data) {
    // логика
});
```

### Sync map

Декларативное описание связей «реле-канал ↔ кнопки-каналы» для синхронизации индикаторов:
```php
$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 и обновляет индикаторы кнопок при изменении состояния реле.

---

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

SPA на vanilla JS + esbuild. Роутинг через hash `#!/route`.

```
src/js/
├── index.js           — init: SmartHomeApi, Screens, routes()
├── routes.js          — привязка роутов к экранам
├── DataProvider.js    — простое key-value хранилище состояния (window.DataProvider)
├── sh/SmartHomeApi.js — HTTP-клиент (fetch + callback-style)
│   └── modules/       — DevicesApi, ScriptsApi, AreasApi
└── components/
    ├── Screens.js     — переключение экранов по hash
    ├── hud.js         — верхний HUD с навигацией
    ├── modals.js      — модальные окна
    ├── toasts.js      — уведомления
    └── screens/       — экраны devices, areas, scripts
```

### SmartHomeApi

Callback-style клиент. Все запросы идут через `proxy.php` (опция `proxy_path`):

```js
const api = new SmartHomeApi({
    base_url: API_BASEURL,
    token: "YOUR_TOKEN",
    proxy_path: "/proxy.php",
});

api.devices.list((err, data) => { ... });
api.scripts.run({ alias: "my_action" }, (err, data) => { ... });
```

Константа `API_BASEURL` инжектируется через esbuild при сборке (`webclient/gulpfile.js`).
