Files
fgl-aircon/include/fgl-aircon/types.hpp
T
petr.polezhaev fa1718f405 core(M3): aircon-слой — шаблоны A/B/F, конверсии+override, публичный API + C-API
- include/fgl-aircon: types.hpp (Prop/State/Error/Value/Template/
  Conversion-linear-custom/PropOverride/Config с задокументированными
  lifetime-контрактами), templates.hpp (PropInfo-таблицы, интроспекция,
  конверсии c контрактом Linear num/den!=0), session.hpp (Session:
  set_int/bool/string, get_prop, batch, cached, set_log_sink с контрактом),
  c_api.h (extern "C" для cffi/pyfglair).
- src/aircon: tables.cpp (шаблоны A=33/B=20/F=34 по PROTOCOL §8.2,
  template_detect + template_is_known; конверсии: default тождественно,
  DisplayTemperature raw 0.01°C(+5000)→API 0.1°C; linear с инверсией
  raw=(api-offset)*den/num и насыщением __builtin_*_overflow; custom fn;
  af_*_swing base_type integer, kind bool), session.cpp (маппинг имя↔Prop,
  коэрсинг int-push к kBool по таблице, кэш со спинлоком, оптимистичный кэш,
  валидация overrides: терминатор/linear/custom_fn), c_api.cpp (шейм,
  static_assert'ы на все enum-значения, fgl_template_detect -1 для
  неизвестных).
- ayla: set_property_string (Command.str_value, coalescing копирует строку
  в т.ч. в batch-ветке); kMaxQueue 48 + static_assert (полный батч A=33);
  commit_batch всегда ставит notify; oversized-строка дропает событие
  (кэш не затирается); max_queue default 40.
- tests/aircon: test_tables (составы/атрибуты/дубли/template_detect/
  swings-integer), test_convert (default/linear/custom/диапазоны/насыщение
  в обе стороны/UB-экстремумы), aircon_runner + test_aircon_mock
  (маппинг+конверсии+batch=1-notify с ожиданием async REG; SET+кэш+
  RO/не-шаблон отказы+клэмпинг raw 450 на модуле; C-API smoke).
- Прибор AP-WC1E: полный батч шаблона A одним notify — 28 int/bool свойств
  (DisplayTemperature 7000→200=20.0°C, DeviceCapabilities=5119), boolean SET
  (JSON true) принят, RO/клэмп/NOTMPL отказы корректны.
- CI: 10/10 ×3 стабильно (3 полных прогона); ESP-IDF esp32 build complete.
Ревью независимым агентом: 3 круга — B1 kMaxQueue<батча, B2 linear div/0,
B3 c_api контракт -1, B4 batch-строки, BL1 INT64_MIN negation UB,
BL2 DisplayTemperature экстремумы, flake aircon_mock — всё закрыто;
APPROVED (условие круга 3: фикс + зелёный CI ×3).
2026-09-28 20:16:54 +03:00

168 lines
6.1 KiB
C++

