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:
+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() {
|
||||
|
||||
Reference in New Issue
Block a user