Files
fgl-aircon/src/ayla/session.hpp
Petr Polezhaev bfdf22ba64 core(M2): машина состояний сессии Ayla LAN + mock-модуль + интеграционные сценарии
- session.{hpp,cpp}: state machine (idle/registering/online/recovering/
  offline/key_error); httpd-обработчики key_exchange (200/426/412, re-key
  прозрачно), commands (одна команда, 206/200, envelope, глобальный seq_no),
  datapoint (unpack -> PropertyEvent / 401+тишина 50с для re-key-восстановления);
  сессионный поток: local_reg POST?dsn/PUT (local_ip_for), keep-alive, backoff
  x1.6->60с, 503->offline/NoSlot, activation-timeout->recovering, delete_session
  с ожиданием выдачи; очередь с coalescing + batch; телеметрия; колбэки из
  двух потоков с задокументированным контрактом; буферы datapoint-пути в Impl.
- platform: local_ip_for (UDP-connect) posix+esp-idf; стек httpd 24576
  (переполнение 16КБ поймано gdb на Release).
- mock_ac.py: мок-модуль, stdlib-only чистый python AES-256 (свёрстан с
  pycryptodome); сценарии: 503, no-poll, rekey-every, stale-gap (эмуляция
  'вернувшегося' приложения), fail-pushes (битая подпись), garbage-pushes
  (обрыв блока), break-outbound (исходящий десинк -> модуль ре-кает на
  local_reg, как probe1-3), push-every, fail-first-ke.
- session_runner + test_session_mock.py: 9 сценариев через ctest, включая
  самосинхронизацию CBC и восстановление после исходящего десинка.
- Прибор AP-WC1E: активация <=1с; re-key семантика ИСПРАВЛЕНА по живым
  тестам: re-key при зазоре local_reg >= ~44-50с (не по возрасту сессии!);
  при честном keep-alive 15с сессия стабильна без re-key; PROTOCOL/LEGACY/
  PLAN обновлены; восстановление = тишина >порога + возврат.
- CI: 7/7 x3 (gcc-Rel, gcc-ASan/UBSan, clang); ESP-IDF esp32 build complete.
Ревью под-агентом: 2 круга (стек httpd, залипание состояний, dangling cfg,
физика десинка) — APPROVED.
2026-09-27 10:53:28 +03:00

127 lines
5.5 KiB
C++
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.
// Сессия Ayla LAN (сторона «приложения»). docs/PROTOCOL.md §4-6, §4.4.
// Потоки: httpd (входящие от модуля: key exchange/commands/datapoint) и
// session (исходящие local_reg, таймеры, backoff). Крипто-цепочки и выдача
// команд — только в httpd-потоке; session-поток читает очередь под mutex.
#pragma once
#include <atomic>
#include <cstddef>
#include <cstdint>
#include "ayla/crypto.hpp"
#include "ayla/httpd.hpp"
namespace fgl::ayla {
enum class SessionState : uint8_t {
kIdle = 0, // создан, не запущен
kRegistering, // local_reg отправлен, ждём key exchange
kOnline, // сессия активна
kRecovering, // ожидание самолечения (re-key по keep-alive / активация)
kOffline, // модуль недоступен (backoff) или нет слотов
kKeyError, // lanip_key_id не совпал — требуется смена конфига
};
enum class SessionError : int {
kNone = 0,
kNoSlot = 1, // 503: оба слота модуля заняты
kUnreachable = 2, // transport/backoff
kKeyMismatch = 3, // key_id != lanip_key_id (state = kKeyError)
kBadKeyExchange = 4, // ver/proto/sec не поддержаны
kActivationTimeout = 5,// KE прошёл, опроса commands.json нет (>5 c)
kDecryptFailed = 6, // подпись/расшифровка push не сошлись (ждём re-key)
};
// Событие обновления свойства (push модуля). Значение — какой-то один тип.
struct PropertyEvent {
char name[40];
bool is_int = false;
int64_t int_value = 0;
bool is_bool = false;
bool bool_value = false;
char str_value[64]; // используется, если !is_int && !is_bool
int cmd_id = -1; // из ?cmd_id=N (ответ на GET), иначе -1
int status = 0; // из ?status=200
int64_t seq_no = 0;
};
struct SessionConfig {
const char* host = nullptr; // DNS-имя или IP модуля
uint16_t device_port = 80; // порт local_reg модуля
const char* dsn = nullptr; // "AC000W00XXXXXXX"
const char* lanip_key = nullptr; // base64-строка как есть
uint32_t lanip_key_id = 0;
uint16_t listen_port = 10275; // 0 — любой свободный
uint32_t keepalive_ms = 15000;
uint8_t max_queue = 16; // лимит очереди команд
};
struct SessionCallbacks {
// КОНТРАКТ: колбэки приходят из потоков ядра (httpd и/или session),
// возможно перекрытие во времени; обязаны быть быстрыми и реентерабельными.
// Вызывать stop() из колбэка запрещено (deadlock на join).
void (*on_state)(void* ctx, SessionState st, SessionError err);
void (*on_property)(void* ctx, const PropertyEvent& ev);
void* ctx = nullptr;
};
// Тайминги поведения (PROTOCOL §4.3-4.4; проверено на приборе).
struct SessionTimings {
uint32_t keepalive_ms = 15000; // период local_reg
uint32_t activation_timeout_ms = 5000; // нет poll после KE
uint32_t recovering_quiet_ms = 50000; // пауза при десинке/«KE без poll»
// (> порога возврата модуля ~44-50с)
uint32_t no_slot_retry_ms = 60000; // повтор после 503
uint32_t backoff_base_ms = 1000; // transport backoff, шаг x1.6
uint32_t backoff_max_ms = 60000;
uint8_t backoff_attempts = 6;
uint32_t delete_wait_ms = 2000;
};
class Session {
public:
static Session* create(const SessionConfig& cfg, const SessionCallbacks& cbs);
~Session();
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
bool start();
// Штатное завершение: DELETE-команда + local_reg notify, ожидание выдачи,
// остановка потоков. state -> kIdle.
void stop();
SessionState state() const;
SessionError last_error() const;
uint16_t listen_port() const;
// Телеметрия (диагностика).
uint32_t rekey_count() const;
uint32_t pushes_ok() const;
uint32_t pushes_bad() const;
uint32_t commands_served() const;
bool had_activity() const;
// ---- Команды (потокобезопасны; кладутся в очередь с coalescing) ----
// GET-команда: свойство придёт on_property (cmd_id совпадает).
bool get_property(const char* name);
// SET-команда (integer/boolean как int64).
bool set_property(const char* name, int64_t value,
const char* base_type = "integer");
// Пакет: собрать несколько команд, один notify на commit.
bool begin_batch();
bool commit_batch();
bool abort_batch();
// DELETE local_reg.json/delete_session (для stop() и ручного завершения).
bool delete_session();
// Тест-хук: тайминги. ТОЛЬКО до start() (после — читаются потоками ядра).
void set_timings_for_test(const SessionTimings& t);
private:
Session(const SessionConfig& cfg, const SessionCallbacks& cbs);
struct Impl;
Impl* impl_;
};
} // namespace fgl::ayla