// Публичные типы fgl-aircon (уровень aircon: свойства, конверсии, конфиг).
// Протокольные детали скрыты в src/ayla; этот заголовок — стабильный API
// для интеграций (ESPHome, HA/pyfglair, CLI).
#pragma once
#include <cstddef>
#include <cstdint>
namespace fgl::aircon {
// ---------------------------------------------------------------------------
// Состояние сессии (зеркалирует ayla-слой).
// ---------------------------------------------------------------------------
enum class State : uint8_t {
kIdle = 0, // создан, не запущен
kRegistering, // local_reg отправлен, ждём key exchange
kOnline, // сессия активна
kRecovering, // самолечение (тишина → re-key)
kOffline, // модуль недоступен (backoff) или нет слотов
kKeyError, // lanip_key_id не совпал — смена конфига вручную
};
enum class Error : int {
kNone = 0,
kNoSlot = 1,
kUnreachable = 2,
kKeyMismatch = 3,
kBadKeyExchange = 4,
kActivationTimeout = 5,
kDecryptFailed = 6,
};
// ---------------------------------------------------------------------------
// Свойства (шаблоны A/B/F; PROTOCOL.md §8.2)
// ---------------------------------------------------------------------------
enum class Prop : uint8_t {
OperationMode = 0,
FanSpeed,
AdjustTemperature,
DisplayTemperature,
AfVerticalDirection,
AfVerticalSwing,
AfHorizontalDirection,
AfHorizontalSwing,
AfVerticalMoveStep1,
AfHorizontalMoveStep1,
OutdoorLowNoise,
IndoorFanControl,
HumanDetAutoSave,
MinHeat,
PowerfulMode,
CoilDryMode,
EconomyMode,
MasterTimerOnOff1,
MasterTimerOnOff2,
ErrorCode,
DemandControl,
FilterSignResetDisplay,
FilterSignReset,
OpStatus,
DeviceName,
BuildingName,
WifiLedEnable,
ServiceContactName,
ServiceContactPhone,
ServiceContactEmail,
AfHorizontalNumDir,
AfVerticalNumDir,
DeviceCapabilities,
GetProp,
HumanDet,
Monitor1,
Refresh,
kCount
};
// Тип значения свойства.
enum class ValueKind : uint8_t { kInt = 0, kBool, kString };
struct Value {
ValueKind kind = ValueKind::kInt;
int64_t i = 0; // kInt / kBool (0|1)
char s[64] = {}; // kString
};
// ---------------------------------------------------------------------------
// Шаблоны устройств (oem_model → шаблон, PROTOCOL.md §8.1)
// ---------------------------------------------------------------------------
enum class Template : uint8_t { kA = 0, kB, kF };
// Определение шаблона по oem_model ("AP-WC1E" → kA). Неизвестные модели
// трактуются как kA (как и legacy) — различить можно template_is_known.
Template template_detect(const char* oem_model);
// Известна ли модель (false → template_detect вернёт kA как fallback).
bool template_is_known(const char* oem_model);
// ---------------------------------------------------------------------------
// Конверсии (PROTOCOL.md §8.3; требования плана §4)
// ---------------------------------------------------------------------------
enum class ConvKind : uint8_t {
kTemplateDefault = 0, // из таблицы шаблона
kLinear, // disp = raw*num/den + offset (int64, округление к нулю)
kCustomFn, // disp = fn(raw, ctx)
};
struct Linear {
int64_t num = 1;
int64_t den = 1;
int64_t offset = 0;
// to_display: api = raw*num/den + offset
// from_input: raw = (api - offset)*den/num (инверсия)
};
using ConvFn = int64_t (*)(int64_t raw, void* ctx);
struct Conversion {
ConvKind kind = ConvKind::kTemplateDefault;
Linear linear{}; // при kLinear
ConvFn fn = nullptr; // при kCustomFn
void* ctx = nullptr; // контекст kCustomFn
};
// Точечная настройка одного свойства (nullptr-terminnated массив в конфиге).
struct PropOverride {
Prop prop;
bool has_range = false;
int64_t min = 0; // в API-единицах (после конверсии)
int64_t max = 0;
Conversion to_display; // raw -> API
Conversion from_input; // API -> raw
};
// ---------------------------------------------------------------------------
// Конфигурация и колбэки сессии
// ---------------------------------------------------------------------------
struct Config {
// host/dsn/lanip_key копируются при create; overrides — нет (см. ниже).
const char* host = nullptr; // DNS-имя или IP модуля
uint16_t device_port = 80;
const char* dsn = nullptr; // "AC000W00XXXXXXX"
const char* lanip_key = nullptr; // base64-строка как есть
uint32_t lanip_key_id = 0;
Template tmpl = Template::kA;
uint16_t listen_port = 10275;
uint32_t keepalive_ms = 15000;
uint8_t max_queue = 40; // >= полного батча шаблона (33)
// Переопределения конверсий. МАССИВ ДОЛЖЕН ПЕРЕЖИТЬ СЕССИЮ (не копируется;
// читается при каждом set/push). Терминатор: элемент с prop == kCount.
const PropOverride* overrides = nullptr;
};
struct PropertyEvent {
Prop prop;
Value value; // уже сконвертированное (to_display)
int cmd_id = -1; // ответ на GET; -1 — спонтанное обновление
int status = 0;
};
struct Callbacks {
// КОНТРАКТ: вызываются из потоков ядра; быстрые и реентерабельные;
// вызывать stop() из колбэка запрещено.
void (*on_state)(void* ctx, State st, Error err);
void (*on_property)(void* ctx, const PropertyEvent& ev);
void* ctx = nullptr;
};
} // namespace fgl::aircon