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:
2026-09-29 11:31:57 +03:00
parent 97842276c8
commit 0292404756
26 changed files with 2236 additions and 63 deletions
+168 -43
View File
@@ -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),