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.
This commit is contained in:
126
src/ayla/session.hpp
Normal file
126
src/ayla/session.hpp
Normal file
@@ -0,0 +1,126 @@
|
||||
// Сессия 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
|
||||
Reference in New Issue
Block a user