Refactor GatewayController to integrate GatewayCache

- Updated CMakeLists.txt to require gateway_cache component.
- Modified gateway_controller.hpp to include GatewayCache and adjust constructor.
- Removed internal scene and group management logic from GatewayController, delegating to GatewayCache.
- Simplified scene and group operations by utilizing GatewayCache methods for enabling, setting details, and deleting.
- Eliminated NVS storage handling code, as scene and group data is now managed by GatewayCache.
- Updated command handling methods to use cached data instead of internal structures.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Tony
2026-05-01 04:39:58 +08:00
parent d16c289626
commit 70c39ea1e1
10 changed files with 781 additions and 400 deletions
+7
View File
@@ -0,0 +1,7 @@
idf_component_register(
SRCS "src/gateway_cache.cpp"
INCLUDE_DIRS "include"
REQUIRES freertos log nvs_flash
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
@@ -0,0 +1,116 @@
#pragma once
#include <array>
#include <cstdint>
#include <map>
#include <string>
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "nvs.h"
namespace gateway {
enum class GatewayCachePriorityMode : uint8_t {
kOutsideBusFirst = 0,
kLocalGatewayFirst = 1,
};
struct GatewayCacheConfig {
std::string storage_namespace{"gateway_rt"};
uint32_t flush_interval_ms{5000};
uint32_t task_stack_size{4096};
UBaseType_t task_priority{3};
GatewayCachePriorityMode default_priority_mode{GatewayCachePriorityMode::kOutsideBusFirst};
};
struct GatewayCacheChannelFlags {
bool need_update_group{false};
bool need_update_scene{false};
bool need_update_settings{false};
};
class GatewayCache {
public:
struct SceneEntry {
bool enabled{false};
uint8_t brightness{254};
uint8_t color_mode{2};
uint8_t data1{0};
uint8_t data2{0};
uint8_t data3{0};
std::string name;
};
struct GroupEntry {
bool enabled{false};
uint8_t target_type{2};
uint8_t target_value{0};
std::string name;
};
using SceneStore = std::array<SceneEntry, 16>;
using GroupStore = std::array<GroupEntry, 16>;
explicit GatewayCache(GatewayCacheConfig config = {});
~GatewayCache();
esp_err_t start();
void preloadChannel(uint8_t gateway_id);
SceneStore scenes(uint8_t gateway_id);
GroupStore groups(uint8_t gateway_id);
SceneEntry scene(uint8_t gateway_id, uint8_t scene_id);
GroupEntry group(uint8_t gateway_id, uint8_t group_id);
bool setSceneEnabled(uint8_t gateway_id, uint8_t scene_id, bool enabled);
bool setSceneDetail(uint8_t gateway_id, uint8_t scene_id, uint8_t brightness,
uint8_t color_mode, uint8_t data1, uint8_t data2, uint8_t data3);
bool setSceneName(uint8_t gateway_id, uint8_t scene_id, std::string_view name);
bool deleteScene(uint8_t gateway_id, uint8_t scene_id);
std::pair<uint8_t, uint8_t> sceneMask(uint8_t gateway_id);
bool setGroupEnabled(uint8_t gateway_id, uint8_t group_id, bool enabled);
bool setGroupDetail(uint8_t gateway_id, uint8_t group_id, uint8_t target_type,
uint8_t target_value);
bool setGroupName(uint8_t gateway_id, uint8_t group_id, std::string_view name);
bool deleteGroup(uint8_t gateway_id, uint8_t group_id);
std::pair<uint8_t, uint8_t> groupMask(uint8_t gateway_id);
GatewayCacheChannelFlags channelFlags(uint8_t gateway_id);
void markGroupUpdateNeeded(uint8_t gateway_id, bool needed = true);
void markSceneUpdateNeeded(uint8_t gateway_id, bool needed = true);
void markSettingsUpdateNeeded(uint8_t gateway_id, bool needed = true);
GatewayCachePriorityMode priorityMode();
void setPriorityMode(GatewayCachePriorityMode mode);
private:
static void TaskEntry(void* arg);
void taskLoop();
bool flushDirty();
bool openStorageLocked();
void closeStorageLocked();
SceneStore& ensureSceneStoreLocked(uint8_t gateway_id);
GroupStore& ensureGroupStoreLocked(uint8_t gateway_id);
void loadSceneStoreLocked(uint8_t gateway_id, SceneStore& scenes);
void loadGroupStoreLocked(uint8_t gateway_id, GroupStore& groups);
std::string readStringLocked(std::string_view key);
bool writeStringLocked(std::string_view key, std::string_view value);
bool eraseKeyLocked(std::string_view key);
GatewayCacheConfig config_;
GatewayCachePriorityMode priority_mode_;
TaskHandle_t task_handle_{nullptr};
SemaphoreHandle_t lock_{nullptr};
nvs_handle_t storage_{0};
std::map<uint8_t, SceneStore> scenes_;
std::map<uint8_t, GroupStore> groups_;
std::map<uint8_t, GatewayCacheChannelFlags> channel_flags_;
bool dirty_{false};
};
} // namespace gateway
@@ -0,0 +1,544 @@
#include "gateway_cache.hpp"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <utility>
#include <vector>
#include "esp_log.h"
namespace gateway {
namespace {
constexpr const char* kTag = "gateway_cache";
constexpr size_t kMaxNameBytes = 32;
class LockGuard {
public:
explicit LockGuard(SemaphoreHandle_t lock) : lock_(lock) {
if (lock_ != nullptr) {
xSemaphoreTakeRecursive(lock_, portMAX_DELAY);
}
}
~LockGuard() {
if (lock_ != nullptr) {
xSemaphoreGiveRecursive(lock_);
}
}
private:
SemaphoreHandle_t lock_;
};
std::string ShortKey(const char* prefix, uint8_t gateway_id, uint8_t slot) {
char key[16] = {0};
std::snprintf(key, sizeof(key), "%s%u_%u", prefix, gateway_id, slot);
return std::string(key);
}
std::vector<int> ParseCsv(std::string_view raw) {
std::vector<int> values;
size_t start = 0;
while (start < raw.size()) {
const size_t comma = raw.find(',', start);
const size_t end = comma == std::string_view::npos ? raw.size() : comma;
if (end > start) {
values.push_back(static_cast<int>(std::strtol(std::string(raw.substr(start, end - start)).c_str(),
nullptr, 10)));
}
if (comma == std::string_view::npos) {
break;
}
start = comma + 1;
}
return values;
}
std::string NormalizeName(std::string_view name) {
std::string normalized(name);
if (normalized.size() > kMaxNameBytes) {
normalized.resize(kMaxNameBytes);
}
return normalized;
}
bool IsDefaultScene(const GatewayCache::SceneEntry& scene) {
return !scene.enabled && scene.brightness == 254 && scene.color_mode == 2 && scene.data1 == 0 &&
scene.data2 == 0 && scene.data3 == 0;
}
bool IsDefaultGroup(const GatewayCache::GroupEntry& group) {
return !group.enabled && group.target_type == 2 && group.target_value == 0;
}
std::string BuildScenePayload(const GatewayCache::SceneEntry& scene) {
char payload[32] = {0};
std::snprintf(payload, sizeof(payload), "%u,%u,%u,%u,%u,%u", scene.enabled ? 1 : 0,
scene.brightness, scene.color_mode, scene.data1, scene.data2, scene.data3);
return std::string(payload);
}
std::string BuildGroupPayload(const GatewayCache::GroupEntry& group) {
char payload[24] = {0};
std::snprintf(payload, sizeof(payload), "%u,%u,%u", group.enabled ? 1 : 0, group.target_type,
group.target_value);
return std::string(payload);
}
} // namespace
GatewayCache::GatewayCache(GatewayCacheConfig config)
: config_(std::move(config)),
priority_mode_(config_.default_priority_mode),
lock_(xSemaphoreCreateRecursiveMutex()) {}
GatewayCache::~GatewayCache() {
if (task_handle_ != nullptr) {
vTaskDelete(task_handle_);
task_handle_ = nullptr;
}
{
LockGuard guard(lock_);
flushDirty();
closeStorageLocked();
}
if (lock_ != nullptr) {
vSemaphoreDelete(lock_);
lock_ = nullptr;
}
}
esp_err_t GatewayCache::start() {
{
LockGuard guard(lock_);
if (!openStorageLocked()) {
return ESP_FAIL;
}
}
if (task_handle_ != nullptr) {
return ESP_OK;
}
const BaseType_t created = xTaskCreate(&GatewayCache::TaskEntry, "gateway_cache",
config_.task_stack_size, this, config_.task_priority,
&task_handle_);
if (created != pdPASS) {
task_handle_ = nullptr;
ESP_LOGE(kTag, "failed to create cache task");
return ESP_ERR_NO_MEM;
}
ESP_LOGI(kTag, "cache started namespace=%s flush_interval_ms=%u",
config_.storage_namespace.c_str(), static_cast<unsigned>(config_.flush_interval_ms));
return ESP_OK;
}
void GatewayCache::preloadChannel(uint8_t gateway_id) {
LockGuard guard(lock_);
ensureSceneStoreLocked(gateway_id);
ensureGroupStoreLocked(gateway_id);
}
GatewayCache::SceneStore GatewayCache::scenes(uint8_t gateway_id) {
LockGuard guard(lock_);
return ensureSceneStoreLocked(gateway_id);
}
GatewayCache::GroupStore GatewayCache::groups(uint8_t gateway_id) {
LockGuard guard(lock_);
return ensureGroupStoreLocked(gateway_id);
}
GatewayCache::SceneEntry GatewayCache::scene(uint8_t gateway_id, uint8_t scene_id) {
LockGuard guard(lock_);
if (scene_id >= 16) {
return {};
}
return ensureSceneStoreLocked(gateway_id)[scene_id];
}
GatewayCache::GroupEntry GatewayCache::group(uint8_t gateway_id, uint8_t group_id) {
LockGuard guard(lock_);
if (group_id >= 16) {
return {};
}
return ensureGroupStoreLocked(gateway_id)[group_id];
}
bool GatewayCache::setSceneEnabled(uint8_t gateway_id, uint8_t scene_id, bool enabled) {
LockGuard guard(lock_);
if (scene_id >= 16) {
return false;
}
auto& entry = ensureSceneStoreLocked(gateway_id)[scene_id];
if (entry.enabled == enabled) {
return true;
}
entry.enabled = enabled;
dirty_ = true;
return true;
}
bool GatewayCache::setSceneDetail(uint8_t gateway_id, uint8_t scene_id, uint8_t brightness,
uint8_t color_mode, uint8_t data1, uint8_t data2,
uint8_t data3) {
LockGuard guard(lock_);
if (scene_id >= 16) {
return false;
}
auto& entry = ensureSceneStoreLocked(gateway_id)[scene_id];
if (entry.brightness == brightness && entry.color_mode == color_mode && entry.data1 == data1 &&
entry.data2 == data2 && entry.data3 == data3) {
return true;
}
entry.brightness = brightness;
entry.color_mode = color_mode;
entry.data1 = data1;
entry.data2 = data2;
entry.data3 = data3;
dirty_ = true;
return true;
}
bool GatewayCache::setSceneName(uint8_t gateway_id, uint8_t scene_id, std::string_view name) {
LockGuard guard(lock_);
if (scene_id >= 16) {
return false;
}
auto& entry = ensureSceneStoreLocked(gateway_id)[scene_id];
const auto normalized = NormalizeName(name);
if (entry.name == normalized) {
return true;
}
entry.name = normalized;
dirty_ = true;
return true;
}
bool GatewayCache::deleteScene(uint8_t gateway_id, uint8_t scene_id) {
LockGuard guard(lock_);
if (scene_id >= 16) {
return false;
}
auto& entry = ensureSceneStoreLocked(gateway_id)[scene_id];
if (IsDefaultScene(entry) && entry.name.empty()) {
return true;
}
entry = SceneEntry{};
dirty_ = true;
return true;
}
std::pair<uint8_t, uint8_t> GatewayCache::sceneMask(uint8_t gateway_id) {
LockGuard guard(lock_);
const auto& store = ensureSceneStoreLocked(gateway_id);
uint16_t mask = 0;
for (size_t index = 0; index < store.size(); ++index) {
if (store[index].enabled) {
mask |= static_cast<uint16_t>(1U << index);
}
}
return {static_cast<uint8_t>(mask & 0xff), static_cast<uint8_t>((mask >> 8) & 0xff)};
}
bool GatewayCache::setGroupEnabled(uint8_t gateway_id, uint8_t group_id, bool enabled) {
LockGuard guard(lock_);
if (group_id >= 16) {
return false;
}
auto& entry = ensureGroupStoreLocked(gateway_id)[group_id];
if (entry.enabled == enabled) {
return true;
}
entry.enabled = enabled;
dirty_ = true;
return true;
}
bool GatewayCache::setGroupDetail(uint8_t gateway_id, uint8_t group_id, uint8_t target_type,
uint8_t target_value) {
LockGuard guard(lock_);
if (group_id >= 16) {
return false;
}
auto& entry = ensureGroupStoreLocked(gateway_id)[group_id];
if (entry.target_type == target_type && entry.target_value == target_value) {
return true;
}
entry.target_type = target_type;
entry.target_value = target_value;
dirty_ = true;
return true;
}
bool GatewayCache::setGroupName(uint8_t gateway_id, uint8_t group_id, std::string_view name) {
LockGuard guard(lock_);
if (group_id >= 16) {
return false;
}
auto& entry = ensureGroupStoreLocked(gateway_id)[group_id];
const auto normalized = NormalizeName(name);
if (entry.name == normalized) {
return true;
}
entry.name = normalized;
dirty_ = true;
return true;
}
bool GatewayCache::deleteGroup(uint8_t gateway_id, uint8_t group_id) {
LockGuard guard(lock_);
if (group_id >= 16) {
return false;
}
auto& entry = ensureGroupStoreLocked(gateway_id)[group_id];
if (IsDefaultGroup(entry) && entry.name.empty()) {
return true;
}
entry = GroupEntry{};
dirty_ = true;
return true;
}
std::pair<uint8_t, uint8_t> GatewayCache::groupMask(uint8_t gateway_id) {
LockGuard guard(lock_);
const auto& store = ensureGroupStoreLocked(gateway_id);
uint16_t mask = 0;
for (size_t index = 0; index < store.size(); ++index) {
if (store[index].enabled) {
mask |= static_cast<uint16_t>(1U << index);
}
}
return {static_cast<uint8_t>(mask & 0xff), static_cast<uint8_t>((mask >> 8) & 0xff)};
}
GatewayCacheChannelFlags GatewayCache::channelFlags(uint8_t gateway_id) {
LockGuard guard(lock_);
return channel_flags_[gateway_id];
}
void GatewayCache::markGroupUpdateNeeded(uint8_t gateway_id, bool needed) {
LockGuard guard(lock_);
channel_flags_[gateway_id].need_update_group = needed;
}
void GatewayCache::markSceneUpdateNeeded(uint8_t gateway_id, bool needed) {
LockGuard guard(lock_);
channel_flags_[gateway_id].need_update_scene = needed;
}
void GatewayCache::markSettingsUpdateNeeded(uint8_t gateway_id, bool needed) {
LockGuard guard(lock_);
channel_flags_[gateway_id].need_update_settings = needed;
}
GatewayCachePriorityMode GatewayCache::priorityMode() {
LockGuard guard(lock_);
return priority_mode_;
}
void GatewayCache::setPriorityMode(GatewayCachePriorityMode mode) {
LockGuard guard(lock_);
priority_mode_ = mode;
}
void GatewayCache::TaskEntry(void* arg) {
static_cast<GatewayCache*>(arg)->taskLoop();
}
void GatewayCache::taskLoop() {
const TickType_t interval_ticks =
std::max<TickType_t>(1, pdMS_TO_TICKS(config_.flush_interval_ms));
while (true) {
vTaskDelay(interval_ticks);
flushDirty();
}
}
bool GatewayCache::flushDirty() {
LockGuard guard(lock_);
if (!dirty_) {
return true;
}
if (!openStorageLocked()) {
return false;
}
for (const auto& [gateway_id, store] : scenes_) {
for (uint8_t scene_id = 0; scene_id < store.size(); ++scene_id) {
const auto& entry = store[scene_id];
if (!IsDefaultScene(entry)) {
if (!writeStringLocked(ShortKey("sc", gateway_id, scene_id), BuildScenePayload(entry))) {
return false;
}
} else if (!eraseKeyLocked(ShortKey("sc", gateway_id, scene_id))) {
return false;
}
if (!entry.name.empty()) {
if (!writeStringLocked(ShortKey("sn", gateway_id, scene_id), entry.name)) {
return false;
}
} else if (!eraseKeyLocked(ShortKey("sn", gateway_id, scene_id))) {
return false;
}
}
}
for (const auto& [gateway_id, store] : groups_) {
for (uint8_t group_id = 0; group_id < store.size(); ++group_id) {
const auto& entry = store[group_id];
if (!IsDefaultGroup(entry)) {
if (!writeStringLocked(ShortKey("gr", gateway_id, group_id), BuildGroupPayload(entry))) {
return false;
}
} else if (!eraseKeyLocked(ShortKey("gr", gateway_id, group_id))) {
return false;
}
if (!entry.name.empty()) {
if (!writeStringLocked(ShortKey("gn", gateway_id, group_id), entry.name)) {
return false;
}
} else if (!eraseKeyLocked(ShortKey("gn", gateway_id, group_id))) {
return false;
}
}
}
const esp_err_t commit_err = nvs_commit(storage_);
if (commit_err != ESP_OK) {
ESP_LOGE(kTag, "cache commit failed: %s", esp_err_to_name(commit_err));
return false;
}
dirty_ = false;
return true;
}
bool GatewayCache::openStorageLocked() {
if (storage_ != 0) {
return true;
}
const esp_err_t err = nvs_open(config_.storage_namespace.c_str(), NVS_READWRITE, &storage_);
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to open cache storage: %s", esp_err_to_name(err));
return false;
}
return true;
}
void GatewayCache::closeStorageLocked() {
if (storage_ != 0) {
nvs_close(storage_);
storage_ = 0;
}
}
GatewayCache::SceneStore& GatewayCache::ensureSceneStoreLocked(uint8_t gateway_id) {
auto [it, inserted] = scenes_.try_emplace(gateway_id);
if (inserted) {
loadSceneStoreLocked(gateway_id, it->second);
}
return it->second;
}
GatewayCache::GroupStore& GatewayCache::ensureGroupStoreLocked(uint8_t gateway_id) {
auto [it, inserted] = groups_.try_emplace(gateway_id);
if (inserted) {
loadGroupStoreLocked(gateway_id, it->second);
}
return it->second;
}
void GatewayCache::loadSceneStoreLocked(uint8_t gateway_id, SceneStore& scenes) {
for (uint8_t scene_id = 0; scene_id < scenes.size(); ++scene_id) {
const auto raw = readStringLocked(ShortKey("sc", gateway_id, scene_id));
const auto values = ParseCsv(raw);
if (values.size() >= 6) {
scenes[scene_id].enabled = values[0] != 0;
scenes[scene_id].brightness = static_cast<uint8_t>(std::clamp(values[1], 0, 254));
scenes[scene_id].color_mode = static_cast<uint8_t>(std::clamp(values[2], 0, 2));
scenes[scene_id].data1 = static_cast<uint8_t>(std::clamp(values[3], 0, 255));
scenes[scene_id].data2 = static_cast<uint8_t>(std::clamp(values[4], 0, 255));
scenes[scene_id].data3 = static_cast<uint8_t>(std::clamp(values[5], 0, 255));
}
scenes[scene_id].name = NormalizeName(readStringLocked(ShortKey("sn", gateway_id, scene_id)));
}
}
void GatewayCache::loadGroupStoreLocked(uint8_t gateway_id, GroupStore& groups) {
for (uint8_t group_id = 0; group_id < groups.size(); ++group_id) {
const auto raw = readStringLocked(ShortKey("gr", gateway_id, group_id));
const auto values = ParseCsv(raw);
if (values.size() >= 2) {
groups[group_id].enabled = values[0] != 0;
uint8_t target_type = static_cast<uint8_t>(std::clamp(values[1], 0, 2));
uint8_t target_value = values.size() >= 3 ? static_cast<uint8_t>(std::clamp(values[2], 0, 255))
: target_type;
if (values.size() < 3) {
target_type = 2;
}
groups[group_id].target_type = target_type;
groups[group_id].target_value = target_type == 0
? static_cast<uint8_t>(std::min<int>(target_value, 63))
: target_type == 1
? static_cast<uint8_t>(std::min<int>(target_value, 15))
: 0;
}
groups[group_id].name = NormalizeName(readStringLocked(ShortKey("gn", gateway_id, group_id)));
}
}
std::string GatewayCache::readStringLocked(std::string_view key) {
if (!openStorageLocked()) {
return {};
}
size_t required_size = 0;
const esp_err_t err = nvs_get_str(storage_, std::string(key).c_str(), nullptr, &required_size);
if (err != ESP_OK || required_size == 0) {
return {};
}
std::string value(required_size - 1, '\0');
if (nvs_get_str(storage_, std::string(key).c_str(), value.data(), &required_size) != ESP_OK) {
return {};
}
return value;
}
bool GatewayCache::writeStringLocked(std::string_view key, std::string_view value) {
if (storage_ == 0) {
return false;
}
const esp_err_t err = nvs_set_str(storage_, std::string(key).c_str(), std::string(value).c_str());
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to write cache key=%s err=%s", std::string(key).c_str(),
esp_err_to_name(err));
return false;
}
return true;
}
bool GatewayCache::eraseKeyLocked(std::string_view key) {
if (storage_ == 0) {
return false;
}
const esp_err_t err = nvs_erase_key(storage_, std::string(key).c_str());
if (err == ESP_ERR_NVS_NOT_FOUND) {
return true;
}
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to erase cache key=%s err=%s", std::string(key).c_str(),
esp_err_to_name(err));
return false;
}
return true;
}
} // namespace gateway