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

View File

@@ -1,6 +1,6 @@
# Makefile for Ballu AC ESP32-C6 Controller # Makefile for Ballu AC ESP32-C6 Controller
.PHONY: all build test clean test-uart test-midea-protocol test-zigbee-zcl test-integration-layer help .PHONY: all build test clean test-uart test-midea-protocol test-zigbee-zcl test-integration-layer test-status-monitor help
# Directories # Directories
SRC_DIR := src SRC_DIR := src
@@ -31,7 +31,7 @@ $(BUILD_DIR)/app: $(SRC_FILES)
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
# Build and run tests # Build and run tests
test: test-uart test-midea-protocol test-zigbee-zcl test-integration-layer test: test-uart test-midea-protocol test-zigbee-zcl test-integration-layer test-status-monitor
@echo "All tests passed!" @echo "All tests passed!"
# Build individual test executables # Build individual test executables
@@ -51,6 +51,10 @@ $(TEST_DIR)/integration_layer_test: $(TEST_DIR)/integration_layer_test.c $(SRC_F
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $< $(SRC_FILES) $(UNITY_SRC) -o $@ $(LDFLAGS) $(CC) $(CFLAGS) $< $(SRC_FILES) $(UNITY_SRC) -o $@ $(LDFLAGS)
$(TEST_DIR)/status_monitor_test: $(TEST_DIR)/status_monitor_test.c $(SRC_FILES) $(UNITY_SRC)
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $< $(SRC_FILES) $(UNITY_SRC) -o $@ $(LDFLAGS)
# Run individual tests # Run individual tests
test-uart: $(TEST_DIR)/uart_driver_test test-uart: $(TEST_DIR)/uart_driver_test
@echo "Running UART driver tests..." @echo "Running UART driver tests..."
@@ -68,6 +72,10 @@ test-integration-layer: $(TEST_DIR)/integration_layer_test
@echo "Running integration layer tests..." @echo "Running integration layer tests..."
@./$(TEST_DIR)/integration_layer_test @./$(TEST_DIR)/integration_layer_test
test-status-monitor: $(TEST_DIR)/status_monitor_test
@echo "Running status monitor tests..."
@./$(TEST_DIR)/status_monitor_test
# Clean build artifacts # Clean build artifacts
clean: clean:
rm -rf $(BUILD_DIR) rm -rf $(BUILD_DIR)

View File

@@ -11,7 +11,7 @@ This project implements a bridge between Zigbee (Home Assistant) and UART-based
- UART communication at 9600 baud 8N1 for MideaUART protocol - UART communication at 9600 baud 8N1 for MideaUART protocol
- Zigbee ZCL Thermostat cluster implementation - Zigbee ZCL Thermostat cluster implementation
- Bidirectional communication between Zigbee and UART layers - Bidirectional communication between Zigbee and UART layers
- Status monitoring and error handling - Status monitoring with periodic AC polling, fault detection, and a communication watchdog
- Configurable timing controls (50ms command spacing) - Configurable timing controls (50ms command spacing)
## Directory Structure ## Directory Structure
@@ -22,6 +22,7 @@ src/
midea_protocol.c/h - MideaUART protocol encoding/decoding midea_protocol.c/h - MideaUART protocol encoding/decoding
zigbee_zcl.c/h - Zigbee ZCL Thermostat cluster zigbee_zcl.c/h - Zigbee ZCL Thermostat cluster
integration_layer.c/h- Integration between Zigbee and UART layers integration_layer.c/h- Integration between Zigbee and UART layers
status_monitor.c/h - AC status polling, fault detection, and comms watchdog
main.c - Application entry point main.c - Application entry point
test/ test/
@@ -29,6 +30,7 @@ test/
midea_protocol_test.c - Unit tests for MideaUART protocol midea_protocol_test.c - Unit tests for MideaUART protocol
zigbee_zcl_test.c - Unit tests for Zigbee ZCL zigbee_zcl_test.c - Unit tests for Zigbee ZCL
integration_layer_test.c - Unit tests for integration layer integration_layer_test.c - Unit tests for integration layer
status_monitor_test.c - Unit tests for status monitoring
docs/ docs/
protocol.md - MideaUART protocol details protocol.md - MideaUART protocol details
@@ -62,6 +64,23 @@ make upload
make test make test
``` ```
## Status Monitoring
The `status_monitor` module (see `src/status_monitor.c/h`) provides:
- Periodic AC status polling: sends a MideaUART status-request frame over the UART
driver, receives the response, and decodes it into a `midea_status_t`.
- ZCL mapping: decoded status is mapped to ZCL Thermostat attributes
(`local_temperature`, `system_mode`) for Home Assistant reporting.
- Fault detection: any non-zero `error_code` or `alarm_mask` from the AC raises a
fault flag (`status_monitor_has_fault`) and records the fault code.
- Communication watchdog: tracks the time of the last successful poll and trips a
timeout when the poll interval (plus per-attempt timeout) is exceeded or when
`max_retries` consecutive attempts fail.
Configuration (`status_monitor_config_t`) exposes `poll_interval_ms` (default
5000ms), `timeout_ms` (default 1000ms), and `max_retries` (default 3).
## Implementation Progress ## Implementation Progress
See `docs/plans/2026-07-05-ballu-ac-esp32c6-controller-implementation.md` for detailed implementation plan and progress tracking. See `docs/plans/2026-07-05-ballu-ac-esp32c6-controller-implementation.md` for detailed implementation plan and progress tracking.

View File

@@ -62,13 +62,13 @@ Implementation of ESP32-C6 based AC controller that bridges Zigbee (Home Assista
- [x] Update Readme.md - [x] Update Readme.md
### Task 5: Status Monitoring and Feedback ### Task 5: Status Monitoring and Feedback
- [ ] Implement AC status polling via UART (temperature, mode, etc.) - [x] Implement AC status polling via UART (temperature, mode, etc.)
- [ ] Map MideaUART status to ZCL attributes for reporting - [x] Map MideaUART status to ZCL attributes for reporting
- [ ] Implement error handling and fault detection - [x] Implement error handling and fault detection
- [ ] Add watchdog for communication timeouts - [x] Add watchdog for communication timeouts
- [ ] Write unit tests for status monitoring and error paths - [x] Write unit tests for status monitoring and error paths
- [ ] Run tests - must pass before next task - [x] Run tests - must pass before next task
- [ ] Update Readme.md - [x] Update Readme.md
### Task 6: End-to-End Integration and Testing ### Task 6: End-to-End Integration and Testing
- [ ] Integrate all layers: Zigbee ←→ Integration ←→ UART - [ ] Integrate all layers: Zigbee ←→ Integration ←→ UART

259
src/status_monitor.c Normal file
View File

@@ -0,0 +1,259 @@
#include "status_monitor.h"
#include "uart_driver.h"
#include "midea_protocol.h"
#include <string.h>
// Default configuration values
#define DEFAULT_POLL_INTERVAL_MS 5000
#define DEFAULT_TIMEOUT_MS 1000
#define DEFAULT_MAX_RETRIES 3
// Maximum expected response frame size
#define STATUS_RESPONSE_MAX 32
bool status_monitor_init(status_monitor_t *monitor, const status_monitor_config_t *config) {
if (!monitor) {
return false;
}
memset(monitor, 0, sizeof(status_monitor_t));
monitor->initialized = true;
monitor->last_poll_success = false;
monitor->last_poll_time = 0;
monitor->last_attempt_time = 0;
monitor->retry_count = 0;
monitor->error_count = 0;
monitor->comm_timeout = false;
monitor->fault_detected = false;
monitor->fault_code = 0;
// Apply configuration with fallback to defaults
if (config) {
monitor->config.poll_interval_ms =
config->poll_interval_ms ? config->poll_interval_ms : DEFAULT_POLL_INTERVAL_MS;
monitor->config.timeout_ms =
config->timeout_ms ? config->timeout_ms : DEFAULT_TIMEOUT_MS;
monitor->config.max_retries =
config->max_retries ? config->max_retries : DEFAULT_MAX_RETRIES;
} else {
monitor->config.poll_interval_ms = DEFAULT_POLL_INTERVAL_MS;
monitor->config.timeout_ms = DEFAULT_TIMEOUT_MS;
monitor->config.max_retries = DEFAULT_MAX_RETRIES;
}
return true;
}
void status_monitor_deinit(status_monitor_t *monitor) {
if (monitor) {
memset(monitor, 0, sizeof(status_monitor_t));
monitor->initialized = false;
}
}
size_t status_monitor_build_request(uint8_t *buffer, size_t buffer_size) {
if (!buffer || buffer_size < 5) {
return 0;
}
// MideaUART status-request frame:
// [Header 0xAA 0x55][Length 1][Command 0x07 = query][Checksum]
size_t offset = 0;
buffer[offset++] = 0xAA;
buffer[offset++] = 0x55;
buffer[offset++] = 1; // length of command payload
buffer[offset++] = 0x07; // query/status-request command
// Checksum: XOR of length + command bytes
uint8_t checksum = 0;
for (size_t i = 2; i < offset; i++) {
checksum ^= buffer[i];
}
buffer[offset++] = checksum;
return offset;
}
// Internal: apply a freshly decoded status to monitor state.
static void status_monitor_apply_status(status_monitor_t *monitor,
const midea_status_t *status,
uint32_t current_time) {
monitor->last_poll_success = true;
monitor->last_poll_time = current_time;
monitor->retry_count = 0;
monitor->comm_timeout = false;
memcpy(&monitor->last_status, status, sizeof(midea_status_t));
// Fault detection: any error code or alarm bit indicates a fault.
if (status->error_code != 0 || status->alarm_mask != 0) {
monitor->fault_detected = true;
monitor->fault_code =
(uint16_t)(status->error_code) | status->alarm_mask;
} else {
monitor->fault_detected = false;
monitor->fault_code = 0;
}
// Keep ZCL attributes in sync for reporting.
status_monitor_map_to_zcl(status, &monitor->last_zcl_attrs);
}
bool status_monitor_process_response(status_monitor_t *monitor,
const uint8_t *buffer, size_t length,
uint32_t current_time, midea_status_t *status) {
if (!monitor || !monitor->initialized || !status) {
return false;
}
monitor->last_attempt_time = current_time;
midea_status_t decoded;
memset(&decoded, 0, sizeof(decoded));
if (!buffer || length == 0 || !midea_protocol_decode(buffer, length, &decoded)) {
// Decode failed -> treat as a communication error.
status_monitor_handle_error(monitor);
return false;
}
status_monitor_apply_status(monitor, &decoded, current_time);
memcpy(status, &decoded, sizeof(midea_status_t));
return true;
}
bool status_monitor_poll(status_monitor_t *monitor, uart_driver_t *uart,
uint32_t current_time, midea_status_t *status) {
if (!monitor || !monitor->initialized || !uart || !status) {
return false;
}
monitor->last_attempt_time = current_time;
// Build and send a status-request frame.
uint8_t request[8];
size_t request_len = status_monitor_build_request(request, sizeof(request));
if (request_len == 0 ||
!uart_driver_send(uart, request, request_len, monitor->config.timeout_ms)) {
status_monitor_handle_error(monitor);
return false;
}
// Receive the response.
uint8_t response[STATUS_RESPONSE_MAX];
int received = uart_driver_receive(uart, response, sizeof(response),
monitor->config.timeout_ms);
if (received <= 0) {
// No data / error -> communication failure.
status_monitor_handle_error(monitor);
return false;
}
return status_monitor_process_response(monitor, response, (size_t)received,
current_time, status);
}
void status_monitor_map_to_zcl(const midea_status_t *status, zcl_thermostat_attrs_t *zcl_attrs) {
if (!status || !zcl_attrs) {
return;
}
// Map MideaUART status to ZCL Thermostat attributes
zcl_attrs->local_temperature = status->indoor_temp;
// Map power state and mode to ZCL system_mode
if (status->power_state == 0x00) {
// AC is OFF
zcl_attrs->system_mode = 0x00; // Off
} else {
// AC is ON, map the mode
switch (status->mode) {
case MODE_COOL:
zcl_attrs->system_mode = 0x03; // Cooling
break;
case MODE_HEAT:
zcl_attrs->system_mode = 0x04; // Heating
break;
case MODE_AUTO:
zcl_attrs->system_mode = 0x01; // Auto
break;
case MODE_DRY:
zcl_attrs->system_mode = 0x08; // Dry
break;
case MODE_FAN:
zcl_attrs->system_mode = 0x00; // Off (treat fan only as off for HVAC)
break;
default:
zcl_attrs->system_mode = 0x00; // Default to Off
break;
}
}
// Set control sequence (simplified)
zcl_attrs->control_sequence = 0x00; // Not used in basic implementation
}
bool status_monitor_check_timeout(const status_monitor_t *monitor, uint32_t current_time) {
if (!monitor || !monitor->initialized) {
return true; // Treat uninitialized monitor as timed out
}
// Never successfully polled -> considered timed out.
if (!monitor->last_poll_success) {
return true;
}
// Watchdog window: one poll interval plus the per-attempt timeout allowance.
uint32_t window = monitor->config.poll_interval_ms + monitor->config.timeout_ms;
// Guard against clock going backwards.
if (current_time < monitor->last_poll_time) {
return false;
}
return (current_time - monitor->last_poll_time) > window;
}
void status_monitor_handle_error(status_monitor_t *monitor) {
if (!monitor) {
return;
}
monitor->last_poll_success = false;
monitor->error_count++;
if (monitor->retry_count < 0xFF) {
monitor->retry_count++;
}
// Trip the watchdog once retries are exhausted.
if (monitor->retry_count >= monitor->config.max_retries) {
monitor->comm_timeout = true;
}
}
bool status_monitor_has_fault(const status_monitor_t *monitor) {
if (!monitor) {
return false;
}
return monitor->fault_detected;
}
bool status_monitor_get_last_status(const status_monitor_t *monitor, midea_status_t *status) {
if (!monitor || !status) {
return false;
}
memcpy(status, &monitor->last_status, sizeof(midea_status_t));
return true;
}
bool status_monitor_get_last_zcl_attrs(const status_monitor_t *monitor, zcl_thermostat_attrs_t *zcl_attrs) {
if (!monitor || !zcl_attrs) {
return false;
}
memcpy(zcl_attrs, &monitor->last_zcl_attrs, sizeof(zcl_thermostat_attrs_t));
return true;
}

77
src/status_monitor.h Normal file
View File

@@ -0,0 +1,77 @@
#ifndef STATUS_MONITOR_H
#define STATUS_MONITOR_H
#include <stdint.h>
#include <stdbool.h>
#include "midea_protocol.h"
#include "zigbee_zcl.h"
#include "uart_driver.h"
// Status monitoring configuration
typedef struct {
uint32_t poll_interval_ms; // How often to poll for status (default: 5000ms)
uint32_t timeout_ms; // Communication timeout per attempt (default: 1000ms)
uint8_t max_retries; // Maximum retry attempts before comm failure (default: 3)
} status_monitor_config_t;
// Status monitoring state
typedef struct {
bool initialized;
bool last_poll_success;
uint32_t last_poll_time; // Timestamp (ms) of last SUCCESSFUL poll
uint32_t last_attempt_time; // Timestamp (ms) of last poll attempt
uint8_t retry_count; // Consecutive failed attempts
uint32_t error_count; // Total accumulated communication errors
bool comm_timeout; // Watchdog tripped (retries exhausted / no response)
bool fault_detected; // AC reported an error_code or alarm_mask
uint16_t fault_code; // Last fault value (error_code | alarm_mask bits)
status_monitor_config_t config;
midea_status_t last_status;
zcl_thermostat_attrs_t last_zcl_attrs;
} status_monitor_t;
// Initialize status monitor. If config is NULL, defaults are applied.
bool status_monitor_init(status_monitor_t *monitor, const status_monitor_config_t *config);
// Deinitialize status monitor
void status_monitor_deinit(status_monitor_t *monitor);
// Poll AC status via UART: sends a status request, receives and decodes the
// response, then updates internal state (fault detection, watchdog).
// current_time is the current monotonic time in milliseconds.
// Returns true on a successful poll, false on communication/decode failure.
bool status_monitor_poll(status_monitor_t *monitor, uart_driver_t *uart,
uint32_t current_time, midea_status_t *status);
// Process a raw UART response buffer (decode + fault detection + state update).
// Exposed separately so it can be exercised without live hardware.
// Returns true if the buffer decoded into a valid status.
bool status_monitor_process_response(status_monitor_t *monitor,
const uint8_t *buffer, size_t length,
uint32_t current_time, midea_status_t *status);
// Map MideaUART status to ZCL attributes
void status_monitor_map_to_zcl(const midea_status_t *status, zcl_thermostat_attrs_t *zcl_attrs);
// Watchdog: returns true if the time since the last successful poll exceeds the
// allowed window (poll_interval_ms + timeout_ms), or if the monitor has never
// polled successfully.
bool status_monitor_check_timeout(const status_monitor_t *monitor, uint32_t current_time);
// Handle a communication error: increments retry/error counters and trips the
// watchdog once max_retries is reached.
void status_monitor_handle_error(status_monitor_t *monitor);
// Returns true if the AC currently reports a fault.
bool status_monitor_has_fault(const status_monitor_t *monitor);
// Get last known status. Returns false on bad args.
bool status_monitor_get_last_status(const status_monitor_t *monitor, midea_status_t *status);
// Get last known ZCL attributes. Returns false on bad args.
bool status_monitor_get_last_zcl_attrs(const status_monitor_t *monitor, zcl_thermostat_attrs_t *zcl_attrs);
// Build a MideaUART status-request frame into buffer. Returns bytes written (0 on error).
size_t status_monitor_build_request(uint8_t *buffer, size_t buffer_size);
#endif // STATUS_MONITOR_H

Binary file not shown.

BIN
test/status_monitor_test Executable file

Binary file not shown.

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();
}

Binary file not shown.

Binary file not shown.

1
unity Submodule

Submodule unity added at b706271f32