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).
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// C-API (extern "C") fgl-aircon — для cffi/pyfglair (Home Assistant).
|
||||
// C++-пользователям — fgl-aircon/*.hpp.
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Версия библиотеки.
|
||||
const char* fgl_version(void);
|
||||
|
||||
// Логирование.
|
||||
typedef void (*fgl_log_sink_fn)(int level, const char* msg, unsigned len,
|
||||
void* ctx);
|
||||
void fgl_log_set_sink(fgl_log_sink_fn sink, void* ctx);
|
||||
void fgl_log_set_level(int min_level);
|
||||
|
||||
// Состояния/ошибки (значения совпадают с fgl::aircon::State/Error).
|
||||
typedef enum {
|
||||
FGL_STATE_IDLE = 0,
|
||||
FGL_STATE_REGISTERING = 1,
|
||||
FGL_STATE_ONLINE = 2,
|
||||
FGL_STATE_RECOVERING = 3,
|
||||
FGL_STATE_OFFLINE = 4,
|
||||
FGL_STATE_KEY_ERROR = 5,
|
||||
} fgl_state_t;
|
||||
|
||||
typedef enum {
|
||||
FGL_ERR_NONE = 0,
|
||||
FGL_ERR_NO_SLOT = 1,
|
||||
FGL_ERR_UNREACHABLE = 2,
|
||||
FGL_ERR_KEY_MISMATCH = 3,
|
||||
FGL_ERR_BAD_KEY_EXCHANGE = 4,
|
||||
FGL_ERR_ACTIVATION_TIMEOUT = 5,
|
||||
FGL_ERR_DECRYPT_FAILED = 6,
|
||||
} fgl_error_t;
|
||||
|
||||
typedef enum {
|
||||
FGL_PROP_OPERATION_MODE = 0,
|
||||
FGL_PROP_FAN_SPEED = 1,
|
||||
FGL_PROP_ADJUST_TEMPERATURE = 2,
|
||||
FGL_PROP_DISPLAY_TEMPERATURE = 3,
|
||||
FGL_PROP_AF_VERTICAL_DIRECTION = 4,
|
||||
FGL_PROP_AF_VERTICAL_SWING = 5,
|
||||
FGL_PROP_AF_HORIZONTAL_DIRECTION = 6,
|
||||
FGL_PROP_AF_HORIZONTAL_SWING = 7,
|
||||
FGL_PROP_AF_VERTICAL_MOVE_STEP1 = 8,
|
||||
FGL_PROP_AF_HORIZONTAL_MOVE_STEP1 = 9,
|
||||
FGL_PROP_OUTDOOR_LOW_NOISE = 10,
|
||||
FGL_PROP_INDOOR_FAN_CONTROL = 11,
|
||||
FGL_PROP_HUMAN_DET_AUTO_SAVE = 12,
|
||||
FGL_PROP_MIN_HEAT = 13,
|
||||
FGL_PROP_POWERFUL_MODE = 14,
|
||||
FGL_PROP_COIL_DRY_MODE = 15,
|
||||
FGL_PROP_ECONOMY_MODE = 16,
|
||||
FGL_PROP_MASTER_TIMER_ON_OFF_1 = 17,
|
||||
FGL_PROP_MASTER_TIMER_ON_OFF_2 = 18,
|
||||
FGL_PROP_ERROR_CODE = 19,
|
||||
FGL_PROP_DEMAND_CONTROL = 20,
|
||||
FGL_PROP_FILTER_SIGN_RESET_DISPLAY = 21,
|
||||
FGL_PROP_FILTER_SIGN_RESET = 22,
|
||||
FGL_PROP_OP_STATUS = 23,
|
||||
FGL_PROP_DEVICE_NAME = 24,
|
||||
FGL_PROP_BUILDING_NAME = 25,
|
||||
FGL_PROP_WIFI_LED_ENABLE = 26,
|
||||
FGL_PROP_SERVICE_CONTACT_NAME = 27,
|
||||
FGL_PROP_SERVICE_CONTACT_PHONE = 28,
|
||||
FGL_PROP_SERVICE_CONTACT_EMAIL = 29,
|
||||
FGL_PROP_AF_HORIZONTAL_NUM_DIR = 30,
|
||||
FGL_PROP_AF_VERTICAL_NUM_DIR = 31,
|
||||
FGL_PROP_DEVICE_CAPABILITIES = 32,
|
||||
FGL_PROP_GET_PROP = 33,
|
||||
FGL_PROP_HUMAN_DET = 34,
|
||||
FGL_PROP_MONITOR1 = 35,
|
||||
FGL_PROP_REFRESH = 36,
|
||||
FGL_PROP_COUNT = 37,
|
||||
} fgl_prop_t;
|
||||
|
||||
typedef enum {
|
||||
FGL_VALUE_INT = 0,
|
||||
FGL_VALUE_BOOL = 1,
|
||||
FGL_VALUE_STRING = 2,
|
||||
} fgl_value_kind_t;
|
||||
|
||||
typedef struct {
|
||||
fgl_value_kind_t kind;
|
||||
int64_t i;
|
||||
char s[64];
|
||||
} fgl_value_t;
|
||||
|
||||
typedef enum {
|
||||
FGL_TEMPLATE_A = 0,
|
||||
FGL_TEMPLATE_B = 1,
|
||||
FGL_TEMPLATE_F = 2,
|
||||
} fgl_template_t;
|
||||
|
||||
// Конфигурация. Строки копируются при создании.
|
||||
typedef struct {
|
||||
const char* host;
|
||||
uint16_t device_port;
|
||||
const char* dsn;
|
||||
const char* lanip_key;
|
||||
uint32_t lanip_key_id;
|
||||
fgl_template_t tmpl;
|
||||
uint16_t listen_port;
|
||||
uint32_t keepalive_ms;
|
||||
uint8_t max_queue;
|
||||
} fgl_config_t;
|
||||
|
||||
typedef struct {
|
||||
fgl_prop_t prop;
|
||||
int32_t cmd_id; // -1 — спонтанное обновление
|
||||
int32_t status;
|
||||
fgl_value_t value;
|
||||
} fgl_property_event_t;
|
||||
|
||||
typedef void (*fgl_on_state_fn)(void* ctx, fgl_state_t st, fgl_error_t err);
|
||||
typedef void (*fgl_on_property_fn)(void* ctx,
|
||||
const fgl_property_event_t* ev);
|
||||
|
||||
typedef struct {
|
||||
fgl_on_state_fn on_state;
|
||||
fgl_on_property_fn on_property;
|
||||
void* ctx;
|
||||
} fgl_callbacks_t;
|
||||
|
||||
// Непрозрачная сессия (определение — в реализации).
|
||||
typedef struct fgl_session fgl_session_t;
|
||||
|
||||
fgl_session_t* fgl_session_create(const fgl_config_t* cfg,
|
||||
const fgl_callbacks_t* cbs);
|
||||
void fgl_session_destroy(fgl_session_t* s);
|
||||
int fgl_session_start(fgl_session_t* s); // 1 ok / 0 ошибка
|
||||
void fgl_session_stop(fgl_session_t* s);
|
||||
fgl_state_t fgl_session_state(const fgl_session_t* s);
|
||||
fgl_error_t fgl_session_last_error(const fgl_session_t* s);
|
||||
|
||||
int fgl_session_set_int(fgl_session_t* s, fgl_prop_t prop, int64_t value);
|
||||
int fgl_session_set_bool(fgl_session_t* s, fgl_prop_t prop, int value);
|
||||
int fgl_session_set_string(fgl_session_t* s, fgl_prop_t prop,
|
||||
const char* value);
|
||||
int fgl_session_get_prop(fgl_session_t* s, fgl_prop_t prop);
|
||||
|
||||
int fgl_session_batch_begin(fgl_session_t* s);
|
||||
int fgl_session_batch_commit(fgl_session_t* s);
|
||||
int fgl_session_batch_abort(fgl_session_t* s);
|
||||
|
||||
int fgl_session_cached(const fgl_session_t* s, fgl_prop_t prop,
|
||||
fgl_value_t* out);
|
||||
|
||||
// Интроспекция шаблонов (превью HA, CLI).
|
||||
int fgl_template_detect(const char* oem_model); // -1 — неизвестно
|
||||
const char* fgl_prop_name(fgl_template_t t, fgl_prop_t prop); // NULL — нет
|
||||
const char* fgl_prop_base_type(fgl_template_t t, fgl_prop_t prop);
|
||||
int fgl_prop_read_only(fgl_template_t t, fgl_prop_t prop); // -1 — нет
|
||||
int64_t fgl_convert_to_display(fgl_template_t t, fgl_prop_t prop,
|
||||
int64_t raw);
|
||||
int64_t fgl_convert_from_input(fgl_template_t t, fgl_prop_t prop,
|
||||
int64_t api_value);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
@@ -0,0 +1,63 @@
|
||||
// Публичная сессия fgl-aircon: обёртка над ayla-сессией с таблицами свойств
|
||||
// шаблонов, конверсиями и кэшем значений.
|
||||
#pragma once
|
||||
|
||||
#include "fgl-aircon/templates.hpp"
|
||||
#include "fgl-aircon/types.hpp"
|
||||
|
||||
namespace fgl::aircon {
|
||||
|
||||
// Глобальный приёмник логов ядра (nullptr — отключено). Быстрый, реентерабельный.
|
||||
// КОНТРАКТ: вызывать ДО create() сессий; не менять при живых сессиях.
|
||||
using LogSink = void (*)(int level, const char* msg, size_t len, void* ctx);
|
||||
void set_log_sink(LogSink sink, void* ctx);
|
||||
void set_log_level(int min_level); // 0=debug..3=error, default 1
|
||||
|
||||
class Session {
|
||||
public:
|
||||
// Создаёт сессию (одноразовые аллокации). nullptr при некорректном конфиге.
|
||||
static Session* create(const Config& cfg, const Callbacks& cbs);
|
||||
~Session();
|
||||
|
||||
Session(const Session&) = delete;
|
||||
Session& operator=(const Session&) = delete;
|
||||
|
||||
// Запуск (httpd + сессионный поток). false — ошибка bind/listen.
|
||||
bool start();
|
||||
// Штатное завершение: DELETE-команда + notify, ожидание выдачи ≤2с.
|
||||
void stop();
|
||||
|
||||
State state() const;
|
||||
Error last_error() const;
|
||||
|
||||
// ---- Управление свойствами (потокобезопасны; очередь с coalescing) ----
|
||||
// set_int/set_bool: read-only свойства и свойства не из шаблона → false.
|
||||
// Значение клампится по диапазону (override), конверсия from_input.
|
||||
bool set_int(Prop prop, int64_t value);
|
||||
bool set_bool(Prop prop, bool value);
|
||||
bool set_string(Prop prop, const char* value); // только строковые
|
||||
|
||||
// Запросить обновление (ответ придёт on_property с cmd_id).
|
||||
bool get_prop(Prop prop);
|
||||
|
||||
// Пакет команд: один local_reg notify=1 на commit.
|
||||
bool batch_begin();
|
||||
bool batch_commit();
|
||||
bool batch_abort();
|
||||
|
||||
// Кэш значений (обновляется push'ами; set_* — оптимистично).
|
||||
bool cached(Prop prop, Value* out) const;
|
||||
|
||||
// Телеметрия.
|
||||
uint32_t rekey_count() const;
|
||||
uint32_t pushes_ok() const;
|
||||
uint32_t pushes_bad() const;
|
||||
uint32_t commands_served() const;
|
||||
|
||||
private:
|
||||
Session();
|
||||
struct Impl;
|
||||
Impl* impl_;
|
||||
};
|
||||
|
||||
} // namespace fgl::aircon
|
||||
@@ -0,0 +1,58 @@
|
||||
// Интроспекция шаблонов и конверсии (для HA-превью, ESPHome, CLI, тестов).
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "fgl-aircon/types.hpp"
|
||||
|
||||
namespace fgl::aircon {
|
||||
|
||||
// Описание свойства в шаблоне.
|
||||
struct PropInfo {
|
||||
Prop prop;
|
||||
const char* name; // имя в протоколе ("operation_mode")
|
||||
const char* base_type; // "integer" | "boolean" | "string"
|
||||
bool read_only;
|
||||
ValueKind kind;
|
||||
int64_t raw_min; // диапазон raw (справочно, из таблиц приложения)
|
||||
int64_t raw_max;
|
||||
};
|
||||
|
||||
// Список свойств шаблона (constexpr-таблица; kCount-терминатора нет —
|
||||
// используйте prop_info_count).
|
||||
const PropInfo* prop_info_begin(Template t);
|
||||
size_t prop_info_count(Template t);
|
||||
|
||||
// Поиск по enum (в шаблоне). nullptr — свойство не входит в шаблон.
|
||||
const PropInfo* prop_info_find(Template t, Prop prop);
|
||||
|
||||
// Поиск по имени протокола (в шаблоне). nullptr — не найдено.
|
||||
const PropInfo* prop_info_find_by_name(Template t, const char* name);
|
||||
|
||||
// Имя enum-значения свойства ("OperationMode") — для логов/CLI.
|
||||
const char* prop_enum_name(Prop prop);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Конверсии. По умолчанию (kTemplateDefault):
|
||||
// - почти все свойства — тождественны (API-значение == raw);
|
||||
// - DisplayTemperature: raw 0.01°C со смещением 5000 → API 0.1°C:
|
||||
// api = (raw - 5000) / 10 [округление к нулю; raw=7300 → 230 → 23.0°C]
|
||||
// Override (linear/custom) применяется поверх (план §4).
|
||||
// КОНТРАКТ Linear (при прямом вызове без Session::create): num != 0 и
|
||||
// den != 0 — Session::create это валидирует; самодельные списки override'ов
|
||||
// для прямых вызовов конверсий должны удовлетворять тому же.
|
||||
// ---------------------------------------------------------------------------
|
||||
int64_t convert_to_display(Template t, Prop prop, int64_t raw,
|
||||
const PropOverride* overrides);
|
||||
// Обратная конверсия (для set_*). kLinear в from_input — ИНВЕРСИЯ той же
|
||||
// спецификации: raw = (api - offset) * den / num (задавайте те же
|
||||
// коэффициенты, что и в to_display).
|
||||
int64_t convert_from_input(Template t, Prop prop, int64_t api_value,
|
||||
const PropOverride* overrides);
|
||||
|
||||
// Диапазон API-значений с учётом override (false — диапазона нет).
|
||||
bool prop_api_range(Template t, Prop prop, const PropOverride* overrides,
|
||||
int64_t* min, int64_t* max);
|
||||
|
||||
} // namespace fgl::aircon
|
||||
@@ -0,0 +1,167 @@
|
||||
// Публичные типы 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
|
||||
Reference in New Issue
Block a user