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:
2026-09-27 10:53:28 +03:00
parent 345fe19ca7
commit e74f3dc67a
15 changed files with 2009 additions and 26 deletions

View File

@@ -0,0 +1,135 @@
// Тестовый раннер сессии: поднимает fgl::ayla::Session против mock_ac.py
// (или реального модуля) и печатает события строками в stdout:
// STATE <state> <err> — смена состояния
// PROP <name> <cmd_id> <status> <kind:value> — push свойства
// DELETED — delete_session забран модулем
// STATS ... — телеметрия (перед выходом)
// Управление окружением:
// RUNNER_KEEPALIVE_MS, RUNNER_ACTIVATION_MS, RUNNER_QUIET_MS,
// RUNNER_NOSLOT_RETRY_MS, RUNNER_DELETE_WAIT_MS
// RUNNER_SET_NAME/VALUE/AT — одиночная SET через AT секунд после старта.
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <thread>
#include "ayla/session.hpp"
using fgl::ayla::PropertyEvent;
using fgl::ayla::Session;
using fgl::ayla::SessionError;
using fgl::ayla::SessionState;
static const char* state_name(SessionState st) {
switch (st) {
case SessionState::kIdle: return "idle";
case SessionState::kRegistering: return "registering";
case SessionState::kOnline: return "online";
case SessionState::kRecovering: return "recovering";
case SessionState::kOffline: return "offline";
case SessionState::kKeyError: return "key_error";
}
return "?";
}
static uint32_t env_u32(const char* name, uint32_t def) {
const char* v = getenv(name);
return v != nullptr ? static_cast<uint32_t>(atoi(v)) : def;
}
static void on_state(void*, SessionState st, SessionError err) {
printf("STATE %s %d\n", state_name(st), static_cast<int>(err));
fflush(stdout);
}
static void on_property(void*, const PropertyEvent& ev) {
if (ev.is_int) {
printf("PROP %s %d %d i:%lld\n", ev.name, ev.cmd_id, ev.status,
static_cast<long long>(ev.int_value));
} else if (ev.is_bool) {
printf("PROP %s %d %d b:%d\n", ev.name, ev.cmd_id, ev.status,
ev.bool_value ? 1 : 0);
} else {
printf("PROP %s %d %d s:%s\n", ev.name, ev.cmd_id, ev.status, ev.str_value);
}
fflush(stdout);
}
int main(int argc, char** argv) {
if (argc < 7) {
fprintf(stderr,
"usage: %s <host> <device_port> <listen_port> <dsn> <lanip_key> "
"<key_id> <duration_sec> [prop ...]\n",
argv[0]);
return 2;
}
fgl::ayla::SessionConfig cfg{};
cfg.host = argv[1];
cfg.device_port = static_cast<uint16_t>(atoi(argv[2]));
cfg.listen_port = static_cast<uint16_t>(atoi(argv[3]));
cfg.dsn = argv[4];
cfg.lanip_key = argv[5];
cfg.lanip_key_id = static_cast<uint32_t>(atoi(argv[6]));
int duration_sec = atoi(argv[7]);
fgl::ayla::SessionCallbacks cbs{};
cbs.on_state = on_state;
cbs.on_property = on_property;
Session* s = Session::create(cfg, cbs);
if (s == nullptr) {
fprintf(stderr, "create failed\n");
return 2;
}
fgl::ayla::SessionTimings timings{};
timings.keepalive_ms = env_u32("RUNNER_KEEPALIVE_MS", timings.keepalive_ms);
timings.activation_timeout_ms =
env_u32("RUNNER_ACTIVATION_MS", timings.activation_timeout_ms);
timings.recovering_quiet_ms =
env_u32("RUNNER_QUIET_MS", timings.recovering_quiet_ms);
timings.no_slot_retry_ms = env_u32("RUNNER_NOSLOT_RETRY_MS",
timings.no_slot_retry_ms);
timings.delete_wait_ms = env_u32("RUNNER_DELETE_WAIT_MS",
timings.delete_wait_ms);
s->set_timings_for_test(timings);
if (!s->start()) {
fprintf(stderr, "start failed\n");
return 2;
}
// Начальная синхронизация: пакет GET всех свойств.
if (argc > 8) {
s->begin_batch();
for (int i = 8; i < argc; i++) {
s->get_property(argv[i]);
}
s->commit_batch();
}
const char* set_name = getenv("RUNNER_SET_NAME");
const char* set_val = getenv("RUNNER_SET_VALUE");
uint32_t set_at = env_u32("RUNNER_SET_AT", 0);
if (set_name != nullptr && set_val != nullptr && set_at > 0) {
std::this_thread::sleep_for(std::chrono::seconds(set_at));
s->set_property(set_name, atoll(set_val));
printf("SET_DONE %s=%s\n", set_name, set_val);
fflush(stdout);
}
auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(duration_sec);
while (std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
s->stop(); // внутри: delete_session + ожидание выдачи
printf("DELETED\n");
printf("STATS rekeys=%u pushes_ok=%u pushes_bad=%u cmds=%u state=%s\n",
s->rekey_count(), s->pushes_ok(), s->pushes_bad(),
s->commands_served(), state_name(s->state()));
fflush(stdout);
delete s;
return 0;
}