ha(H1): pyfglair — cffi-ядро (очередь событий), wheel, CLI discover/monitor
- CMake: shared-таргет fgl-aircon-shared (FGL_BUILD_SHARED, PIC, SOVERSION) - C-API: fgl_prop_raw_range + кольцевые очереди событий/логов (fgl_session_poll/wait_events, fgl_log_poll/wait_events): прямые колбэки из потоков ядра со стеком 8 КБ небезопасны для интерпретатора Python - pyfglair: _corebuild (cmake->wheel), _cffi (ABI), Session (reader-поток -> loop.call_soon_threadsafe), templates (интроспекция C-таблиц), provision (aiohttp), CLI discover/monitor + esphome-secrets - setup.py/pyproject: платформенный wheel py3-none-linux_x86_64 с .so - tests/pyfglair: templates, provision (мок-облако), session + CLI против mock_ac.py; scripts/py-ci.sh; ci.sh запускает python-часть
This commit is contained in:
+168
-43
@@ -2,8 +2,11 @@
|
||||
// обязаны совпадать с C++ enum'ами — проверено static_assert'ами.
|
||||
#include "fgl-aircon/c_api.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
|
||||
#include "fgl-aircon/session.hpp"
|
||||
@@ -52,74 +55,174 @@ static_assert(FGL_VALUE_STRING ==
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
constexpr size_t kSessionEventCap = 64;
|
||||
constexpr size_t kLogEventCap = 64;
|
||||
|
||||
Ring<fgl_log_event_t, kLogEventCap> g_log_ring;
|
||||
|
||||
struct LogWrap {
|
||||
fgl_log_sink_fn fn;
|
||||
void* ctx;
|
||||
};
|
||||
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);
|
||||
if (g_log_wrap.fn != nullptr) {
|
||||
g_log_wrap.fn(level, msg, len, g_log_wrap.ctx);
|
||||
}
|
||||
}
|
||||
|
||||
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.fn = sink;
|
||||
g_log_wrap.ctx = ctx;
|
||||
fgl::aircon::set_log_sink(sink != nullptr ? log_wrap_trampoline : nullptr,
|
||||
nullptr);
|
||||
// Ядро всегда пишет в кольцевой буфер (его читает 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);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Сессия
|
||||
// ---------------------------------------------------------------------------
|
||||
namespace {
|
||||
|
||||
// Контексты колбэков, хранимые в fgl_session.
|
||||
struct CbCtx {
|
||||
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* c = static_cast<CbCtx*>(ctx);
|
||||
if (c->on_state != nullptr) {
|
||||
c->on_state(c->user, static_cast<fgl_state_t>(st),
|
||||
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* 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);
|
||||
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
|
||||
|
||||
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;
|
||||
@@ -136,37 +239,36 @@ fgl_session_t* fgl_session_create(const fgl_config_t* cfg,
|
||||
// Конверсии-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;
|
||||
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 = cbctx;
|
||||
cpp_cbs.ctx = out;
|
||||
|
||||
auto* s = fgl::aircon::Session::create(cc, cpp_cbs);
|
||||
if (s == nullptr) {
|
||||
delete cbctx;
|
||||
ensure_log_pipe();
|
||||
out->s = fgl::aircon::Session::create(cc, cpp_cbs);
|
||||
if (out->s == nullptr) {
|
||||
delete out->events;
|
||||
delete out;
|
||||
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->events;
|
||||
delete s;
|
||||
}
|
||||
|
||||
@@ -178,6 +280,18 @@ 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);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -262,6 +376,17 @@ int fgl_prop_read_only(fgl_template_t t, fgl_prop_t 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),
|
||||
|
||||
Reference in New Issue
Block a user