feat: implement status monitoring, fault detection, and comms watchdog

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 19:20:23 +03:00
parent 73407b8272
commit 0c0883480d
11 changed files with 707 additions and 10 deletions

333
test/status_monitor_test.c Normal file
View File

@@ -0,0 +1,333 @@
#include "unity.h"
#include "status_monitor.h"
#include "midea_protocol.h"
#include "uart_driver.h"
#include <string.h>
void setUp(void) {}
void tearDown(void) {}
// Build a valid MideaUART status response frame for testing.
// data[0..7] follow the decoder layout:
// d0: (mode & 0x0F) | (power << 4)
// d1,d2: indoor_temp (LE)
// d3,d4: target_temp (LE)
// d5: (fan & 0x0F) | (error << 4)
// d6: alarm low, d7: alarm high
static size_t build_status_frame(uint8_t *buf, uint8_t mode, uint8_t power,
int16_t indoor, int16_t target,
uint8_t fan, uint8_t error, uint16_t alarm) {
size_t o = 0;
buf[o++] = 0xAA;
buf[o++] = 0x55;
buf[o++] = 8; // length
buf[o++] = 0x07; // status command
buf[o++] = (mode & 0x0F) | ((power & 0x01) << 4);
buf[o++] = indoor & 0xFF;
buf[o++] = (indoor >> 8) & 0xFF;
buf[o++] = target & 0xFF;
buf[o++] = (target >> 8) & 0xFF;
buf[o++] = (fan & 0x0F) | ((error & 0x0F) << 4);
buf[o++] = alarm & 0xFF;
buf[o++] = (alarm >> 8) & 0xFF;
uint8_t chk = 0;
for (size_t i = 2; i < o; i++) chk ^= buf[i];
buf[o++] = chk;
return o;
}
void test_status_monitor_init_defaults(void) {
status_monitor_t m;
TEST_ASSERT_TRUE(status_monitor_init(&m, NULL));
TEST_ASSERT_TRUE(m.initialized);
TEST_ASSERT_EQUAL_UINT32(5000, m.config.poll_interval_ms);
TEST_ASSERT_EQUAL_UINT32(1000, m.config.timeout_ms);
TEST_ASSERT_EQUAL_UINT8(3, m.config.max_retries);
TEST_ASSERT_FALSE(m.last_poll_success);
TEST_ASSERT_FALSE(m.fault_detected);
}
void test_status_monitor_init_custom_config(void) {
status_monitor_config_t cfg = { .poll_interval_ms = 2000, .timeout_ms = 500, .max_retries = 5 };
status_monitor_t m;
TEST_ASSERT_TRUE(status_monitor_init(&m, &cfg));
TEST_ASSERT_EQUAL_UINT32(2000, m.config.poll_interval_ms);
TEST_ASSERT_EQUAL_UINT32(500, m.config.timeout_ms);
TEST_ASSERT_EQUAL_UINT8(5, m.config.max_retries);
}
void test_status_monitor_init_null(void) {
TEST_ASSERT_FALSE(status_monitor_init(NULL, NULL));
}
void test_build_request_frame(void) {
uint8_t req[8];
size_t len = status_monitor_build_request(req, sizeof(req));
TEST_ASSERT_EQUAL_UINT(5, len);
TEST_ASSERT_EQUAL_UINT8(0xAA, req[0]);
TEST_ASSERT_EQUAL_UINT8(0x55, req[1]);
TEST_ASSERT_EQUAL_UINT8(0x07, req[3]);
// checksum = len ^ cmd = 1 ^ 0x07
TEST_ASSERT_EQUAL_UINT8((uint8_t)(1 ^ 0x07), req[4]);
}
void test_build_request_buffer_too_small(void) {
uint8_t req[3];
TEST_ASSERT_EQUAL_UINT(0, status_monitor_build_request(req, sizeof(req)));
TEST_ASSERT_EQUAL_UINT(0, status_monitor_build_request(NULL, 8));
}
void test_process_response_success(void) {
status_monitor_t m;
status_monitor_init(&m, NULL);
uint8_t frame[16];
size_t len = build_status_frame(frame, MODE_COOL, 1, 2300, 2400, 2, 0, 0);
midea_status_t status;
TEST_ASSERT_TRUE(status_monitor_process_response(&m, frame, len, 1000, &status));
TEST_ASSERT_EQUAL_UINT8(MODE_COOL, status.mode);
TEST_ASSERT_EQUAL_INT16(2300, status.indoor_temp);
TEST_ASSERT_EQUAL_INT16(2400, status.target_temp);
TEST_ASSERT_TRUE(m.last_poll_success);
TEST_ASSERT_EQUAL_UINT32(1000, m.last_poll_time);
TEST_ASSERT_EQUAL_UINT8(0, m.retry_count);
TEST_ASSERT_FALSE(m.fault_detected);
}
void test_process_response_decode_failure(void) {
status_monitor_t m;
status_monitor_init(&m, NULL);
// Bad header -> decode fails -> counts as error.
uint8_t bad[12] = { 0x00, 0x00, 8, 0x07, 0, 0, 0, 0, 0, 0, 0, 0 };
midea_status_t status;
TEST_ASSERT_FALSE(status_monitor_process_response(&m, bad, sizeof(bad), 500, &status));
TEST_ASSERT_FALSE(m.last_poll_success);
TEST_ASSERT_EQUAL_UINT8(1, m.retry_count);
TEST_ASSERT_EQUAL_UINT32(1, m.error_count);
}
void test_process_response_null_buffer(void) {
status_monitor_t m;
status_monitor_init(&m, NULL);
midea_status_t status;
TEST_ASSERT_FALSE(status_monitor_process_response(&m, NULL, 0, 100, &status));
TEST_ASSERT_EQUAL_UINT8(1, m.retry_count);
}
void test_fault_detection_error_code(void) {
status_monitor_t m;
status_monitor_init(&m, NULL);
uint8_t frame[16];
size_t len = build_status_frame(frame, MODE_COOL, 1, 2300, 2400, 2, 3, 0);
midea_status_t status;
TEST_ASSERT_TRUE(status_monitor_process_response(&m, frame, len, 1000, &status));
TEST_ASSERT_EQUAL_UINT8(3, status.error_code);
TEST_ASSERT_TRUE(status_monitor_has_fault(&m));
TEST_ASSERT_TRUE(m.fault_code != 0);
}
void test_fault_detection_alarm_mask(void) {
status_monitor_t m;
status_monitor_init(&m, NULL);
uint8_t frame[16];
size_t len = build_status_frame(frame, MODE_HEAT, 1, 2000, 2100, 1, 0, 0x0004);
midea_status_t status;
TEST_ASSERT_TRUE(status_monitor_process_response(&m, frame, len, 1000, &status));
TEST_ASSERT_EQUAL_UINT16(0x0004, status.alarm_mask);
TEST_ASSERT_TRUE(status_monitor_has_fault(&m));
}
void test_fault_cleared_on_good_status(void) {
status_monitor_t m;
status_monitor_init(&m, NULL);
uint8_t frame[16];
// First a faulty status.
size_t len = build_status_frame(frame, MODE_COOL, 1, 2300, 2400, 2, 5, 0);
midea_status_t status;
status_monitor_process_response(&m, frame, len, 1000, &status);
TEST_ASSERT_TRUE(status_monitor_has_fault(&m));
// Then a clean status clears the fault.
len = build_status_frame(frame, MODE_COOL, 1, 2300, 2400, 2, 0, 0);
status_monitor_process_response(&m, frame, len, 2000, &status);
TEST_ASSERT_FALSE(status_monitor_has_fault(&m));
TEST_ASSERT_EQUAL_UINT16(0, m.fault_code);
}
void test_handle_error_trips_watchdog(void) {
status_monitor_config_t cfg = { .poll_interval_ms = 5000, .timeout_ms = 1000, .max_retries = 3 };
status_monitor_t m;
status_monitor_init(&m, &cfg);
status_monitor_handle_error(&m);
TEST_ASSERT_EQUAL_UINT8(1, m.retry_count);
TEST_ASSERT_FALSE(m.comm_timeout);
status_monitor_handle_error(&m);
TEST_ASSERT_EQUAL_UINT8(2, m.retry_count);
TEST_ASSERT_FALSE(m.comm_timeout);
status_monitor_handle_error(&m);
TEST_ASSERT_EQUAL_UINT8(3, m.retry_count);
TEST_ASSERT_TRUE(m.comm_timeout);
TEST_ASSERT_EQUAL_UINT32(3, m.error_count);
}
void test_check_timeout_never_polled(void) {
status_monitor_t m;
status_monitor_init(&m, NULL);
TEST_ASSERT_TRUE(status_monitor_check_timeout(&m, 0));
TEST_ASSERT_TRUE(status_monitor_check_timeout(&m, 100000));
}
void test_check_timeout_within_window(void) {
status_monitor_t m;
status_monitor_init(&m, NULL); // window = 5000 + 1000 = 6000
uint8_t frame[16];
size_t len = build_status_frame(frame, MODE_COOL, 1, 2300, 2400, 2, 0, 0);
midea_status_t status;
status_monitor_process_response(&m, frame, len, 1000, &status);
// 1000 -> 5000 : within 6000ms window
TEST_ASSERT_FALSE(status_monitor_check_timeout(&m, 5000));
}
void test_check_timeout_exceeded(void) {
status_monitor_t m;
status_monitor_init(&m, NULL); // window = 6000
uint8_t frame[16];
size_t len = build_status_frame(frame, MODE_COOL, 1, 2300, 2400, 2, 0, 0);
midea_status_t status;
status_monitor_process_response(&m, frame, len, 1000, &status);
// 1000 -> 8000 = 7000ms elapsed > 6000ms window
TEST_ASSERT_TRUE(status_monitor_check_timeout(&m, 8000));
}
void test_check_timeout_uninitialized(void) {
status_monitor_t m;
memset(&m, 0, sizeof(m));
TEST_ASSERT_TRUE(status_monitor_check_timeout(&m, 100));
TEST_ASSERT_TRUE(status_monitor_check_timeout(NULL, 100));
}
void test_poll_uart_no_data(void) {
// The mock UART receive returns 0 bytes -> poll should fail and count error.
status_monitor_t m;
status_monitor_init(&m, NULL);
uart_config_t ucfg = { .tx_pin = 1, .rx_pin = 2, .baud_rate = 9600,
.data_bits = 8, .parity = 0, .stop_bits = 1 };
uart_driver_t uart;
TEST_ASSERT_TRUE(uart_driver_init(&uart, &ucfg));
midea_status_t status;
TEST_ASSERT_FALSE(status_monitor_poll(&m, &uart, 1000, &status));
TEST_ASSERT_FALSE(m.last_poll_success);
TEST_ASSERT_EQUAL_UINT8(1, m.retry_count);
TEST_ASSERT_EQUAL_UINT32(1000, m.last_attempt_time);
}
void test_poll_null_args(void) {
status_monitor_t m;
status_monitor_init(&m, NULL);
midea_status_t status;
TEST_ASSERT_FALSE(status_monitor_poll(&m, NULL, 0, &status));
TEST_ASSERT_FALSE(status_monitor_poll(NULL, NULL, 0, &status));
}
void test_map_to_zcl_modes(void) {
midea_status_t s;
zcl_thermostat_attrs_t z;
memset(&s, 0, sizeof(s));
// Power off -> Off regardless of mode
s.power_state = 0; s.mode = MODE_COOL; s.indoor_temp = 2500;
status_monitor_map_to_zcl(&s, &z);
TEST_ASSERT_EQUAL_UINT8(0x00, z.system_mode);
TEST_ASSERT_EQUAL_INT16(2500, z.local_temperature);
s.power_state = 1;
s.mode = MODE_COOL; status_monitor_map_to_zcl(&s, &z);
TEST_ASSERT_EQUAL_UINT8(0x03, z.system_mode);
s.mode = MODE_HEAT; status_monitor_map_to_zcl(&s, &z);
TEST_ASSERT_EQUAL_UINT8(0x04, z.system_mode);
s.mode = MODE_AUTO; status_monitor_map_to_zcl(&s, &z);
TEST_ASSERT_EQUAL_UINT8(0x01, z.system_mode);
s.mode = MODE_DRY; status_monitor_map_to_zcl(&s, &z);
TEST_ASSERT_EQUAL_UINT8(0x08, z.system_mode);
}
void test_get_last_status_and_zcl(void) {
status_monitor_t m;
status_monitor_init(&m, NULL);
uint8_t frame[16];
size_t len = build_status_frame(frame, MODE_HEAT, 1, 2100, 2200, 3, 0, 0);
midea_status_t status;
status_monitor_process_response(&m, frame, len, 1000, &status);
midea_status_t got;
TEST_ASSERT_TRUE(status_monitor_get_last_status(&m, &got));
TEST_ASSERT_EQUAL_UINT8(MODE_HEAT, got.mode);
TEST_ASSERT_EQUAL_INT16(2100, got.indoor_temp);
zcl_thermostat_attrs_t z;
TEST_ASSERT_TRUE(status_monitor_get_last_zcl_attrs(&m, &z));
TEST_ASSERT_EQUAL_UINT8(0x04, z.system_mode);
TEST_ASSERT_EQUAL_INT16(2100, z.local_temperature);
TEST_ASSERT_FALSE(status_monitor_get_last_status(&m, NULL));
TEST_ASSERT_FALSE(status_monitor_get_last_status(NULL, &got));
}
void test_successful_poll_resets_retry(void) {
status_monitor_t m;
status_monitor_init(&m, NULL);
// Accumulate errors
status_monitor_handle_error(&m);
status_monitor_handle_error(&m);
TEST_ASSERT_EQUAL_UINT8(2, m.retry_count);
// A good response resets retry_count and clears comm_timeout
uint8_t frame[16];
size_t len = build_status_frame(frame, MODE_COOL, 1, 2300, 2400, 2, 0, 0);
midea_status_t status;
TEST_ASSERT_TRUE(status_monitor_process_response(&m, frame, len, 3000, &status));
TEST_ASSERT_EQUAL_UINT8(0, m.retry_count);
TEST_ASSERT_FALSE(m.comm_timeout);
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_status_monitor_init_defaults);
RUN_TEST(test_status_monitor_init_custom_config);
RUN_TEST(test_status_monitor_init_null);
RUN_TEST(test_build_request_frame);
RUN_TEST(test_build_request_buffer_too_small);
RUN_TEST(test_process_response_success);
RUN_TEST(test_process_response_decode_failure);
RUN_TEST(test_process_response_null_buffer);
RUN_TEST(test_fault_detection_error_code);
RUN_TEST(test_fault_detection_alarm_mask);
RUN_TEST(test_fault_cleared_on_good_status);
RUN_TEST(test_handle_error_trips_watchdog);
RUN_TEST(test_check_timeout_never_polled);
RUN_TEST(test_check_timeout_within_window);
RUN_TEST(test_check_timeout_exceeded);
RUN_TEST(test_check_timeout_uninitialized);
RUN_TEST(test_poll_uart_no_data);
RUN_TEST(test_poll_null_args);
RUN_TEST(test_map_to_zcl_modes);
RUN_TEST(test_get_last_status_and_zcl);
RUN_TEST(test_successful_poll_resets_retry);
return UNITY_END();
}