- 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 очередь
417 lines
14 KiB
C++
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);
|
|
}
|