feat: implement status monitoring, fault detection, and comms watchdog
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
12
Makefile
12
Makefile
@@ -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)
|
||||||
|
|||||||
21
Readme.md
21
Readme.md
@@ -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.
|
||||||
|
|||||||
@@ -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
259
src/status_monitor.c
Normal 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
77
src/status_monitor.h
Normal 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
BIN
test/status_monitor_test
Executable file
Binary file not shown.
333
test/status_monitor_test.c
Normal file
333
test/status_monitor_test.c
Normal 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
1
unity
Submodule
Submodule unity added at b706271f32
Reference in New Issue
Block a user