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,276 @@
|
||||
// Реализация extern "C" шейма (cffi/pyfglair). Значения enum'ов C-API
|
||||
// обязаны совпадать с C++ enum'ами — проверено static_assert'ами.
|
||||
#include "fgl-aircon/c_api.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
#include "fgl-aircon/session.hpp"
|
||||
#include "fgl-aircon/templates.hpp"
|
||||
#include "fgl-aircon/version.hpp"
|
||||
|
||||
using fgl::aircon::Callbacks;
|
||||
using fgl::aircon::Config;
|
||||
using fgl::aircon::Error;
|
||||
using fgl::aircon::Prop;
|
||||
using fgl::aircon::PropInfo;
|
||||
using fgl::aircon::State;
|
||||
using fgl::aircon::Template;
|
||||
using fgl::aircon::Value;
|
||||
|
||||
static_assert(FGL_STATE_IDLE == static_cast<int>(State::kIdle));
|
||||
static_assert(FGL_STATE_REGISTERING == static_cast<int>(State::kRegistering));
|
||||
static_assert(FGL_STATE_ONLINE == static_cast<int>(State::kOnline));
|
||||
static_assert(FGL_STATE_RECOVERING == static_cast<int>(State::kRecovering));
|
||||
static_assert(FGL_STATE_OFFLINE == static_cast<int>(State::kOffline));
|
||||
static_assert(FGL_STATE_KEY_ERROR == static_cast<int>(State::kKeyError));
|
||||
static_assert(FGL_ERR_NONE == static_cast<int>(Error::kNone));
|
||||
static_assert(FGL_ERR_NO_SLOT == static_cast<int>(Error::kNoSlot));
|
||||
static_assert(FGL_ERR_UNREACHABLE == static_cast<int>(Error::kUnreachable));
|
||||
static_assert(FGL_ERR_KEY_MISMATCH == static_cast<int>(Error::kKeyMismatch));
|
||||
static_assert(FGL_ERR_BAD_KEY_EXCHANGE == static_cast<int>(Error::kBadKeyExchange));
|
||||
static_assert(FGL_ERR_ACTIVATION_TIMEOUT == static_cast<int>(Error::kActivationTimeout));
|
||||
static_assert(FGL_ERR_DECRYPT_FAILED == static_cast<int>(Error::kDecryptFailed));
|
||||
static_assert(FGL_PROP_OPERATION_MODE == static_cast<int>(Prop::OperationMode));
|
||||
static_assert(FGL_PROP_FAN_SPEED == static_cast<int>(Prop::FanSpeed));
|
||||
static_assert(FGL_PROP_ADJUST_TEMPERATURE == static_cast<int>(Prop::AdjustTemperature));
|
||||
static_assert(FGL_PROP_DISPLAY_TEMPERATURE == static_cast<int>(Prop::DisplayTemperature));
|
||||
static_assert(FGL_PROP_ECONOMY_MODE == static_cast<int>(Prop::EconomyMode));
|
||||
static_assert(FGL_PROP_ERROR_CODE == static_cast<int>(Prop::ErrorCode));
|
||||
static_assert(FGL_PROP_DEVICE_CAPABILITIES == static_cast<int>(Prop::DeviceCapabilities));
|
||||
static_assert(FGL_PROP_REFRESH == static_cast<int>(Prop::Refresh));
|
||||
static_assert(FGL_PROP_COUNT == static_cast<int>(Prop::kCount));
|
||||
static_assert(FGL_TEMPLATE_A == static_cast<int>(Template::kA));
|
||||
static_assert(FGL_TEMPLATE_B == static_cast<int>(Template::kB));
|
||||
static_assert(FGL_TEMPLATE_F == static_cast<int>(Template::kF));
|
||||
static_assert(FGL_VALUE_INT == static_cast<int>(fgl::aircon::ValueKind::kInt));
|
||||
static_assert(FGL_VALUE_BOOL == static_cast<int>(fgl::aircon::ValueKind::kBool));
|
||||
static_assert(FGL_VALUE_STRING ==
|
||||
static_cast<int>(fgl::aircon::ValueKind::kString));
|
||||
|
||||
const char* fgl_version(void) { return FGL_AIRCON_VERSION_STRING; }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Логирование
|
||||
// ---------------------------------------------------------------------------
|
||||
namespace {
|
||||
struct LogWrap {
|
||||
fgl_log_sink_fn fn;
|
||||
void* ctx;
|
||||
};
|
||||
LogWrap g_log_wrap;
|
||||
|
||||
void log_wrap_trampoline(int level, const char* msg, size_t len, void*) {
|
||||
if (g_log_wrap.fn != nullptr) {
|
||||
g_log_wrap.fn(level, msg, len, g_log_wrap.ctx);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void fgl_log_set_sink(fgl_log_sink_fn sink, void* ctx) {
|
||||
g_log_wrap.fn = sink;
|
||||
g_log_wrap.ctx = ctx;
|
||||
fgl::aircon::set_log_sink(sink != nullptr ? log_wrap_trampoline : nullptr,
|
||||
nullptr);
|
||||
}
|
||||
|
||||
void fgl_log_set_level(int min_level) {
|
||||
fgl::aircon::set_log_level(min_level);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Сессия
|
||||
// ---------------------------------------------------------------------------
|
||||
namespace {
|
||||
|
||||
// Контексты колбэков, хранимые в fgl_session.
|
||||
struct CbCtx {
|
||||
fgl_on_state_fn on_state;
|
||||
fgl_on_property_fn on_property;
|
||||
void* user;
|
||||
};
|
||||
|
||||
void on_state_trampoline(void* ctx, State st, Error err) {
|
||||
auto* c = static_cast<CbCtx*>(ctx);
|
||||
if (c->on_state != nullptr) {
|
||||
c->on_state(c->user, static_cast<fgl_state_t>(st),
|
||||
static_cast<fgl_error_t>(err));
|
||||
}
|
||||
}
|
||||
|
||||
void on_property_trampoline(void* ctx,
|
||||
const fgl::aircon::PropertyEvent& ev) {
|
||||
auto* c = static_cast<CbCtx*>(ctx);
|
||||
if (c->on_property == nullptr) return;
|
||||
fgl_property_event_t out{};
|
||||
out.prop = static_cast<fgl_prop_t>(ev.prop);
|
||||
out.cmd_id = ev.cmd_id;
|
||||
out.status = ev.status;
|
||||
out.value.kind = static_cast<fgl_value_kind_t>(ev.value.kind);
|
||||
out.value.i = ev.value.i;
|
||||
memcpy(out.value.s, ev.value.s, sizeof(out.value.s));
|
||||
c->on_property(c->user, &out);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct fgl_session {
|
||||
fgl::aircon::Session* s;
|
||||
CbCtx* cbctx;
|
||||
};
|
||||
|
||||
fgl_session_t* fgl_session_create(const fgl_config_t* cfg,
|
||||
const fgl_callbacks_t* cbs) {
|
||||
if (cfg == nullptr) return nullptr;
|
||||
Config cc{};
|
||||
cc.host = cfg->host;
|
||||
cc.device_port = cfg->device_port;
|
||||
cc.dsn = cfg->dsn;
|
||||
cc.lanip_key = cfg->lanip_key;
|
||||
cc.lanip_key_id = cfg->lanip_key_id;
|
||||
cc.tmpl = static_cast<Template>(cfg->tmpl);
|
||||
cc.listen_port = cfg->listen_port;
|
||||
cc.keepalive_ms = cfg->keepalive_ms;
|
||||
cc.max_queue = cfg->max_queue;
|
||||
// Конверсии-override через C-API не задаются (применяются на стороне
|
||||
// pyfglair при превью/вычислениях; сессия работает в единицах API).
|
||||
|
||||
auto* cbctx = new (std::nothrow) CbCtx{};
|
||||
if (cbctx == nullptr) return nullptr;
|
||||
cbctx->on_state = cbs != nullptr ? cbs->on_state : nullptr;
|
||||
cbctx->on_property = cbs != nullptr ? cbs->on_property : nullptr;
|
||||
cbctx->user = cbs != nullptr ? cbs->ctx : nullptr;
|
||||
|
||||
Callbacks cpp_cbs{};
|
||||
cpp_cbs.on_state = on_state_trampoline;
|
||||
cpp_cbs.on_property = on_property_trampoline;
|
||||
cpp_cbs.ctx = cbctx;
|
||||
|
||||
auto* s = fgl::aircon::Session::create(cc, cpp_cbs);
|
||||
if (s == nullptr) {
|
||||
delete cbctx;
|
||||
return nullptr;
|
||||
}
|
||||
auto* out = new (std::nothrow) fgl_session_t();
|
||||
if (out == nullptr) {
|
||||
delete s;
|
||||
delete cbctx;
|
||||
return nullptr;
|
||||
}
|
||||
out->s = s;
|
||||
out->cbctx = cbctx;
|
||||
return out;
|
||||
}
|
||||
|
||||
void fgl_session_destroy(fgl_session_t* s) {
|
||||
if (s == nullptr) return;
|
||||
delete s->s;
|
||||
delete s->cbctx;
|
||||
delete s;
|
||||
}
|
||||
|
||||
int fgl_session_start(fgl_session_t* s) {
|
||||
return (s != nullptr && s->s->start()) ? 1 : 0;
|
||||
}
|
||||
|
||||
void fgl_session_stop(fgl_session_t* s) {
|
||||
if (s != nullptr) s->s->stop();
|
||||
}
|
||||
|
||||
fgl_state_t fgl_session_state(const fgl_session_t* s) {
|
||||
return s != nullptr ? static_cast<fgl_state_t>(s->s->state())
|
||||
: FGL_STATE_IDLE;
|
||||
}
|
||||
|
||||
fgl_error_t fgl_session_last_error(const fgl_session_t* s) {
|
||||
return s != nullptr ? static_cast<fgl_error_t>(s->s->last_error())
|
||||
: FGL_ERR_NONE;
|
||||
}
|
||||
|
||||
int fgl_session_set_int(fgl_session_t* s, fgl_prop_t prop, int64_t value) {
|
||||
return (s != nullptr && s->s->set_int(static_cast<Prop>(prop), value)) ? 1
|
||||
: 0;
|
||||
}
|
||||
|
||||
int fgl_session_set_bool(fgl_session_t* s, fgl_prop_t prop, int value) {
|
||||
return (s != nullptr &&
|
||||
s->s->set_bool(static_cast<Prop>(prop), value != 0))
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
|
||||
int fgl_session_set_string(fgl_session_t* s, fgl_prop_t prop,
|
||||
const char* value) {
|
||||
return (s != nullptr &&
|
||||
s->s->set_string(static_cast<Prop>(prop), value))
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
|
||||
int fgl_session_get_prop(fgl_session_t* s, fgl_prop_t prop) {
|
||||
return (s != nullptr && s->s->get_prop(static_cast<Prop>(prop))) ? 1 : 0;
|
||||
}
|
||||
|
||||
int fgl_session_batch_begin(fgl_session_t* s) {
|
||||
return (s != nullptr && s->s->batch_begin()) ? 1 : 0;
|
||||
}
|
||||
int fgl_session_batch_commit(fgl_session_t* s) {
|
||||
return (s != nullptr && s->s->batch_commit()) ? 1 : 0;
|
||||
}
|
||||
int fgl_session_batch_abort(fgl_session_t* s) {
|
||||
return (s != nullptr && s->s->batch_abort()) ? 1 : 0;
|
||||
}
|
||||
|
||||
int fgl_session_cached(const fgl_session_t* s, fgl_prop_t prop,
|
||||
fgl_value_t* out) {
|
||||
if (s == nullptr) return 0;
|
||||
Value v{};
|
||||
if (!s->s->cached(static_cast<Prop>(prop), &v)) return 0;
|
||||
if (out != nullptr) {
|
||||
out->kind = static_cast<fgl_value_kind_t>(v.kind);
|
||||
out->i = v.i;
|
||||
memcpy(out->s, v.s, sizeof(out->s));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Интроспекция
|
||||
// ---------------------------------------------------------------------------
|
||||
int fgl_template_detect(const char* oem_model) {
|
||||
// Контракт c_api.h: -1 — неизвестная модель (template_detect дал бы kA).
|
||||
if (!fgl::aircon::template_is_known(oem_model)) return -1;
|
||||
return static_cast<int>(fgl::aircon::template_detect(oem_model));
|
||||
}
|
||||
|
||||
const char* fgl_prop_name(fgl_template_t t, fgl_prop_t prop) {
|
||||
const PropInfo* info = fgl::aircon::prop_info_find(static_cast<Template>(t),
|
||||
static_cast<Prop>(prop));
|
||||
return info != nullptr ? info->name : nullptr;
|
||||
}
|
||||
|
||||
const char* fgl_prop_base_type(fgl_template_t t, fgl_prop_t prop) {
|
||||
const PropInfo* info = fgl::aircon::prop_info_find(static_cast<Template>(t),
|
||||
static_cast<Prop>(prop));
|
||||
return info != nullptr ? info->base_type : nullptr;
|
||||
}
|
||||
|
||||
int fgl_prop_read_only(fgl_template_t t, fgl_prop_t prop) {
|
||||
const PropInfo* info = fgl::aircon::prop_info_find(static_cast<Template>(t),
|
||||
static_cast<Prop>(prop));
|
||||
return info != nullptr ? (info->read_only ? 1 : 0) : -1;
|
||||
}
|
||||
|
||||
int64_t fgl_convert_to_display(fgl_template_t t, fgl_prop_t prop,
|
||||
int64_t raw) {
|
||||
return fgl::aircon::convert_to_display(static_cast<Template>(t),
|
||||
static_cast<Prop>(prop), raw, nullptr);
|
||||
}
|
||||
|
||||
int64_t fgl_convert_from_input(fgl_template_t t, fgl_prop_t prop,
|
||||
int64_t api_value) {
|
||||
return fgl::aircon::convert_from_input(static_cast<Template>(t),
|
||||
static_cast<Prop>(prop), api_value,
|
||||
nullptr);
|
||||
}
|
||||
Reference in New Issue
Block a user