Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- [x] Project the detected inventory with a 20-device cap, ROMs and independent temperature validity.
- [x] Add native tests for compatibility, text boundaries, full/empty inventory, diagnostics and copy semantics.
- [x] Document the minimal dependency graph, current #218 progress, #170 ownership gate and sensor projection bridge.
- [x] Add a bounded, allocation-free `ControllerCommandQueue` for adapter-to-loop hand-over.
- [ ] In #216/#217, verify the expanded payloads and transport resource budgets.
- [ ] In #218, validate Web/legacy NTP input without silent truncation and complete owner/adapter regressions.
- [ ] In #218/#220, verify both-bus inventory aggregation, deduplication, overflow and rescan replacement on the owning task.
Expand Down
88 changes: 88 additions & 0 deletions src/ControllerCommandQueue.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) 2018-2026 Smart Swimming Pool, Stephan Strittmatter
// SPDX-License-Identifier: MIT

/**
* @file ControllerCommandQueue.hpp
* @brief Bounded hand-over queue for typed controller commands.
*/

#pragma once

#include <cstddef>

#include "ControllerCommand.hpp"

#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
#include <freertos/FreeRTOS.h>
#include <freertos/portmacro.h>
#else
#include <mutex>
#endif

namespace PoolController {

/**
* @brief Fixed-capacity queue for commands crossing callback/task boundaries.
*
* The queue owns copies of trivially-copyable `ControllerCommand` values and
* never allocates memory. It is safe for multiple adapter producers and a
* single Core-1 application consumer.
*/
class ControllerCommandQueue final {
public:
static constexpr std::size_t kCapacity = 16;

enum class PushResult : std::uint8_t {
OK,
FULL,
};

PushResult push(const ControllerCommand &command) {
lock();
if (count_ == kCapacity) {
unlock();
return PushResult::FULL;
}
buffer_[(head_ + count_) % kCapacity] = command;
++count_;
unlock();
return PushResult::OK;
}

bool pop(ControllerCommand &out) {
lock();
if (count_ == 0) {
unlock();
return false;
}
out = buffer_[head_];
head_ = (head_ + 1) % kCapacity;
--count_;
unlock();
return true;
}

std::size_t size() {
lock();
const std::size_t result = count_;
unlock();
return result;
}

private:
#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
portMUX_TYPE mux_ = portMUX_INITIALIZER_UNLOCKED;
void lock() { portENTER_CRITICAL(&mux_); }
void unlock() { portEXIT_CRITICAL(&mux_); }
#else
std::mutex mutex_;
void lock() { mutex_.lock(); }
void unlock() { mutex_.unlock(); }
#endif

ControllerCommand buffer_[kCapacity]{};
std::size_t head_{0};
std::size_t count_{0};
};

} // namespace PoolController
1 change: 1 addition & 0 deletions test/native/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ set(TEST_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_mqtt_command_queue.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_controller_contracts.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_controller_snapshot_diagnostics.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_controller_command_queue.cpp
)

add_executable(test_runner
Expand Down
103 changes: 103 additions & 0 deletions test/native/tests/test_controller_command_queue.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) 2018-2026 Smart Swimming Pool, Stephan Strittmatter
// SPDX-License-Identifier: MIT

/**
* @file test_controller_command_queue.cpp
* @brief Tests for the bounded typed controller command queue.
*/

#include "ControllerCommandQueue.hpp"

extern void test_begin(const char *suite, const char *name);
extern void test_pass(const char *file, int line);
extern void test_fail(const char *file, int line, const char *msg);
extern void test_suite_end(const char *name, int passed, int failed);

#define ASSERT_TRUE(cond) \
do { \
if (!(cond)) { \
test_fail(__FILE__, __LINE__, "Expected true: " #cond); \
return 1; \
} \
test_pass(__FILE__, __LINE__); \
} while (0)

#define ASSERT_EQ(a, b) ASSERT_TRUE((a) == (b))

using PoolController::CommandSource;
using PoolController::ControllerCommand;
using PoolController::ControllerCommandQueue;
using PoolController::ControllerCommandType;
using PoolController::OperationMode;

static int test_fifo_order() {
test_begin("ControllerCommandQueue", "commands are returned in FIFO order");
ControllerCommandQueue queue;

ControllerCommand first{};
first.type = ControllerCommandType::SET_MODE;
first.source = CommandSource::MQTT;
first.mode = OperationMode::BOOST;

ControllerCommand second{};
second.type = ControllerCommandType::SET_POOL_MAX_TEMPERATURE;
second.source = CommandSource::WEB;
second.value = 29.5F;

ASSERT_EQ(queue.push(first), ControllerCommandQueue::PushResult::OK);
ASSERT_EQ(queue.push(second), ControllerCommandQueue::PushResult::OK);
ASSERT_EQ(queue.size(), 2U);

ControllerCommand out{};
ASSERT_TRUE(queue.pop(out));
ASSERT_EQ(out.type, ControllerCommandType::SET_MODE);
ASSERT_EQ(out.source, CommandSource::MQTT);
ASSERT_EQ(out.mode, OperationMode::BOOST);

ASSERT_TRUE(queue.pop(out));
ASSERT_EQ(out.type, ControllerCommandType::SET_POOL_MAX_TEMPERATURE);
ASSERT_EQ(out.source, CommandSource::WEB);
ASSERT_EQ(out.value, 29.5F);
ASSERT_TRUE(!queue.pop(out));
return 0;
}

static int test_full_queue_and_wraparound() {
test_begin("ControllerCommandQueue", "full queue rejects and reuses released slots");
ControllerCommandQueue queue;
ControllerCommand command{};
command.type = ControllerCommandType::SET_MODE;

for (std::size_t i = 0; i < ControllerCommandQueue::kCapacity; ++i) {
command.value = static_cast<float>(i);
ASSERT_EQ(queue.push(command), ControllerCommandQueue::PushResult::OK);
}
ASSERT_EQ(queue.push(command), ControllerCommandQueue::PushResult::FULL);

ControllerCommand out{};
ASSERT_TRUE(queue.pop(out));
command.value = 99.0F;
ASSERT_EQ(queue.push(command), ControllerCommandQueue::PushResult::OK);

std::size_t drained = 0;
while (queue.pop(out)) {
++drained;
}
ASSERT_EQ(drained, ControllerCommandQueue::kCapacity);
return 0;
}

int run_controller_command_queue_tests() {
int passed = 0;
int failed = 0;
int (*tests[])() = {test_fifo_order, test_full_queue_and_wraparound};
for (auto test : tests) {
if (test() == 0) {
++passed;
} else {
++failed;
}
}
test_suite_end("ControllerCommandQueue", passed, failed);
return failed;
}
2 changes: 2 additions & 0 deletions test/native/tests/test_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ extern int run_calibration_manager_tests();
extern int run_mqtt_command_queue_tests();
extern int run_controller_contract_tests();
extern int run_controller_snapshot_diagnostics_tests();
extern int run_controller_command_queue_tests();

int main() {
printf("\n══════════════════════════════════════════════════\n");
Expand All @@ -136,6 +137,7 @@ int main() {
total += run_mqtt_command_queue_tests();
total += run_controller_contract_tests();
total += run_controller_snapshot_diagnostics_tests();
total += run_controller_command_queue_tests();

printf("\n══════════════════════════════════════════════════\n");
printf(" Results: %d suites passed, %d suites failed\n", g_testsPassed, g_testsFailed);
Expand Down
Loading