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);
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
// Реализация публичной сессии fgl-aircon поверх ayla-сессии.
|
||||
#include "fgl-aircon/session.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
#include "ayla/log.hpp"
|
||||
#include "ayla/session.hpp"
|
||||
|
||||
namespace fgl::aircon {
|
||||
|
||||
namespace {
|
||||
|
||||
struct LogCtx {
|
||||
LogSink fn = nullptr;
|
||||
void* user = nullptr;
|
||||
};
|
||||
LogCtx g_log_ctx;
|
||||
|
||||
void log_trampoline(int level, const char* msg, size_t len, void*) {
|
||||
if (g_log_ctx.fn != nullptr) {
|
||||
g_log_ctx.fn(level, msg, len, g_log_ctx.user);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void set_log_sink(LogSink sink, void* ctx) {
|
||||
g_log_ctx.fn = sink;
|
||||
g_log_ctx.user = ctx;
|
||||
ayla::log::set_sink(sink != nullptr ? log_trampoline : nullptr, nullptr);
|
||||
}
|
||||
|
||||
void set_log_level(int min_level) { ayla::log::set_min_level(min_level); }
|
||||
|
||||
struct Session::Impl {
|
||||
Config cfg{};
|
||||
Callbacks cbs{};
|
||||
Template tmpl = Template::kA;
|
||||
ayla::Session* ayla = nullptr;
|
||||
|
||||
struct CacheEntry {
|
||||
Value value;
|
||||
bool present = false;
|
||||
};
|
||||
CacheEntry cache[static_cast<size_t>(Prop::kCount)];
|
||||
|
||||
// Спинлок (no-heap): сериализует запись кэша (колбэк ayla) и чтение
|
||||
// (cached()). Критические секции — копии POD ≤ 80 Б.
|
||||
volatile int spin = 0;
|
||||
void lock() {
|
||||
while (__sync_lock_test_and_set(&spin, 1)) {
|
||||
}
|
||||
}
|
||||
void unlock() { __sync_lock_release(&spin); }
|
||||
|
||||
static void on_ayla_state(void* ctx, ayla::SessionState st,
|
||||
ayla::SessionError err);
|
||||
static void on_ayla_property(void* ctx, const ayla::PropertyEvent& ev);
|
||||
|
||||
const PropOverride* overrides() const { return cfg.overrides; }
|
||||
};
|
||||
|
||||
void Session::Impl::on_ayla_state(void* ctx, ayla::SessionState st,
|
||||
ayla::SessionError err) {
|
||||
auto* impl = static_cast<Impl*>(ctx);
|
||||
if (impl->cbs.on_state != nullptr) {
|
||||
impl->cbs.on_state(impl->cbs.ctx, static_cast<State>(st),
|
||||
static_cast<Error>(err));
|
||||
}
|
||||
}
|
||||
|
||||
void Session::Impl::on_ayla_property(void* ctx,
|
||||
const ayla::PropertyEvent& ev) {
|
||||
auto* impl = static_cast<Impl*>(ctx);
|
||||
const PropInfo* info = prop_info_find_by_name(impl->tmpl, ev.name);
|
||||
if (info == nullptr) return; // свойство не из шаблона
|
||||
|
||||
Value v{};
|
||||
if (ev.is_int) {
|
||||
// Прибор шлёт bool-свойства как int 0/1 — коэрсим к типу из таблицы.
|
||||
v.kind = (info->kind == ValueKind::kBool) ? ValueKind::kBool
|
||||
: ValueKind::kInt;
|
||||
v.i = convert_to_display(impl->tmpl, info->prop, ev.int_value,
|
||||
impl->overrides());
|
||||
} else if (ev.is_bool) {
|
||||
v.kind = ValueKind::kBool;
|
||||
v.i = ev.bool_value ? 1 : 0;
|
||||
} else {
|
||||
v.kind = ValueKind::kString;
|
||||
snprintf(v.s, sizeof(v.s), "%s", ev.str_value);
|
||||
}
|
||||
{
|
||||
impl->lock();
|
||||
auto& entry = impl->cache[static_cast<size_t>(info->prop)];
|
||||
entry.value = v;
|
||||
entry.present = true;
|
||||
impl->unlock();
|
||||
}
|
||||
if (impl->cbs.on_property != nullptr) {
|
||||
PropertyEvent out{};
|
||||
out.prop = info->prop;
|
||||
out.value = v;
|
||||
out.cmd_id = ev.cmd_id;
|
||||
out.status = ev.status;
|
||||
impl->cbs.on_property(impl->cbs.ctx, out);
|
||||
}
|
||||
}
|
||||
|
||||
Session::Session() : impl_(nullptr) {}
|
||||
|
||||
Session::~Session() {
|
||||
if (impl_ != nullptr) {
|
||||
stop();
|
||||
delete impl_->ayla;
|
||||
delete impl_;
|
||||
impl_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Session* Session::create(const Config& cfg, const Callbacks& cbs) {
|
||||
if (cfg.host == nullptr || cfg.dsn == nullptr || cfg.lanip_key == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
if (cfg.overrides != nullptr) {
|
||||
// Валидация списка: терминатор Prop::kCount (обязателен, контракт
|
||||
// документирован в types.hpp); linear без нулевых делителей; custom_fn
|
||||
// с ненулевым fn. Сканирование ограничено kCount+1 элементами.
|
||||
bool term = false;
|
||||
for (ptrdiff_t i = 0;; i++) {
|
||||
if (i > static_cast<ptrdiff_t>(Prop::kCount)) return nullptr;
|
||||
const PropOverride& o = cfg.overrides[i];
|
||||
if (o.prop == Prop::kCount) {
|
||||
term = true;
|
||||
break;
|
||||
}
|
||||
if (static_cast<uint8_t>(o.prop) >=
|
||||
static_cast<uint8_t>(Prop::kCount)) {
|
||||
return nullptr;
|
||||
}
|
||||
if (o.to_display.kind == ConvKind::kLinear &&
|
||||
(o.to_display.linear.num == 0 || o.to_display.linear.den == 0)) {
|
||||
return nullptr; // деление на ноль
|
||||
}
|
||||
if (o.from_input.kind == ConvKind::kLinear &&
|
||||
(o.from_input.linear.num == 0 || o.from_input.linear.den == 0)) {
|
||||
return nullptr; // деление на ноль
|
||||
}
|
||||
if (o.to_display.kind == ConvKind::kCustomFn &&
|
||||
o.to_display.fn == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
if (o.from_input.kind == ConvKind::kCustomFn &&
|
||||
o.from_input.fn == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
if (!term) return nullptr;
|
||||
}
|
||||
|
||||
auto* impl = new (std::nothrow) Session::Impl();
|
||||
if (impl == nullptr) return nullptr;
|
||||
impl->cfg = cfg;
|
||||
impl->cbs = cbs;
|
||||
impl->tmpl = cfg.tmpl;
|
||||
|
||||
ayla::SessionConfig acfg{};
|
||||
acfg.host = cfg.host;
|
||||
acfg.device_port = cfg.device_port;
|
||||
acfg.dsn = cfg.dsn;
|
||||
acfg.lanip_key = cfg.lanip_key;
|
||||
acfg.lanip_key_id = cfg.lanip_key_id;
|
||||
acfg.listen_port = cfg.listen_port;
|
||||
acfg.keepalive_ms = cfg.keepalive_ms;
|
||||
acfg.max_queue = cfg.max_queue;
|
||||
ayla::SessionCallbacks acbs{};
|
||||
acbs.on_state = &Session::Impl::on_ayla_state;
|
||||
acbs.on_property = &Session::Impl::on_ayla_property;
|
||||
acbs.ctx = impl;
|
||||
impl->ayla = ayla::Session::create(acfg, acbs);
|
||||
if (impl->ayla == nullptr) {
|
||||
delete impl;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* s = new (std::nothrow) Session();
|
||||
if (s == nullptr) {
|
||||
delete impl->ayla;
|
||||
delete impl;
|
||||
return nullptr;
|
||||
}
|
||||
s->impl_ = impl;
|
||||
return s;
|
||||
}
|
||||
|
||||
bool Session::start() {
|
||||
return impl_ != nullptr && impl_->ayla != nullptr && impl_->ayla->start();
|
||||
}
|
||||
|
||||
void Session::stop() {
|
||||
if (impl_ != nullptr && impl_->ayla != nullptr) {
|
||||
impl_->ayla->stop();
|
||||
}
|
||||
}
|
||||
|
||||
State Session::state() const {
|
||||
if (impl_ == nullptr || impl_->ayla == nullptr) return State::kIdle;
|
||||
return static_cast<State>(impl_->ayla->state());
|
||||
}
|
||||
|
||||
Error Session::last_error() const {
|
||||
if (impl_ == nullptr || impl_->ayla == nullptr) return Error::kNone;
|
||||
return static_cast<Error>(impl_->ayla->last_error());
|
||||
}
|
||||
|
||||
bool Session::set_int(Prop prop, int64_t value) {
|
||||
if (impl_ == nullptr ||
|
||||
static_cast<uint8_t>(prop) >= static_cast<uint8_t>(Prop::kCount)) {
|
||||
return false;
|
||||
}
|
||||
const PropInfo* info = prop_info_find(impl_->tmpl, prop);
|
||||
if (info == nullptr || info->read_only || info->kind == ValueKind::kString) {
|
||||
return false;
|
||||
}
|
||||
int64_t lo = 0, hi = 0;
|
||||
if (prop_api_range(impl_->tmpl, prop, impl_->overrides(), &lo, &hi)) {
|
||||
if (value < lo) value = lo;
|
||||
if (value > hi) value = hi;
|
||||
}
|
||||
int64_t raw =
|
||||
convert_from_input(impl_->tmpl, prop, value, impl_->overrides());
|
||||
bool ok = impl_->ayla->set_property(info->name, raw, info->base_type);
|
||||
if (ok) {
|
||||
// Оптимистичный кэш (эха нет — PROTOCOL §5.3).
|
||||
impl_->lock();
|
||||
auto& entry = impl_->cache[static_cast<size_t>(prop)];
|
||||
entry.value.kind = info->kind;
|
||||
entry.value.i = value;
|
||||
entry.present = true;
|
||||
impl_->unlock();
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool Session::set_bool(Prop prop, bool value) {
|
||||
if (impl_ == nullptr) return false;
|
||||
const PropInfo* info = prop_info_find(impl_->tmpl, prop);
|
||||
if (info == nullptr || info->kind != ValueKind::kBool) return false;
|
||||
return set_int(prop, value ? 1 : 0);
|
||||
}
|
||||
|
||||
bool Session::set_string(Prop prop, const char* value) {
|
||||
if (impl_ == nullptr || value == nullptr) return false;
|
||||
const PropInfo* info = prop_info_find(impl_->tmpl, prop);
|
||||
if (info == nullptr || info->read_only ||
|
||||
info->kind != ValueKind::kString) {
|
||||
return false;
|
||||
}
|
||||
bool ok = impl_->ayla->set_property_string(info->name, value);
|
||||
if (ok) {
|
||||
impl_->lock();
|
||||
auto& entry = impl_->cache[static_cast<size_t>(prop)];
|
||||
snprintf(entry.value.s, sizeof(entry.value.s), "%s", value);
|
||||
entry.value.kind = ValueKind::kString;
|
||||
entry.present = true;
|
||||
impl_->unlock();
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool Session::get_prop(Prop prop) {
|
||||
if (impl_ == nullptr) return false;
|
||||
const PropInfo* info = prop_info_find(impl_->tmpl, prop);
|
||||
if (info == nullptr) return false;
|
||||
return impl_->ayla->get_property(info->name);
|
||||
}
|
||||
|
||||
bool Session::batch_begin() {
|
||||
return impl_ != nullptr && impl_->ayla != nullptr &&
|
||||
impl_->ayla->begin_batch();
|
||||
}
|
||||
bool Session::batch_commit() {
|
||||
return impl_ != nullptr && impl_->ayla != nullptr &&
|
||||
impl_->ayla->commit_batch();
|
||||
}
|
||||
bool Session::batch_abort() {
|
||||
return impl_ != nullptr && impl_->ayla != nullptr &&
|
||||
impl_->ayla->abort_batch();
|
||||
}
|
||||
|
||||
bool Session::cached(Prop prop, Value* out) const {
|
||||
if (impl_ == nullptr ||
|
||||
static_cast<uint8_t>(prop) >= static_cast<uint8_t>(Prop::kCount)) {
|
||||
return false;
|
||||
}
|
||||
impl_->lock();
|
||||
const auto& entry = impl_->cache[static_cast<size_t>(prop)];
|
||||
bool present = entry.present;
|
||||
if (present && out != nullptr) *out = entry.value;
|
||||
impl_->unlock();
|
||||
return present;
|
||||
}
|
||||
|
||||
uint32_t Session::rekey_count() const {
|
||||
return impl_ != nullptr && impl_->ayla != nullptr
|
||||
? impl_->ayla->rekey_count()
|
||||
: 0;
|
||||
}
|
||||
uint32_t Session::pushes_ok() const {
|
||||
return impl_ != nullptr && impl_->ayla != nullptr
|
||||
? impl_->ayla->pushes_ok()
|
||||
: 0;
|
||||
}
|
||||
uint32_t Session::pushes_bad() const {
|
||||
return impl_ != nullptr && impl_->ayla != nullptr
|
||||
? impl_->ayla->pushes_bad()
|
||||
: 0;
|
||||
}
|
||||
uint32_t Session::commands_served() const {
|
||||
return impl_ != nullptr && impl_->ayla != nullptr
|
||||
? impl_->ayla->commands_served()
|
||||
: 0;
|
||||
}
|
||||
|
||||
} // namespace fgl::aircon
|
||||
@@ -0,0 +1,354 @@
|
||||
// Таблицы свойств шаблонов A/B/F (docs/PROTOCOL.md §8.1-8.2, по данным APK).
|
||||
#include "fgl-aircon/templates.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace fgl::aircon {
|
||||
|
||||
namespace {
|
||||
|
||||
// raw-диапазоны: PROTOCOL §8.3 (таблица приложения −10..45 °C; фактические
|
||||
// ограничения прибора уже; направления — по num_dir, таймеры/прочее — 0..N).
|
||||
constexpr PropInfo kTemplateA[] = {
|
||||
{Prop::OperationMode, "operation_mode", "integer", false, ValueKind::kInt, 0, 6},
|
||||
{Prop::FanSpeed, "fan_speed", "integer", false, ValueKind::kInt, 0, 4},
|
||||
{Prop::AdjustTemperature, "adjust_temperature", "integer", false, ValueKind::kInt, -100, 450},
|
||||
{Prop::DisplayTemperature, "display_temperature", "integer", true, ValueKind::kInt, 4000, 9500},
|
||||
{Prop::AfVerticalDirection, "af_vertical_direction", "integer", false, ValueKind::kInt, 0, 15},
|
||||
{Prop::AfVerticalSwing, "af_vertical_swing", "integer", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::AfHorizontalDirection, "af_horizontal_direction", "integer", false, ValueKind::kInt, 0, 15},
|
||||
{Prop::AfHorizontalSwing, "af_horizontal_swing", "integer", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::OutdoorLowNoise, "outdoor_low_noise", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::IndoorFanControl, "indoor_fan_control", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::HumanDetAutoSave, "human_det_auto_save", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::MinHeat, "min_heat", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::PowerfulMode, "powerful_mode", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::CoilDryMode, "coil_dry_mode", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::EconomyMode, "economy_mode", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::MasterTimerOnOff1, "master_timer_on_off_1", "integer", false, ValueKind::kInt, 0, 0},
|
||||
{Prop::MasterTimerOnOff2, "master_timer_on_off_2", "integer", false, ValueKind::kInt, 0, 0},
|
||||
{Prop::ErrorCode, "error_code", "integer", true, ValueKind::kInt, 0, 4095},
|
||||
{Prop::DemandControl, "demand_control", "integer", false, ValueKind::kInt, 0, 0},
|
||||
{Prop::FilterSignResetDisplay, "filter_sign_reset_display", "integer", false, ValueKind::kInt, 0, 1},
|
||||
{Prop::OpStatus, "op_status", "integer", true, ValueKind::kInt, 0, 0},
|
||||
{Prop::DeviceName, "device_name", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::BuildingName, "building_name", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::WifiLedEnable, "wifi_led_enable", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::ServiceContactName, "service_contact_name", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::ServiceContactPhone, "service_contact_phone", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::ServiceContactEmail, "service_contact_email", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::AfHorizontalNumDir, "af_horizontal_num_dir", "integer", true, ValueKind::kInt, 0, 15},
|
||||
{Prop::AfVerticalNumDir, "af_vertical_num_dir", "integer", true, ValueKind::kInt, 0, 15},
|
||||
{Prop::DeviceCapabilities, "device_capabilities", "integer", true, ValueKind::kInt, 0, 0},
|
||||
{Prop::GetProp, "get_prop", "integer", false, ValueKind::kInt, 0, 1},
|
||||
{Prop::HumanDet, "human_det", "integer", false, ValueKind::kInt, 0, 0},
|
||||
{Prop::Refresh, "refresh", "integer", false, ValueKind::kInt, 0, 1},
|
||||
};
|
||||
|
||||
constexpr PropInfo kTemplateB[] = {
|
||||
{Prop::OperationMode, "operation_mode", "integer", false, ValueKind::kInt, 0, 6},
|
||||
{Prop::FanSpeed, "fan_speed", "integer", false, ValueKind::kInt, 0, 4},
|
||||
{Prop::AdjustTemperature, "adjust_temperature", "integer", false, ValueKind::kInt, -100, 450},
|
||||
{Prop::AfVerticalMoveStep1, "af_vertical_move_step1", "integer", false, ValueKind::kInt, 0, 0},
|
||||
{Prop::AfHorizontalMoveStep1, "af_horizontal_move_step1", "integer", false, ValueKind::kInt, 0, 0},
|
||||
{Prop::EconomyMode, "economy_mode", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::MasterTimerOnOff1, "master_timer_on_off_1", "integer", false, ValueKind::kInt, 0, 0},
|
||||
{Prop::MasterTimerOnOff2, "master_timer_on_off_2", "integer", false, ValueKind::kInt, 0, 0},
|
||||
{Prop::ErrorCode, "error_code", "integer", true, ValueKind::kInt, 0, 4095},
|
||||
{Prop::DemandControl, "demand_control", "integer", false, ValueKind::kInt, 0, 0},
|
||||
{Prop::FilterSignResetDisplay, "filter_sign_reset_display", "integer", false, ValueKind::kInt, 0, 1},
|
||||
{Prop::OpStatus, "op_status", "integer", true, ValueKind::kInt, 0, 0},
|
||||
{Prop::DeviceName, "device_name", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::BuildingName, "building_name", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::WifiLedEnable, "wifi_led_enable", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::ServiceContactName, "service_contact_name", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::ServiceContactPhone, "service_contact_phone", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::ServiceContactEmail, "service_contact_email", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::DeviceCapabilities, "device_capabilities", "integer", true, ValueKind::kInt, 0, 0},
|
||||
{Prop::Refresh, "refresh", "integer", false, ValueKind::kInt, 0, 1},
|
||||
};
|
||||
|
||||
// F = A + monitor1, filter_sign_reset вместо filter_sign_reset_display.
|
||||
constexpr PropInfo kTemplateF[] = {
|
||||
{Prop::OperationMode, "operation_mode", "integer", false, ValueKind::kInt, 0, 6},
|
||||
{Prop::FanSpeed, "fan_speed", "integer", false, ValueKind::kInt, 0, 4},
|
||||
{Prop::AdjustTemperature, "adjust_temperature", "integer", false, ValueKind::kInt, -100, 450},
|
||||
{Prop::DisplayTemperature, "display_temperature", "integer", true, ValueKind::kInt, 4000, 9500},
|
||||
{Prop::AfVerticalDirection, "af_vertical_direction", "integer", false, ValueKind::kInt, 0, 15},
|
||||
{Prop::AfVerticalSwing, "af_vertical_swing", "integer", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::AfHorizontalDirection, "af_horizontal_direction", "integer", false, ValueKind::kInt, 0, 15},
|
||||
{Prop::AfHorizontalSwing, "af_horizontal_swing", "integer", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::OutdoorLowNoise, "outdoor_low_noise", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::IndoorFanControl, "indoor_fan_control", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::HumanDetAutoSave, "human_det_auto_save", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::MinHeat, "min_heat", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::PowerfulMode, "powerful_mode", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::CoilDryMode, "coil_dry_mode", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::EconomyMode, "economy_mode", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::MasterTimerOnOff1, "master_timer_on_off_1", "integer", false, ValueKind::kInt, 0, 0},
|
||||
{Prop::MasterTimerOnOff2, "master_timer_on_off_2", "integer", false, ValueKind::kInt, 0, 0},
|
||||
{Prop::ErrorCode, "error_code", "integer", true, ValueKind::kInt, 0, 4095},
|
||||
{Prop::DemandControl, "demand_control", "integer", false, ValueKind::kInt, 0, 0},
|
||||
{Prop::FilterSignReset, "filter_sign_reset", "integer", false, ValueKind::kInt, 0, 1},
|
||||
{Prop::OpStatus, "op_status", "integer", true, ValueKind::kInt, 0, 0},
|
||||
{Prop::DeviceName, "device_name", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::BuildingName, "building_name", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::WifiLedEnable, "wifi_led_enable", "boolean", false, ValueKind::kBool, 0, 1},
|
||||
{Prop::ServiceContactName, "service_contact_name", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::ServiceContactPhone, "service_contact_phone", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::ServiceContactEmail, "service_contact_email", "string", false, ValueKind::kString, 0, 0},
|
||||
{Prop::AfHorizontalNumDir, "af_horizontal_num_dir", "integer", true, ValueKind::kInt, 0, 15},
|
||||
{Prop::AfVerticalNumDir, "af_vertical_num_dir", "integer", true, ValueKind::kInt, 0, 15},
|
||||
{Prop::DeviceCapabilities, "device_capabilities", "integer", true, ValueKind::kInt, 0, 0},
|
||||
{Prop::GetProp, "get_prop", "integer", false, ValueKind::kInt, 0, 1},
|
||||
{Prop::HumanDet, "human_det", "integer", false, ValueKind::kInt, 0, 0},
|
||||
{Prop::Monitor1, "monitor1", "integer", true, ValueKind::kInt, 0, 0},
|
||||
{Prop::Refresh, "refresh", "integer", false, ValueKind::kInt, 0, 1},
|
||||
};
|
||||
|
||||
constexpr const char* kOemA[] = {"AP-WA1E", "AP-WA2E", "AP-WA3E", "AP-WA4E",
|
||||
"AP-WA5E", "AP-WA6E", "AP-WC1E", "AP-WC2E",
|
||||
"AP-WC3E", "AP-WC4E", "AP-WD1E"};
|
||||
constexpr const char* kOemB[] = {"AP-WB1E", "AP-WB2E", "AP-WB3E", "AP-WB4E"};
|
||||
constexpr const char* kOemF[] = {"AP-WF1E", "AP-WF2E", "AP-WF3E", "AP-WF4E"};
|
||||
|
||||
bool in_list(const char* const* list, size_t n, const char* model) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (strcmp(list[i], model) == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr size_t kOemACount = sizeof(kOemA) / sizeof(kOemA[0]);
|
||||
constexpr size_t kOemBCount = sizeof(kOemB) / sizeof(kOemB[0]);
|
||||
constexpr size_t kOemFCount = sizeof(kOemF) / sizeof(kOemF[0]);
|
||||
|
||||
const PropInfo* table_begin(Template t) {
|
||||
switch (t) {
|
||||
case Template::kA: return kTemplateA;
|
||||
case Template::kB: return kTemplateB;
|
||||
case Template::kF: return kTemplateF;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
size_t table_count(Template t) {
|
||||
switch (t) {
|
||||
case Template::kA: return sizeof(kTemplateA) / sizeof(kTemplateA[0]);
|
||||
case Template::kB: return sizeof(kTemplateB) / sizeof(kTemplateB[0]);
|
||||
case Template::kF: return sizeof(kTemplateF) / sizeof(kTemplateF[0]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Деление int64 с округлением к нулю; b != 0 (валидация в create);
|
||||
// INT64_MIN/-1 → насыщение (иначе UB).
|
||||
int64_t div_round_zero(int64_t a, int64_t b) {
|
||||
// Нативное / уже усекает к нулю; единственный UB-случай — INT64_MIN / -1.
|
||||
if (a == INT64_MIN && b == -1) return INT64_MAX;
|
||||
return a / b;
|
||||
}
|
||||
|
||||
const Conversion* pick_override(const PropOverride* overrides, Prop prop,
|
||||
bool to_display) {
|
||||
if (overrides == nullptr) return nullptr;
|
||||
for (const PropOverride* o = overrides; o->prop != Prop::kCount; o++) {
|
||||
if (o->prop == prop) {
|
||||
return to_display ? &o->to_display : &o->from_input;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const PropOverride* find_override(const PropOverride* overrides, Prop prop) {
|
||||
if (overrides == nullptr) return nullptr;
|
||||
for (const PropOverride* o = overrides; o->prop != Prop::kCount; o++) {
|
||||
if (o->prop == prop) return o;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool template_is_known(const char* oem_model) {
|
||||
if (oem_model == nullptr) return false;
|
||||
return in_list(kOemA, kOemACount, oem_model) ||
|
||||
in_list(kOemB, kOemBCount, oem_model) ||
|
||||
in_list(kOemF, kOemFCount, oem_model);
|
||||
}
|
||||
|
||||
Template template_detect(const char* oem_model) {
|
||||
if (oem_model == nullptr) {
|
||||
// Эквивалент kA, чтобы API без модели работал; явная проверка у вызывающего.
|
||||
return Template::kA;
|
||||
}
|
||||
if (in_list(kOemA, kOemACount, oem_model)) return Template::kA;
|
||||
if (in_list(kOemB, kOemBCount, oem_model)) return Template::kB;
|
||||
if (in_list(kOemF, kOemFCount, oem_model)) return Template::kF;
|
||||
return Template::kA; // неизвестные модели трактуем как A (как и legacy)
|
||||
}
|
||||
|
||||
const PropInfo* prop_info_begin(Template t) { return table_begin(t); }
|
||||
size_t prop_info_count(Template t) { return table_count(t); }
|
||||
|
||||
const PropInfo* prop_info_find(Template t, Prop prop) {
|
||||
for (const PropInfo* p = table_begin(t);
|
||||
p != table_begin(t) + table_count(t); p++) {
|
||||
if (p->prop == prop) return p;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const PropInfo* prop_info_find_by_name(Template t, const char* name) {
|
||||
if (name == nullptr) return nullptr;
|
||||
for (const PropInfo* p = table_begin(t);
|
||||
p != table_begin(t) + table_count(t); p++) {
|
||||
if (strcmp(p->name, name) == 0) return p;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* prop_enum_name(Prop prop) {
|
||||
switch (prop) {
|
||||
case Prop::OperationMode: return "OperationMode";
|
||||
case Prop::FanSpeed: return "FanSpeed";
|
||||
case Prop::AdjustTemperature: return "AdjustTemperature";
|
||||
case Prop::DisplayTemperature: return "DisplayTemperature";
|
||||
case Prop::AfVerticalDirection: return "AfVerticalDirection";
|
||||
case Prop::AfVerticalSwing: return "AfVerticalSwing";
|
||||
case Prop::AfHorizontalDirection: return "AfHorizontalDirection";
|
||||
case Prop::AfHorizontalSwing: return "AfHorizontalSwing";
|
||||
case Prop::AfVerticalMoveStep1: return "AfVerticalMoveStep1";
|
||||
case Prop::AfHorizontalMoveStep1: return "AfHorizontalMoveStep1";
|
||||
case Prop::OutdoorLowNoise: return "OutdoorLowNoise";
|
||||
case Prop::IndoorFanControl: return "IndoorFanControl";
|
||||
case Prop::HumanDetAutoSave: return "HumanDetAutoSave";
|
||||
case Prop::MinHeat: return "MinHeat";
|
||||
case Prop::PowerfulMode: return "PowerfulMode";
|
||||
case Prop::CoilDryMode: return "CoilDryMode";
|
||||
case Prop::EconomyMode: return "EconomyMode";
|
||||
case Prop::MasterTimerOnOff1: return "MasterTimerOnOff1";
|
||||
case Prop::MasterTimerOnOff2: return "MasterTimerOnOff2";
|
||||
case Prop::ErrorCode: return "ErrorCode";
|
||||
case Prop::DemandControl: return "DemandControl";
|
||||
case Prop::FilterSignResetDisplay: return "FilterSignResetDisplay";
|
||||
case Prop::FilterSignReset: return "FilterSignReset";
|
||||
case Prop::OpStatus: return "OpStatus";
|
||||
case Prop::DeviceName: return "DeviceName";
|
||||
case Prop::BuildingName: return "BuildingName";
|
||||
case Prop::WifiLedEnable: return "WifiLedEnable";
|
||||
case Prop::ServiceContactName: return "ServiceContactName";
|
||||
case Prop::ServiceContactPhone: return "ServiceContactPhone";
|
||||
case Prop::ServiceContactEmail: return "ServiceContactEmail";
|
||||
case Prop::AfHorizontalNumDir: return "AfHorizontalNumDir";
|
||||
case Prop::AfVerticalNumDir: return "AfVerticalNumDir";
|
||||
case Prop::DeviceCapabilities: return "DeviceCapabilities";
|
||||
case Prop::GetProp: return "GetProp";
|
||||
case Prop::HumanDet: return "HumanDet";
|
||||
case Prop::Monitor1: return "Monitor1";
|
||||
case Prop::Refresh: return "Refresh";
|
||||
case Prop::kCount: break;
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
int64_t convert_to_display(Template t, Prop prop, int64_t raw,
|
||||
const PropOverride* overrides) {
|
||||
(void)t; // шаблонных не-тождественных конверсий пока нет (кроме общей ниже)
|
||||
const Conversion* c = pick_override(overrides, prop, /*to_display=*/true);
|
||||
if (c != nullptr) {
|
||||
switch (c->kind) {
|
||||
case ConvKind::kLinear: {
|
||||
int64_t prod = 0;
|
||||
if (__builtin_mul_overflow(raw, c->linear.num, &prod)) {
|
||||
prod = (raw < 0) != (c->linear.num < 0) ? INT64_MIN : INT64_MAX;
|
||||
}
|
||||
int64_t q = div_round_zero(prod, c->linear.den);
|
||||
int64_t out = 0;
|
||||
if (__builtin_add_overflow(q, c->linear.offset, &out)) {
|
||||
out = q < 0 ? INT64_MIN : INT64_MAX;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
case ConvKind::kCustomFn:
|
||||
return c->fn != nullptr ? c->fn(raw, c->ctx) : raw;
|
||||
case ConvKind::kTemplateDefault:
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Шаблонная конверсия по умолчанию.
|
||||
if (prop == Prop::DisplayTemperature) {
|
||||
int64_t diff = 0;
|
||||
if (__builtin_sub_overflow(raw, 5000, &diff)) {
|
||||
diff = INT64_MIN; // raw < INT64_MIN+5000 — дальше только насыщение
|
||||
}
|
||||
return div_round_zero(diff, 10); // raw 0.01°C(+5000) -> API 0.1°C
|
||||
}
|
||||
return raw; // остальные свойства — тождественны
|
||||
}
|
||||
|
||||
int64_t convert_from_input(Template t, Prop prop, int64_t api_value,
|
||||
const PropOverride* overrides) {
|
||||
(void)t;
|
||||
const Conversion* c = pick_override(overrides, prop, /*to_display=*/false);
|
||||
if (c != nullptr) {
|
||||
switch (c->kind) {
|
||||
case ConvKind::kLinear: {
|
||||
// Инверсия: raw = (api - offset) * den / num (с насыщением).
|
||||
int64_t diff = 0;
|
||||
// Направление насыщения: по знаку уменьшаемого (offset с экстремумом
|
||||
// сам бы переполнил любую разность).
|
||||
if (__builtin_sub_overflow(api_value, c->linear.offset, &diff)) {
|
||||
diff = api_value < 0 ? INT64_MIN : INT64_MAX;
|
||||
}
|
||||
int64_t prod = 0;
|
||||
if (__builtin_mul_overflow(diff, c->linear.den, &prod)) {
|
||||
prod = (diff < 0) != (c->linear.den < 0) ? INT64_MIN : INT64_MAX;
|
||||
}
|
||||
return div_round_zero(prod, c->linear.num);
|
||||
}
|
||||
case ConvKind::kCustomFn:
|
||||
return c->fn != nullptr ? c->fn(api_value, c->ctx) : api_value;
|
||||
case ConvKind::kTemplateDefault:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (prop == Prop::DisplayTemperature) {
|
||||
int64_t prod = 0;
|
||||
if (__builtin_mul_overflow(api_value, 10, &prod)) {
|
||||
prod = api_value < 0 ? INT64_MIN : INT64_MAX;
|
||||
}
|
||||
int64_t out = 0;
|
||||
if (__builtin_add_overflow(prod, 5000, &out)) {
|
||||
out = prod < 0 ? INT64_MIN : INT64_MAX;
|
||||
}
|
||||
return out; // API 0.1°C -> raw (read-only, но симметрично)
|
||||
}
|
||||
return api_value;
|
||||
}
|
||||
|
||||
bool prop_api_range(Template t, Prop prop, const PropOverride* overrides,
|
||||
int64_t* min, int64_t* max) {
|
||||
const PropOverride* o = find_override(overrides, prop);
|
||||
if (o != nullptr && o->has_range) {
|
||||
if (min != nullptr) *min = o->min;
|
||||
if (max != nullptr) *max = o->max;
|
||||
return true;
|
||||
}
|
||||
const PropInfo* info = prop_info_find(t, prop);
|
||||
if (info == nullptr || info->raw_min == info->raw_max) {
|
||||
return false; // нет диапазона (0..0 в таблице — «не определён»)
|
||||
}
|
||||
int64_t lo = convert_to_display(t, prop, info->raw_min, overrides);
|
||||
int64_t hi = convert_to_display(t, prop, info->raw_max, overrides);
|
||||
if (lo > hi) {
|
||||
int64_t tmp = lo;
|
||||
lo = hi;
|
||||
hi = tmp;
|
||||
}
|
||||
if (min != nullptr) *min = lo;
|
||||
if (max != nullptr) *max = hi;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace fgl::aircon
|
||||
+67
-18
@@ -18,16 +18,26 @@ namespace {
|
||||
|
||||
constexpr uint32_t kLoopTickMs = 20;
|
||||
constexpr size_t kMaxName = 40;
|
||||
constexpr size_t kMaxQueue = 32;
|
||||
constexpr size_t kMaxQueue = 48; // >= cfg.max_queue default 40 (полный батч
|
||||
// шаблона A = 33 свойства) и с запасом
|
||||
static_assert(kMaxQueue >= 40, "kMaxQueue must cover full template batch");
|
||||
|
||||
struct Command {
|
||||
uint8_t type; // 1=GET, 2=SET, 3=DELETE
|
||||
char name[kMaxName];
|
||||
int64_t value;
|
||||
char base_type[10];
|
||||
char str_value[48]; // строковые SET
|
||||
int cmd_id;
|
||||
};
|
||||
|
||||
// Безопасная копия строк (без snprintf — избегаем -Wrestrict при инлайне).
|
||||
void copy_str(char* dst, const char* src, size_t cap) {
|
||||
size_t i = 0;
|
||||
for (; i + 1 < cap && src[i] != '\0'; i++) dst[i] = src[i];
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
bool gen_random_token(char* out, size_t len) {
|
||||
static const char kAlpha[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
@@ -143,6 +153,8 @@ struct Session::Impl {
|
||||
strncmp(queue[i].name, cmd.name, kMaxName) == 0) {
|
||||
if (cmd.type == 2) {
|
||||
queue[i].value = cmd.value; // замещаем
|
||||
copy_str(queue[i].str_value, cmd.str_value,
|
||||
sizeof(queue[i].str_value));
|
||||
return true;
|
||||
}
|
||||
if (cmd.type == 1) return true; // дубликат GET
|
||||
@@ -165,6 +177,8 @@ struct Session::Impl {
|
||||
strncmp(batch[i].name, cmd.name, kMaxName) == 0) {
|
||||
if (cmd.type == 2) {
|
||||
batch[i].value = cmd.value;
|
||||
copy_str(batch[i].str_value, cmd.str_value,
|
||||
sizeof(batch[i].str_value));
|
||||
return true;
|
||||
}
|
||||
if (cmd.type == 1) return true;
|
||||
@@ -370,6 +384,8 @@ void Session::Impl::build_set_payload(char* out, size_t out_cap,
|
||||
w.key("value");
|
||||
if (strcmp(c.base_type, "boolean") == 0) {
|
||||
w.boolean(c.value != 0);
|
||||
} else if (strcmp(c.base_type, "string") == 0) {
|
||||
w.string(c.str_value);
|
||||
} else {
|
||||
w.integer(c.value);
|
||||
}
|
||||
@@ -522,26 +538,38 @@ void Session::Impl::handle_datapoint(const HttpRequest& req,
|
||||
snprintf(ev.name, sizeof(ev.name), "%s", name);
|
||||
int64_t iv = 0;
|
||||
bool bv = false;
|
||||
const char* sv = nullptr;
|
||||
size_t sv_len = 0;
|
||||
jsmntype_t vt;
|
||||
if (data_doc.get_int("value", &iv)) {
|
||||
ev.is_int = true;
|
||||
ev.int_value = iv;
|
||||
} else if (data_doc.get_bool("value", &bv)) {
|
||||
ev.is_bool = true;
|
||||
ev.bool_value = bv;
|
||||
} else if (data_doc.find("value", &sv, &sv_len, &vt) &&
|
||||
vt == JSMN_STRING && sv_len + 1 <= sizeof(ev.str_value)) {
|
||||
memcpy(ev.str_value, sv, sv_len);
|
||||
ev.str_value[sv_len] = '\0';
|
||||
}
|
||||
int cmd_id = -1, status = 0;
|
||||
parse_query(req.query, &cmd_id, &status);
|
||||
ev.cmd_id = cmd_id;
|
||||
ev.status = status;
|
||||
if (cbs.on_property != nullptr) {
|
||||
cbs.on_property(cbs.ctx, ev);
|
||||
bool value_ok = true;
|
||||
if (ev.is_int || ev.is_bool) {
|
||||
value_ok = true;
|
||||
} else {
|
||||
// Строка: значение должно влезать в буфер события; иначе событие
|
||||
// не отправляем (кэш не затираем пустышкой).
|
||||
const char* sv2 = nullptr;
|
||||
size_t sv2_len = 0;
|
||||
jsmntype_t vt2;
|
||||
value_ok = data_doc.find("value", &sv2, &sv2_len, &vt2) &&
|
||||
vt2 == JSMN_STRING &&
|
||||
sv2_len + 1 <= sizeof(ev.str_value);
|
||||
if (value_ok) {
|
||||
memcpy(ev.str_value, sv2, sv2_len);
|
||||
ev.str_value[sv2_len] = '\0';
|
||||
}
|
||||
}
|
||||
if (value_ok) {
|
||||
int cmd_id = -1, status = 0;
|
||||
parse_query(req.query, &cmd_id, &status);
|
||||
ev.cmd_id = cmd_id;
|
||||
ev.status = status;
|
||||
if (cbs.on_property != nullptr) {
|
||||
cbs.on_property(cbs.ctx, ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -717,7 +745,7 @@ Session* Session::create(const SessionConfig& cfg, const SessionCallbacks& cbs)
|
||||
if (impl == nullptr) return nullptr;
|
||||
impl->cfg = cfg;
|
||||
impl->cbs = cbs;
|
||||
if (impl->cfg.max_queue == 0) impl->cfg.max_queue = 16;
|
||||
if (impl->cfg.max_queue == 0) impl->cfg.max_queue = 40;
|
||||
if (impl->cfg.keepalive_ms == 0) impl->cfg.keepalive_ms = 15000;
|
||||
impl->timings.keepalive_ms = impl->cfg.keepalive_ms;
|
||||
snprintf(impl->host, sizeof(impl->host), "%s", cfg.host);
|
||||
@@ -860,6 +888,26 @@ void Session::set_timings_for_test(const SessionTimings& t) {
|
||||
impl_->timings = tmp;
|
||||
}
|
||||
|
||||
bool Session::set_property_string(const char* name, const char* value) {
|
||||
if (impl_ == nullptr || name == nullptr || value == nullptr ||
|
||||
strlen(name) >= kMaxName || strlen(value) >= 48) {
|
||||
return false;
|
||||
}
|
||||
Command cmd{};
|
||||
cmd.type = 2;
|
||||
snprintf(cmd.name, sizeof(cmd.name), "%s", name);
|
||||
cmd.value = 0;
|
||||
snprintf(cmd.base_type, sizeof(cmd.base_type), "string");
|
||||
snprintf(cmd.str_value, sizeof(cmd.str_value), "%s", value);
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(impl_->queue_mu);
|
||||
cmd.cmd_id = impl_->next_cmd_id++;
|
||||
}
|
||||
if (!impl_->submit(cmd)) return false;
|
||||
impl_->want_notify.store(true, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Session::begin_batch() {
|
||||
std::lock_guard<std::mutex> lk(impl_->queue_mu);
|
||||
if (impl_->batch_open) return false;
|
||||
@@ -869,19 +917,20 @@ bool Session::begin_batch() {
|
||||
}
|
||||
|
||||
bool Session::commit_batch() {
|
||||
bool ok = true;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(impl_->queue_mu);
|
||||
if (!impl_->batch_open) return false;
|
||||
bool ok = true;
|
||||
for (uint8_t i = 0; i < impl_->batch_len; i++) {
|
||||
if (!impl_->enqueue_locked(impl_->batch[i])) ok = false;
|
||||
}
|
||||
impl_->batch_open = false;
|
||||
impl_->batch_len = 0;
|
||||
if (!ok) return false;
|
||||
}
|
||||
// notify ставится ВСЕГДА: даже при частичном отказе поставленные команды
|
||||
// должны уйти немедленно, а не ждать keep-alive.
|
||||
impl_->want_notify.store(true, std::memory_order_release);
|
||||
return true;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool Session::abort_batch() {
|
||||
|
||||
@@ -53,7 +53,8 @@ struct SessionConfig {
|
||||
uint32_t lanip_key_id = 0;
|
||||
uint16_t listen_port = 10275; // 0 — любой свободный
|
||||
uint32_t keepalive_ms = 15000;
|
||||
uint8_t max_queue = 16; // лимит очереди команд
|
||||
uint8_t max_queue = 40; // лимит очереди команд (>= полного
|
||||
// батча свойств шаблона, 33)
|
||||
};
|
||||
|
||||
struct SessionCallbacks {
|
||||
@@ -107,6 +108,8 @@ class Session {
|
||||
// SET-команда (integer/boolean как int64).
|
||||
bool set_property(const char* name, int64_t value,
|
||||
const char* base_type = "integer");
|
||||
// SET-команда строкового свойства (base_type "string").
|
||||
bool set_property_string(const char* name, const char* value);
|
||||
// Пакет: собрать несколько команд, один notify на commit.
|
||||
bool begin_batch();
|
||||
bool commit_batch();
|
||||
|
||||
Reference in New Issue
Block a user