Files
fgl-aircon/src/aircon/c_api.cpp
T
petr.polezhaev b84bd8d555 ha(H1): исправления по ревью — жизненный цикл сессии, самодостаточный wheel
- C1: единственный владелец stop+destroy — reader-поток; close() только
  выставляет флаг и ждёт; повторный close и close из колбэка безопасны
  (регресс-тесты; TSan на close с мёртвым модулем — 0 гонок)
- M1: wheel собирается с bundled mbedtls (нет внешней soname mbedcrypto);
  попутно исправлена bundled-ветка CMake (-lSTATIC)
- C-API: счётчики потерянных событий/логов, лог-кольцо без union-балласта,
  атомарные fn/ctx лог-синка
- provision: валидация формата облачных ответов; CLI: человеческие ошибки
  (exit 2), --out с правами 0600, console_script pyfglair
- упаковка: MANIFEST.in (sdist с C++-исходниками), очистка stale .so,
  readme в метаданных; find_library без ложных .bak
- тесты: close-сценарии, доставка логов, ошибки CLI/provision, C-API очередь
2026-09-29 11:48:32 +03:00

417 lines
14 KiB
C++

// Реализация extern "C" шейма (cffi/pyfglair). Значения enum'ов C-API
// обязаны совпадать с C++ enum'ами — проверено static_assert'ами.
#include "fgl-aircon/c_api.h"
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <cstring>
#include <mutex>
#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; }
// ---------------------------------------------------------------------------
// Кольцевые буферы событий/логов (fixed-size, без аллокаций в рантайме)
// ---------------------------------------------------------------------------
namespace {
template <typename T, size_t N>
struct Ring {
std::mutex mtx;
std::condition_variable cv;
T buf[N];
size_t head = 0;
size_t count = 0;
uint64_t dropped = 0;
void push(const T& value) {
std::lock_guard<std::mutex> lock(mtx);
if (count == N) {
buf[head] = value;
head = (head + 1) % N;
dropped++;
} else {
buf[(head + count) % N] = value;
count++;
}
cv.notify_all();
}
int drain(T* out, int max_events) {
std::lock_guard<std::mutex> lock(mtx);
int n = 0;
while (n < max_events && count > 0) {
out[n++] = buf[head];
head = (head + 1) % N;
count--;
}
return n;
}
int wait(T* out, int max_events, int timeout_ms) {
std::unique_lock<std::mutex> lock(mtx);
if (count == 0) {
if (timeout_ms < 0) {
cv.wait(lock, [this] { return count > 0; });
} else {
cv.wait_for(lock, std::chrono::milliseconds(timeout_ms),
[this] { return count > 0; });
}
}
int n = 0;
while (n < max_events && count > 0) {
out[n++] = buf[head];
head = (head + 1) % N;
count--;
}
return n;
}
uint64_t dropped_count() {
std::lock_guard<std::mutex> lock(mtx);
return dropped;
}
};
constexpr size_t kSessionEventCap = 64;
constexpr size_t kLogEventCap = 64;
Ring<fgl_log_event_t, kLogEventCap> g_log_ring;
struct LogWrap {
std::atomic<fgl_log_sink_fn> fn{nullptr};
std::atomic<void*> ctx{nullptr};
};
LogWrap g_log_wrap;
std::once_flag g_log_installed;
void log_wrap_trampoline(int level, const char* msg, size_t len, void*) {
fgl_log_event_t ev{};
ev.level = level;
size_t n = len < sizeof(ev.msg) - 1 ? len : sizeof(ev.msg) - 1;
memcpy(ev.msg, msg, n);
ev.msg[n] = '\0';
g_log_ring.push(ev);
auto fn = g_log_wrap.fn.load(std::memory_order_acquire);
if (fn != nullptr) {
fn(level, msg, len, g_log_wrap.ctx.load(std::memory_order_relaxed));
}
}
void ensure_log_pipe() {
std::call_once(g_log_installed,
[] { fgl::aircon::set_log_sink(log_wrap_trampoline, nullptr); });
}
} // namespace
void fgl_log_set_sink(fgl_log_sink_fn sink, void* ctx) {
g_log_wrap.ctx.store(ctx, std::memory_order_relaxed);
g_log_wrap.fn.store(sink, std::memory_order_release);
// Ядро всегда пишет в кольцевой буфер (его читает pyfglair); sink —
// дополнительный прямой колбэк для C/C++ (только малые стеки!).
ensure_log_pipe();
}
void fgl_log_set_level(int min_level) {
fgl::aircon::set_log_level(min_level);
}
int fgl_log_poll_events(fgl_log_event_t* out, int max_events) {
if (out == nullptr || max_events <= 0) return 0;
return g_log_ring.drain(out, max_events);
}
int fgl_log_wait_events(fgl_log_event_t* out, int max_events,
int timeout_ms) {
if (out == nullptr || max_events <= 0) return 0;
return g_log_ring.wait(out, max_events, timeout_ms);
}
// ---------------------------------------------------------------------------
// Сессия
// ---------------------------------------------------------------------------
struct fgl_session {
fgl::aircon::Session* s;
Ring<fgl_event_t, kSessionEventCap>* events;
fgl_on_state_fn on_state;
fgl_on_property_fn on_property;
void* user;
};
namespace {
void on_state_trampoline(void* ctx, State st, Error err) {
auto* s = static_cast<fgl_session*>(ctx);
if (s->events != nullptr) {
fgl_event_t ev{};
ev.type = FGL_EVENT_STATE;
ev.data.state.state = static_cast<fgl_state_t>(st);
ev.data.state.error = static_cast<fgl_error_t>(err);
s->events->push(ev);
}
if (s->on_state != nullptr) {
s->on_state(s->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* s = static_cast<fgl_session*>(ctx);
if (s->events != nullptr) {
fgl_event_t out{};
out.type = FGL_EVENT_PROPERTY;
out.data.property.prop = static_cast<fgl_prop_t>(ev.prop);
out.data.property.cmd_id = ev.cmd_id;
out.data.property.status = ev.status;
out.data.property.value.kind = static_cast<fgl_value_kind_t>(ev.value.kind);
out.data.property.value.i = ev.value.i;
memcpy(out.data.property.value.s, ev.value.s,
sizeof(out.data.property.value.s));
s->events->push(out);
}
if (s->on_property != nullptr) {
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));
s->on_property(s->user, &out);
}
}
} // namespace
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* out = new (std::nothrow) fgl_session{};
if (out == nullptr) return nullptr;
out->events = new (std::nothrow) Ring<fgl_event_t, kSessionEventCap>();
if (out->events == nullptr) {
delete out;
return nullptr;
}
out->on_state = cbs != nullptr ? cbs->on_state : nullptr;
out->on_property = cbs != nullptr ? cbs->on_property : nullptr;
out->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 = out;
ensure_log_pipe();
out->s = fgl::aircon::Session::create(cc, cpp_cbs);
if (out->s == nullptr) {
delete out->events;
delete out;
return nullptr;
}
return out;
}
void fgl_session_destroy(fgl_session_t* s) {
if (s == nullptr) return;
delete s->s;
delete s->events;
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();
}
int fgl_session_poll_events(fgl_session_t* s, fgl_event_t* out,
int max_events) {
if (s == nullptr || out == nullptr || max_events <= 0) return 0;
return s->events->drain(out, max_events);
}
int fgl_session_wait_events(fgl_session_t* s, fgl_event_t* out,
int max_events, int timeout_ms) {
if (s == nullptr || out == nullptr || max_events <= 0) return 0;
return s->events->wait(out, max_events, timeout_ms);
}
uint64_t fgl_session_events_dropped(const fgl_session_t* s) {
return (s != nullptr && s->events != nullptr)
? s->events->dropped_count()
: 0;
}
uint64_t fgl_log_events_dropped(void) { return g_log_ring.dropped_count(); }
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;
}
int fgl_prop_raw_range(fgl_template_t t, fgl_prop_t prop, int64_t* min,
int64_t* max) {
const PropInfo* info = fgl::aircon::prop_info_find(static_cast<Template>(t),
static_cast<Prop>(prop));
if (info == nullptr) return -1;
if (info->raw_min == info->raw_max) return 0; // «не определён»
if (min != nullptr) *min = info->raw_min;
if (max != nullptr) *max = info->raw_max;
return 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);
}