From 16e475f01fd3f06786a94eb6dcb93dd89260a503 Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 00:27:36 +0200
Subject: [PATCH 01/19] Add Xiaomi Compact 4 ESP32 bringup driver
---
.gitignore | 3 +
platforms/ESP-IDF/main/CMakeLists.txt | 2 +-
platforms/ESP-IDF/sdkconfig.defaults.esp32 | 4 +-
platforms/obk_main.cmake | 1 +
platforms/obk_main.mk | 1 +
src/driver/drv_local.h | 7 +
src/driver/drv_main.c | 17 +
src/driver/drv_xiaomi_compact4.c | 775 +++++++++++++++++++++
src/driver/drv_xiaomi_compact4.h | 10 +
src/hal/espidf/hal_uart_espidf.c | 7 +-
src/hal/espidf/hal_wifi_espidf.c | 42 +-
src/httpserver/http_fns.c | 4 +-
src/httpserver/rest_interface.c | 5 +-
src/obk_config.h | 3 +
14 files changed, 863 insertions(+), 18 deletions(-)
create mode 100644 src/driver/drv_xiaomi_compact4.c
create mode 100644 src/driver/drv_xiaomi_compact4.h
diff --git a/.gitignore b/.gitignore
index d22fec2403..75c5cb3db2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,6 @@ enc_temp_folder/**
*.i
*.ii
*.s
+
+# Local planning/reference notes
+/Xiaomi/
diff --git a/platforms/ESP-IDF/main/CMakeLists.txt b/platforms/ESP-IDF/main/CMakeLists.txt
index cf61f19d71..e06ac8bda7 100644
--- a/platforms/ESP-IDF/main/CMakeLists.txt
+++ b/platforms/ESP-IDF/main/CMakeLists.txt
@@ -36,7 +36,7 @@ if(ENABLE_MDNS_ESPIDF)
)
endif()
idf_component_register(SRCS ${PROJ_ALL_SRC} WHOLE_ARCHIVE
- PRIV_REQUIRES mqtt lwip esp_wifi nvs_flash esp_driver_tsens esp_driver_gpio esp_pm esp_partition app_update esp_adc esp_driver_uart esp_driver_ledc spi_flash esp_driver_spi esp_driver_gptimer)
+ PRIV_REQUIRES mqtt lwip esp_wifi nvs_flash esp_driver_tsens esp_driver_gpio esp_pm esp_partition app_update esp_adc esp_driver_uart esp_driver_i2c esp_driver_ledc spi_flash esp_driver_spi esp_driver_gptimer)
target_compile_definitions(${COMPONENT_LIB} PRIVATE USER_SW_VER="$ENV{APP_VERSION}")
target_compile_definitions(${COMPONENT_LIB} PRIVATE OBK_VARIANT=$ENV{OBK_VARIANT})
diff --git a/platforms/ESP-IDF/sdkconfig.defaults.esp32 b/platforms/ESP-IDF/sdkconfig.defaults.esp32
index 5112865ca8..668cd46d3e 100644
--- a/platforms/ESP-IDF/sdkconfig.defaults.esp32
+++ b/platforms/ESP-IDF/sdkconfig.defaults.esp32
@@ -1,5 +1,7 @@
CONFIG_PM_ENABLE=y
CONFIG_PM_DFS_INIT_AUTO=y
+CONFIG_FREERTOS_UNICORE=y
+CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_ESPTOOLPY_FLASHFREQ_40M=y
-CONFIG_ESPTOOLPY_FLASHFREQ="40m"
\ No newline at end of file
+CONFIG_ESPTOOLPY_FLASHFREQ="40m"
diff --git a/platforms/obk_main.cmake b/platforms/obk_main.cmake
index 312cc51ee1..25f36d417e 100644
--- a/platforms/obk_main.cmake
+++ b/platforms/obk_main.cmake
@@ -159,6 +159,7 @@ set(OBKM_SRC
${OBK_SRCS}driver/drv_ucs1912.c
${OBK_SRCS}driver/drv_wemo.c
${OBK_SRCS}driver/drv_widget.c
+ ${OBK_SRCS}driver/drv_xiaomi_compact4.c
${OBK_SRCS}i2c/drv_i2c_ads1115.c
${OBK_SRCS}i2c/drv_i2c_lcd_pcf8574t.c
${OBK_SRCS}i2c/drv_i2c_main.c
diff --git a/platforms/obk_main.mk b/platforms/obk_main.mk
index 2fce567e34..9d68af470f 100644
--- a/platforms/obk_main.mk
+++ b/platforms/obk_main.mk
@@ -178,6 +178,7 @@ OBKM_SRC += $(OBK_SRCS)driver/drv_uart_tcp.c
OBKM_SRC += $(OBK_SRCS)driver/drv_ucs1912.c
OBKM_SRC += $(OBK_SRCS)driver/drv_wemo.c
OBKM_SRC += $(OBK_SRCS)driver/drv_widget.c
+OBKM_SRC += $(OBK_SRCS)driver/drv_xiaomi_compact4.c
OBKM_SRC += $(OBK_SRCS)i2c/drv_i2c_ads1115.c
OBKM_SRC += $(OBK_SRCS)i2c/drv_i2c_lcd_pcf8574t.c
OBKM_SRC += $(OBK_SRCS)i2c/drv_i2c_main.c
diff --git a/src/driver/drv_local.h b/src/driver/drv_local.h
index f802de16c9..5538e07618 100644
--- a/src/driver/drv_local.h
+++ b/src/driver/drv_local.h
@@ -251,6 +251,13 @@ void Drawers_QuickTick();
void HGS02_Init(void);
void HGS02_RunEverySecond(void);
+void XiaomiCompact4_Init(void);
+void XiaomiCompact4_RunEverySecond(void);
+void XiaomiCompact4_RunQuickTick(void);
+void XiaomiCompact4_Stop(void);
+void XiaomiCompact4_OnChannelChanged(int ch, int value);
+void XiaomiCompact4_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState);
+
void UART_TCP_Init(void);
void UART_TCP_Deinit(void);
diff --git a/src/driver/drv_main.c b/src/driver/drv_main.c
index a8f9d51eaa..6e5191018b 100644
--- a/src/driver/drv_main.c
+++ b/src/driver/drv_main.c
@@ -22,6 +22,7 @@
#include "drv_ds1820_common.h"
#include "drv_ds3231.h"
#include "drv_hlw8112.h"
+#include "drv_xiaomi_compact4.h"
#include "drv_DCF77.h"
void DRV_MQTTServer_Init();
@@ -175,6 +176,22 @@ static driver_t g_drivers[] = {
false, // loaded
},
#endif
+#if ENABLE_DRIVER_XIAOMI_COMPACT4
+ //drvdetail:{"name":"XiaomiCompact4",
+ //drvdetail:"title":"Xiaomi Smart Air Purifier 4 Compact",
+ //drvdetail:"descr":"Device-specific driver for Xiaomi Smart Air Purifier 4 Compact (zhimi.airp.cpa4) on ESP32-WROOM-32D hardware.",
+ //drvdetail:"requires":"ESP32"}
+ { "XiaomiCompact4", // Driver Name
+ XiaomiCompact4_Init, // Init
+ XiaomiCompact4_RunEverySecond, // onEverySecond
+ XiaomiCompact4_AppendInformationToHTTPIndexPage, // appendInformationToHTTPIndexPage
+ XiaomiCompact4_RunQuickTick, // runQuickTick
+ XiaomiCompact4_Stop, // stopFunction
+ XiaomiCompact4_OnChannelChanged, // onChannelChanged
+ NULL, // onHassDiscovery
+ false, // loaded
+ },
+#endif
#if ENABLE_DRIVER_TESTSPIFLASH
//drvdetail:{"name":"TESTSPIFLASH",
//drvdetail:"title":"TODO",
diff --git a/src/driver/drv_xiaomi_compact4.c b/src/driver/drv_xiaomi_compact4.c
new file mode 100644
index 0000000000..e2712c3caf
--- /dev/null
+++ b/src/driver/drv_xiaomi_compact4.c
@@ -0,0 +1,775 @@
+#include "../obk_config.h"
+
+#if ENABLE_DRIVER_XIAOMI_COMPACT4
+
+#include "../new_common.h"
+#include "../new_cfg.h"
+#include "../new_pins.h"
+#include "../cmnds/cmd_public.h"
+#include "../logging/logging.h"
+#include "../mqtt/new_mqtt.h"
+#include "../hal/hal_flashVars.h"
+#include "../hal/hal_pins.h"
+#include "../hal/hal_wifi.h"
+#include "../httpserver/new_http.h"
+#include "drv_local.h"
+
+#include "driver/i2c.h"
+#include "driver/ledc.h"
+#include "driver/uart.h"
+
+#define XIAOMI_C4_PIN_PM25_TX 17
+#define XIAOMI_C4_PIN_PM25_RX 16
+#define XIAOMI_C4_UART_PORT UART_NUM_1
+#define XIAOMI_C4_UART_BAUD 9600
+#define XIAOMI_C4_UART_BUF_SIZE 256
+
+#define XIAOMI_C4_PIN_I2C_SDA 14
+#define XIAOMI_C4_PIN_I2C_SCL 27
+#define XIAOMI_C4_I2C_PORT I2C_NUM_0
+#define XIAOMI_C4_I2C_FREQ_HZ 100000
+#define XIAOMI_C4_I2C_BRIGHTNESS 0x24
+#define XIAOMI_C4_I2C_STATUS_LEDS 0x34
+#define XIAOMI_C4_I2C_BUTTON_POWER 0x35
+#define XIAOMI_C4_I2C_BUTTON_BRIGHTNESS 0x36
+#define XIAOMI_C4_I2C_BUTTON_MODE 0x37
+
+#define XIAOMI_C4_PIN_MOTOR_EN 2
+#define XIAOMI_C4_PIN_MOTOR_PWM 26
+#define XIAOMI_C4_MOTOR_LEDC_TIMER LEDC_TIMER_1
+#define XIAOMI_C4_MOTOR_LEDC_CHANNEL LEDC_CHANNEL_5
+#define XIAOMI_C4_MOTOR_LEDC_RES LEDC_TIMER_13_BIT
+#define XIAOMI_C4_MOTOR_DUTY_50_PERCENT 4096
+#define XIAOMI_C4_PIN_TACH 34
+#define XIAOMI_C4_PIN_BUTTON_MODE 5
+#define XIAOMI_C4_PIN_BUTTON_LIGHT 18
+#define XIAOMI_C4_PIN_BUTTON_POWER 19
+#define XIAOMI_C4_PIN_BUZZER 4
+#define XIAOMI_C4_PIN_LED_RED 21
+#define XIAOMI_C4_PIN_LED_ORANGE 32
+#define XIAOMI_C4_PIN_LED_GREEN 33
+
+#define XIAOMI_C4_CH_POWER 0
+#define XIAOMI_C4_CH_MODE 1
+#define XIAOMI_C4_CH_BRIGHTNESS 2
+#define XIAOMI_C4_CH_CHILD_LOCK 3
+#define XIAOMI_C4_CH_PM25 4
+#define XIAOMI_C4_CH_MOTOR_RPM 5
+#define XIAOMI_C4_CH_FILTER_USAGE 6
+#define XIAOMI_C4_CH_FILTER_HEALTH 7
+#define XIAOMI_C4_CH_REPLACE_FILTER 8
+#define XIAOMI_C4_CH_FAV_SPEED 9
+#define XIAOMI_C4_CH_NIGHT_SPEED 10
+#define XIAOMI_C4_CH_P_FACTOR 11
+#define XIAOMI_C4_CH_FILTER_LIFESPAN 12
+
+#define XIAOMI_C4_FLASH_BASE 48
+#define XIAOMI_C4_VAR_POWER (XIAOMI_C4_FLASH_BASE + 0)
+#define XIAOMI_C4_VAR_MODE (XIAOMI_C4_FLASH_BASE + 1)
+#define XIAOMI_C4_VAR_BRIGHTNESS (XIAOMI_C4_FLASH_BASE + 2)
+#define XIAOMI_C4_VAR_CHILD_LOCK (XIAOMI_C4_FLASH_BASE + 3)
+#define XIAOMI_C4_VAR_FAV_SPEED (XIAOMI_C4_FLASH_BASE + 4)
+#define XIAOMI_C4_VAR_NIGHT_SPEED (XIAOMI_C4_FLASH_BASE + 5)
+#define XIAOMI_C4_VAR_P_FACTOR_X100 (XIAOMI_C4_FLASH_BASE + 6)
+#define XIAOMI_C4_VAR_FILTER_LIFESPAN (XIAOMI_C4_FLASH_BASE + 7)
+#define XIAOMI_C4_VAR_FILTER_USAGE (XIAOMI_C4_FLASH_BASE + 8)
+
+#define XIAOMI_C4_MODE_FAV 0
+#define XIAOMI_C4_MODE_NIGHT 1
+#define XIAOMI_C4_MODE_AUTO 2
+#define XIAOMI_C4_BRIGHTNESS_FULL 0
+#define XIAOMI_C4_BRIGHTNESS_MID 1
+#define XIAOMI_C4_BRIGHTNESS_ZERO 2
+
+#define XIAOMI_C4_BUTTON_LED_ON 0x3C
+#define XIAOMI_C4_BUTTON_LED_OFF 0x00
+#define XIAOMI_C4_STATUS_REPLACE_FILTER 0x01
+#define XIAOMI_C4_STATUS_WIFI 0x02
+#define XIAOMI_C4_STATUS_CHILD_LOCK 0x04
+#define XIAOMI_C4_STATUS_MODE_AUTO 0x08
+#define XIAOMI_C4_STATUS_MODE_NIGHT 0x10
+#define XIAOMI_C4_STATUS_MODE_FAV 0x20
+
+#define XIAOMI_C4_PM25_FRAME_LEN 20
+#define XIAOMI_C4_PM25_MAX_VALID 2000
+#define XIAOMI_C4_PM25_POLL_SECONDS 1
+#define XIAOMI_C4_LONG_PRESS_MS 7000
+
+static int g_power;
+static int g_mode;
+static int g_brightness;
+static int g_childLock;
+static int g_favSpeed;
+static int g_nightSpeed;
+static int g_pFactorX100;
+static int g_filterLifespanDays;
+static uint32_t g_filterUsageSeconds;
+static int g_lastPm25 = -1;
+static int g_lastMotorRpm;
+static int g_lastButtonPower;
+static int g_lastButtonLight;
+static int g_lastButtonMode;
+static int g_powerPressTicks;
+static int g_lightPressTicks;
+static int g_modePressTicks;
+static int g_modeLongHandled;
+static uint32_t g_tachPulses;
+static int g_pm25PollCountdown;
+static int g_filterCountdown;
+static int g_childLockNotificationTicks;
+static int g_childLockNotificationFlips;
+static int g_childLockNotificationState;
+static int g_ignoreChannelChange;
+static int g_initialized;
+static uint8_t g_pm25RxBuf[XIAOMI_C4_UART_BUF_SIZE];
+static int g_pm25RxLen;
+
+static int XiaomiCompact4_ClampInt(int value, int min, int max) {
+ if (value < min) return min;
+ if (value > max) return max;
+ return value;
+}
+
+static float XiaomiCompact4_ClampFloat(float value, float min, float max) {
+ if (value < min) return min;
+ if (value > max) return max;
+ return value;
+}
+
+static int XiaomiCompact4_LoadInt(int index, int def, int min, int max) {
+ int value = HAL_FlashVars_GetChannelValue(index);
+ if (value < min || value > max) {
+ value = def;
+ }
+ return value;
+}
+
+static void XiaomiCompact4_SaveInt(int index, int value) {
+ HAL_FlashVars_SaveChannel(index, value);
+}
+
+static const char *XiaomiCompact4_ModeToStr(int mode) {
+ switch (mode) {
+ case XIAOMI_C4_MODE_FAV:
+ return "FAV";
+ case XIAOMI_C4_MODE_NIGHT:
+ return "NIGHT";
+ default:
+ return "AUTO";
+ }
+}
+
+static const char *XiaomiCompact4_BrightnessToStr(int brightness) {
+ switch (brightness) {
+ case XIAOMI_C4_BRIGHTNESS_MID:
+ return "MID";
+ case XIAOMI_C4_BRIGHTNESS_ZERO:
+ return "ZERO";
+ default:
+ return "FULL";
+ }
+}
+
+static int XiaomiCompact4_ParseMode(const char *s) {
+ if (!stricmp(s, "FAV")) return XIAOMI_C4_MODE_FAV;
+ if (!stricmp(s, "NIGHT")) return XIAOMI_C4_MODE_NIGHT;
+ return XIAOMI_C4_MODE_AUTO;
+}
+
+static int XiaomiCompact4_ParseBrightness(const char *s) {
+ if (!stricmp(s, "MID")) return XIAOMI_C4_BRIGHTNESS_MID;
+ if (!stricmp(s, "ZERO")) return XIAOMI_C4_BRIGHTNESS_ZERO;
+ return XIAOMI_C4_BRIGHTNESS_FULL;
+}
+
+static void XiaomiCompact4_I2CInit(void) {
+ i2c_config_t conf = {
+ .mode = I2C_MODE_MASTER,
+ .sda_io_num = XIAOMI_C4_PIN_I2C_SDA,
+ .scl_io_num = XIAOMI_C4_PIN_I2C_SCL,
+ .sda_pullup_en = GPIO_PULLUP_ENABLE,
+ .scl_pullup_en = GPIO_PULLUP_ENABLE,
+ .master.clk_speed = XIAOMI_C4_I2C_FREQ_HZ,
+ };
+ i2c_param_config(XIAOMI_C4_I2C_PORT, &conf);
+ if (i2c_driver_install(XIAOMI_C4_I2C_PORT, I2C_MODE_MASTER, 0, 0, 0) != ESP_OK) {
+ ADDLOG_WARN(LOG_FEATURE_DRV, "XiaomiCompact4 I2C driver install failed");
+ }
+}
+
+static void XiaomiCompact4_I2CWrite1(int addr7, int value) {
+ uint8_t data = (uint8_t)value;
+ esp_err_t err = i2c_master_write_to_device(XIAOMI_C4_I2C_PORT, addr7, &data, 1, 100 / portTICK_PERIOD_MS);
+ if (err != ESP_OK) {
+ ADDLOG_DEBUG(LOG_FEATURE_DRV, "XiaomiCompact4 I2C write failed addr 0x%02X err %i", addr7, err);
+ }
+}
+
+static void XiaomiCompact4_UARTInit(void) {
+ uart_config_t uart_config = {
+ .baud_rate = XIAOMI_C4_UART_BAUD,
+ .data_bits = UART_DATA_8_BITS,
+ .parity = UART_PARITY_DISABLE,
+ .stop_bits = UART_STOP_BITS_1,
+ .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
+ .source_clk = UART_SCLK_DEFAULT,
+ };
+ if (uart_is_driver_installed(XIAOMI_C4_UART_PORT)) {
+ uart_driver_delete(XIAOMI_C4_UART_PORT);
+ }
+ uart_param_config(XIAOMI_C4_UART_PORT, &uart_config);
+ uart_set_pin(XIAOMI_C4_UART_PORT, XIAOMI_C4_PIN_PM25_TX, XIAOMI_C4_PIN_PM25_RX, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
+ uart_driver_install(XIAOMI_C4_UART_PORT, XIAOMI_C4_UART_BUF_SIZE, 0, 0, NULL, 0);
+ g_pm25RxLen = 0;
+}
+
+static void XiaomiCompact4_UARTReadAvailable(void) {
+ uint8_t tmp[64];
+ int len = uart_read_bytes(XIAOMI_C4_UART_PORT, tmp, sizeof(tmp), 0);
+ if (len <= 0) {
+ return;
+ }
+ if (g_pm25RxLen + len > (int)sizeof(g_pm25RxBuf)) {
+ g_pm25RxLen = 0;
+ }
+ memcpy(g_pm25RxBuf + g_pm25RxLen, tmp, len);
+ g_pm25RxLen += len;
+}
+
+static uint8_t XiaomiCompact4_UARTPeek(int index) {
+ if (index < 0 || index >= g_pm25RxLen) {
+ return 0;
+ }
+ return g_pm25RxBuf[index];
+}
+
+static void XiaomiCompact4_UARTConsume(int count) {
+ if (count <= 0) {
+ return;
+ }
+ if (count >= g_pm25RxLen) {
+ g_pm25RxLen = 0;
+ return;
+ }
+ memmove(g_pm25RxBuf, g_pm25RxBuf + count, g_pm25RxLen - count);
+ g_pm25RxLen -= count;
+}
+
+static void XiaomiCompact4_MotorPWMInit(void) {
+ ledc_timer_config_t timer = {
+ .speed_mode = LEDC_LOW_SPEED_MODE,
+ .timer_num = XIAOMI_C4_MOTOR_LEDC_TIMER,
+ .duty_resolution = XIAOMI_C4_MOTOR_LEDC_RES,
+ .freq_hz = 1000,
+ .clk_cfg = SOC_MOD_CLK_RC_FAST,
+ };
+ ledc_channel_config_t channel = {
+ .gpio_num = XIAOMI_C4_PIN_MOTOR_PWM,
+ .speed_mode = LEDC_LOW_SPEED_MODE,
+ .channel = XIAOMI_C4_MOTOR_LEDC_CHANNEL,
+ .intr_type = LEDC_INTR_DISABLE,
+ .timer_sel = XIAOMI_C4_MOTOR_LEDC_TIMER,
+ .duty = 0,
+ .hpoint = 0,
+ };
+ ledc_timer_config(&timer);
+ ledc_channel_config(&channel);
+}
+
+static void XiaomiCompact4_MotorPWMSet(int frequency) {
+ if (frequency <= 0) {
+ ledc_stop(LEDC_LOW_SPEED_MODE, XIAOMI_C4_MOTOR_LEDC_CHANNEL, 0);
+ return;
+ }
+ ledc_set_freq(LEDC_LOW_SPEED_MODE, XIAOMI_C4_MOTOR_LEDC_TIMER, frequency);
+ ledc_set_duty(LEDC_LOW_SPEED_MODE, XIAOMI_C4_MOTOR_LEDC_CHANNEL, XIAOMI_C4_MOTOR_DUTY_50_PERCENT);
+ ledc_update_duty(LEDC_LOW_SPEED_MODE, XIAOMI_C4_MOTOR_LEDC_CHANNEL);
+}
+
+static void XiaomiCompact4_SetLedPWM(int pin, float level) {
+ if (level <= 0.0f) {
+ HAL_PIN_PWM_Update(pin, 0);
+ return;
+ }
+ HAL_PIN_PWM_Update(pin, XiaomiCompact4_ClampFloat(level * 100.0f, 0.0f, 100.0f));
+}
+
+static int XiaomiCompact4_IsWiFiConnected(void) {
+ const char *ip = HAL_GetMyIPString();
+ return ip && strcmp(ip, "0.0.0.0");
+}
+
+static int XiaomiCompact4_ReplaceFilter(void) {
+ uint32_t limit = (uint32_t)g_filterLifespanDays * 24U * 3600U;
+ return g_filterUsageSeconds > limit;
+}
+
+static void XiaomiCompact4_SetChannels(void) {
+ g_ignoreChannelChange = 1;
+ CHANNEL_Set(XIAOMI_C4_CH_POWER, g_power, CHANNEL_SET_FLAG_SILENT);
+ CHANNEL_Set(XIAOMI_C4_CH_MODE, g_mode, CHANNEL_SET_FLAG_SILENT);
+ CHANNEL_Set(XIAOMI_C4_CH_BRIGHTNESS, g_brightness, CHANNEL_SET_FLAG_SILENT);
+ CHANNEL_Set(XIAOMI_C4_CH_CHILD_LOCK, g_childLock, CHANNEL_SET_FLAG_SILENT);
+ if (g_lastPm25 >= 0) {
+ CHANNEL_Set(XIAOMI_C4_CH_PM25, g_lastPm25, CHANNEL_SET_FLAG_SILENT);
+ }
+ CHANNEL_Set(XIAOMI_C4_CH_MOTOR_RPM, g_lastMotorRpm, CHANNEL_SET_FLAG_SILENT);
+ CHANNEL_Set(XIAOMI_C4_CH_FILTER_USAGE, (int)g_filterUsageSeconds, CHANNEL_SET_FLAG_SILENT);
+ CHANNEL_Set(XIAOMI_C4_CH_FILTER_HEALTH, XiaomiCompact4_ClampInt(100 - (int)((100U * g_filterUsageSeconds) / ((uint32_t)g_filterLifespanDays * 24U * 3600U)), 0, 100), CHANNEL_SET_FLAG_SILENT);
+ CHANNEL_Set(XIAOMI_C4_CH_REPLACE_FILTER, XiaomiCompact4_ReplaceFilter(), CHANNEL_SET_FLAG_SILENT);
+ CHANNEL_Set(XIAOMI_C4_CH_FAV_SPEED, g_favSpeed, CHANNEL_SET_FLAG_SILENT);
+ CHANNEL_Set(XIAOMI_C4_CH_NIGHT_SPEED, g_nightSpeed, CHANNEL_SET_FLAG_SILENT);
+ CHANNEL_Set(XIAOMI_C4_CH_P_FACTOR, g_pFactorX100, CHANNEL_SET_FLAG_SILENT);
+ CHANNEL_Set(XIAOMI_C4_CH_FILTER_LIFESPAN, g_filterLifespanDays, CHANNEL_SET_FLAG_SILENT);
+ g_ignoreChannelChange = 0;
+}
+
+static void XiaomiCompact4_SaveState(void) {
+ XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_POWER, g_power);
+ XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_MODE, g_mode);
+ XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_BRIGHTNESS, g_brightness);
+ XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_CHILD_LOCK, g_childLock);
+ XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_FAV_SPEED, g_favSpeed);
+ XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_NIGHT_SPEED, g_nightSpeed);
+ XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_P_FACTOR_X100, g_pFactorX100);
+ XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_FILTER_LIFESPAN, g_filterLifespanDays);
+ XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_FILTER_USAGE, (int)g_filterUsageSeconds);
+}
+
+static void XiaomiCompact4_StartChildLockNotification(void) {
+ g_childLockNotificationTicks = 1;
+ g_childLockNotificationFlips = 8;
+ g_childLockNotificationState = 0;
+ HAL_PIN_PWM_Update(XIAOMI_C4_PIN_BUZZER, 50);
+}
+
+static void XiaomiCompact4_UpdateHID(void) {
+ int isFullBacklight = g_brightness == XIAOMI_C4_BRIGHTNESS_FULL;
+ int isNoBacklight = g_brightness == XIAOMI_C4_BRIGHTNESS_ZERO;
+ int disableHid = isNoBacklight || !g_power;
+ int expectedBacklight = (isFullBacklight && g_power) ? 8 : 3;
+ float brightnessCoefficient = isFullBacklight ? 1.0f : 0.6f;
+ int status = 0;
+
+ XiaomiCompact4_I2CWrite1(XIAOMI_C4_I2C_BRIGHTNESS, ((expectedBacklight == 8 ? 0 : expectedBacklight) << 4) | 0x01);
+ XiaomiCompact4_I2CWrite1(XIAOMI_C4_I2C_BUTTON_MODE, disableHid ? XIAOMI_C4_BUTTON_LED_OFF : XIAOMI_C4_BUTTON_LED_ON);
+ XiaomiCompact4_I2CWrite1(XIAOMI_C4_I2C_BUTTON_BRIGHTNESS, disableHid ? XIAOMI_C4_BUTTON_LED_OFF : XIAOMI_C4_BUTTON_LED_ON);
+ XiaomiCompact4_I2CWrite1(XIAOMI_C4_I2C_BUTTON_POWER, XIAOMI_C4_BUTTON_LED_ON);
+
+ if (!disableHid) {
+ if (g_mode == XIAOMI_C4_MODE_FAV) status |= XIAOMI_C4_STATUS_MODE_FAV;
+ else if (g_mode == XIAOMI_C4_MODE_NIGHT) status |= XIAOMI_C4_STATUS_MODE_NIGHT;
+ else status |= XIAOMI_C4_STATUS_MODE_AUTO;
+ if (XiaomiCompact4_IsWiFiConnected()) status |= XIAOMI_C4_STATUS_WIFI;
+ if (XiaomiCompact4_ReplaceFilter()) status |= XIAOMI_C4_STATUS_REPLACE_FILTER;
+ }
+ if (g_childLock) {
+ status |= XIAOMI_C4_STATUS_CHILD_LOCK;
+ }
+ if (g_childLockNotificationFlips > 0 && g_childLockNotificationState) {
+ status ^= XIAOMI_C4_STATUS_CHILD_LOCK;
+ }
+ XiaomiCompact4_I2CWrite1(XIAOMI_C4_I2C_STATUS_LEDS, status);
+
+ if (disableHid || g_lastPm25 < 0) {
+ XiaomiCompact4_SetLedPWM(XIAOMI_C4_PIN_LED_GREEN, 0);
+ XiaomiCompact4_SetLedPWM(XIAOMI_C4_PIN_LED_ORANGE, 0);
+ XiaomiCompact4_SetLedPWM(XIAOMI_C4_PIN_LED_RED, 0);
+ } else {
+ float green = 0.0f, orange = 0.0f, red = 0.0f;
+ int pm = g_lastPm25;
+ if (pm < 20) {
+ green = 1.0f;
+ } else if (pm < 40) {
+ green = 0.6f;
+ orange = 1.0f;
+ } else if (pm < 60) {
+ green = 0.35f;
+ orange = 1.0f;
+ } else if (pm < 100) {
+ orange = 1.0f;
+ red = 0.35f;
+ } else {
+ orange = 1.0f;
+ red = 1.0f;
+ }
+ XiaomiCompact4_SetLedPWM(XIAOMI_C4_PIN_LED_GREEN, green * brightnessCoefficient);
+ XiaomiCompact4_SetLedPWM(XIAOMI_C4_PIN_LED_ORANGE, orange * brightnessCoefficient);
+ XiaomiCompact4_SetLedPWM(XIAOMI_C4_PIN_LED_RED, red * brightnessCoefficient);
+ }
+}
+
+static void XiaomiCompact4_UpdateMotor(void) {
+ if (!g_power) {
+ XiaomiCompact4_MotorPWMSet(0);
+ return;
+ }
+ float motorSetpoint = 0.0f;
+ if (g_mode == XIAOMI_C4_MODE_AUTO) {
+ if (g_lastPm25 >= 0) {
+ motorSetpoint = XiaomiCompact4_ClampFloat((float)g_lastPm25 * ((float)g_pFactorX100 / 100.0f), 0.0f, 100.0f);
+ } else {
+ motorSetpoint = g_favSpeed;
+ }
+ } else if (g_mode == XIAOMI_C4_MODE_FAV) {
+ motorSetpoint = g_favSpeed;
+ } else {
+ motorSetpoint = g_nightSpeed;
+ }
+ XiaomiCompact4_MotorPWMSet((int)(motorSetpoint * 4.12f + 100.0f));
+}
+
+static void XiaomiCompact4_ApplyState(int save) {
+ HAL_PIN_SetOutputValue(XIAOMI_C4_PIN_MOTOR_EN, g_power ? 1 : 0);
+ XiaomiCompact4_UpdateHID();
+ XiaomiCompact4_UpdateMotor();
+ XiaomiCompact4_SetChannels();
+ if (save) {
+ XiaomiCompact4_SaveState();
+ }
+}
+
+static void XiaomiCompact4_SetPower(int value) {
+ g_power = value ? 1 : 0;
+ XiaomiCompact4_ApplyState(1);
+}
+
+static void XiaomiCompact4_SetMode(int value) {
+ g_mode = XiaomiCompact4_ClampInt(value, XIAOMI_C4_MODE_FAV, XIAOMI_C4_MODE_AUTO);
+ if (g_brightness == XIAOMI_C4_BRIGHTNESS_ZERO) {
+ g_brightness = XIAOMI_C4_BRIGHTNESS_MID;
+ }
+ XiaomiCompact4_ApplyState(1);
+}
+
+static void XiaomiCompact4_SetBrightness(int value) {
+ g_brightness = XiaomiCompact4_ClampInt(value, XIAOMI_C4_BRIGHTNESS_FULL, XIAOMI_C4_BRIGHTNESS_ZERO);
+ XiaomiCompact4_ApplyState(1);
+}
+
+static void XiaomiCompact4_SetChildLock(int value) {
+ g_childLock = value ? 1 : 0;
+ XiaomiCompact4_StartChildLockNotification();
+ XiaomiCompact4_ApplyState(1);
+}
+
+static void XiaomiCompact4_ProcessClick(int pin) {
+ if (pin == XIAOMI_C4_PIN_BUTTON_POWER) {
+ if (g_childLock) {
+ XiaomiCompact4_StartChildLockNotification();
+ } else {
+ XiaomiCompact4_SetPower(!g_power);
+ }
+ } else if (pin == XIAOMI_C4_PIN_BUTTON_LIGHT) {
+ if (!g_power) return;
+ if (g_childLock) {
+ XiaomiCompact4_StartChildLockNotification();
+ } else {
+ XiaomiCompact4_SetBrightness((g_brightness + 1) % 3);
+ }
+ } else if (pin == XIAOMI_C4_PIN_BUTTON_MODE) {
+ if (!g_power) return;
+ if (g_childLock) {
+ XiaomiCompact4_StartChildLockNotification();
+ } else {
+ XiaomiCompact4_SetMode((g_mode + 1) % 3);
+ }
+ }
+}
+
+static void XiaomiCompact4_ServiceButton(int pin, int *last, int *pressTicks, int *longHandled) {
+ int pressed = HAL_PIN_ReadDigitalInput(pin) == 0;
+ if (pressed && !*last) {
+ *pressTicks = 0;
+ if (longHandled) *longHandled = 0;
+ }
+ if (pressed) {
+ (*pressTicks)++;
+ if (longHandled && !*longHandled && *pressTicks >= (XIAOMI_C4_LONG_PRESS_MS / 100)) {
+ *longHandled = 1;
+ XiaomiCompact4_SetChildLock(!g_childLock);
+ }
+ } else if (*last) {
+ if (!longHandled || !*longHandled) {
+ if (*pressTicks <= 10) {
+ XiaomiCompact4_ProcessClick(pin);
+ }
+ }
+ *pressTicks = 0;
+ }
+ *last = pressed;
+}
+
+static void XiaomiCompact4_TachISR(int gpio) {
+ g_tachPulses++;
+}
+
+static void XiaomiCompact4_SendPM25Query(void) {
+ static const byte query[] = { 0x11, 0x02, 0x0B, 0x01, 0xE1 };
+ uart_write_bytes(XIAOMI_C4_UART_PORT, query, sizeof(query));
+}
+
+static void XiaomiCompact4_ProcessPM25UART(void) {
+ XiaomiCompact4_UARTReadAvailable();
+ while (g_pm25RxLen >= 3) {
+ if (XiaomiCompact4_UARTPeek(0) != 0x16 || XiaomiCompact4_UARTPeek(1) != 0x11 || XiaomiCompact4_UARTPeek(2) != 0x0B) {
+ XiaomiCompact4_UARTConsume(1);
+ continue;
+ }
+ if (g_pm25RxLen < XIAOMI_C4_PM25_FRAME_LEN) {
+ break;
+ }
+ int pm25 = (((int)XiaomiCompact4_UARTPeek(15)) << 8) | XiaomiCompact4_UARTPeek(16);
+ XiaomiCompact4_UARTConsume(XIAOMI_C4_PM25_FRAME_LEN);
+ if (pm25 <= XIAOMI_C4_PM25_MAX_VALID) {
+ g_lastPm25 = pm25;
+ XiaomiCompact4_UpdateHID();
+ XiaomiCompact4_UpdateMotor();
+ XiaomiCompact4_SetChannels();
+ }
+ }
+}
+
+static void XiaomiCompact4_ServiceNotification(void) {
+ if (g_childLockNotificationFlips <= 0) {
+ HAL_PIN_PWM_Update(XIAOMI_C4_PIN_BUZZER, 0);
+ return;
+ }
+ g_childLockNotificationTicks--;
+ if (g_childLockNotificationTicks <= 0) {
+ g_childLockNotificationTicks = 3;
+ g_childLockNotificationState = !g_childLockNotificationState;
+ g_childLockNotificationFlips--;
+ if (g_childLockNotificationFlips == 0) {
+ HAL_PIN_PWM_Update(XIAOMI_C4_PIN_BUZZER, 0);
+ }
+ XiaomiCompact4_UpdateHID();
+ }
+}
+
+void XiaomiCompact4_RunQuickTick(void) {
+ if (!g_initialized) return;
+ XiaomiCompact4_ProcessPM25UART();
+ XiaomiCompact4_ServiceButton(XIAOMI_C4_PIN_BUTTON_POWER, &g_lastButtonPower, &g_powerPressTicks, NULL);
+ XiaomiCompact4_ServiceButton(XIAOMI_C4_PIN_BUTTON_LIGHT, &g_lastButtonLight, &g_lightPressTicks, NULL);
+ XiaomiCompact4_ServiceButton(XIAOMI_C4_PIN_BUTTON_MODE, &g_lastButtonMode, &g_modePressTicks, &g_modeLongHandled);
+ XiaomiCompact4_ServiceNotification();
+}
+
+void XiaomiCompact4_RunEverySecond(void) {
+ if (!g_initialized) return;
+ g_pm25PollCountdown--;
+ if (g_pm25PollCountdown <= 0) {
+ g_pm25PollCountdown = XIAOMI_C4_PM25_POLL_SECONDS;
+ XiaomiCompact4_SendPM25Query();
+ }
+ g_filterCountdown--;
+ if (g_filterCountdown <= 0) {
+ g_filterCountdown = 60;
+ if (g_power) {
+ g_filterUsageSeconds += 60;
+ XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_FILTER_USAGE, (int)g_filterUsageSeconds);
+ }
+ }
+ g_lastMotorRpm = (int)((g_tachPulses * 60U) / 15U);
+ g_tachPulses = 0;
+ XiaomiCompact4_UpdateHID();
+ XiaomiCompact4_SetChannels();
+}
+
+void XiaomiCompact4_Stop(void) {
+ XiaomiCompact4_MotorPWMSet(0);
+ HAL_PIN_SetOutputValue(XIAOMI_C4_PIN_MOTOR_EN, 0);
+ HAL_DetachInterrupt(XIAOMI_C4_PIN_TACH);
+ g_initialized = 0;
+}
+
+void XiaomiCompact4_OnChannelChanged(int ch, int value) {
+ if (g_ignoreChannelChange || !g_initialized) return;
+ switch (ch) {
+ case XIAOMI_C4_CH_POWER:
+ XiaomiCompact4_SetPower(value);
+ break;
+ case XIAOMI_C4_CH_MODE:
+ XiaomiCompact4_SetMode(value);
+ break;
+ case XIAOMI_C4_CH_BRIGHTNESS:
+ XiaomiCompact4_SetBrightness(value);
+ break;
+ case XIAOMI_C4_CH_CHILD_LOCK:
+ XiaomiCompact4_SetChildLock(value);
+ break;
+ case XIAOMI_C4_CH_FAV_SPEED:
+ g_favSpeed = XiaomiCompact4_ClampInt(value, 0, 100);
+ XiaomiCompact4_ApplyState(1);
+ break;
+ case XIAOMI_C4_CH_NIGHT_SPEED:
+ g_nightSpeed = XiaomiCompact4_ClampInt(value, 0, 100);
+ XiaomiCompact4_ApplyState(1);
+ break;
+ case XIAOMI_C4_CH_P_FACTOR:
+ g_pFactorX100 = XiaomiCompact4_ClampInt(value, 1, 1000);
+ XiaomiCompact4_ApplyState(1);
+ break;
+ case XIAOMI_C4_CH_FILTER_LIFESPAN:
+ g_filterLifespanDays = XiaomiCompact4_ClampInt(value, 30, 1000);
+ XiaomiCompact4_ApplyState(1);
+ break;
+ }
+}
+
+static commandResult_t CMD_XiaomiCompact4_SetPower(const void *context, const char *cmd, const char *args, int cmdFlags) {
+ Tokenizer_TokenizeString(args, 0);
+ if (Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS;
+ XiaomiCompact4_SetPower(Tokenizer_GetArgInteger(0));
+ return CMD_RES_OK;
+}
+
+static commandResult_t CMD_XiaomiCompact4_SetMode(const void *context, const char *cmd, const char *args, int cmdFlags) {
+ Tokenizer_TokenizeString(args, 0);
+ if (Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS;
+ XiaomiCompact4_SetMode(XiaomiCompact4_ParseMode(Tokenizer_GetArg(0)));
+ return CMD_RES_OK;
+}
+
+static commandResult_t CMD_XiaomiCompact4_SetBrightness(const void *context, const char *cmd, const char *args, int cmdFlags) {
+ Tokenizer_TokenizeString(args, 0);
+ if (Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS;
+ XiaomiCompact4_SetBrightness(XiaomiCompact4_ParseBrightness(Tokenizer_GetArg(0)));
+ return CMD_RES_OK;
+}
+
+static commandResult_t CMD_XiaomiCompact4_SetChildLock(const void *context, const char *cmd, const char *args, int cmdFlags) {
+ Tokenizer_TokenizeString(args, 0);
+ if (Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS;
+ XiaomiCompact4_SetChildLock(Tokenizer_GetArgInteger(0));
+ return CMD_RES_OK;
+}
+
+static commandResult_t CMD_XiaomiCompact4_ResetFilter(const void *context, const char *cmd, const char *args, int cmdFlags) {
+ g_filterUsageSeconds = 0;
+ XiaomiCompact4_ApplyState(1);
+ return CMD_RES_OK;
+}
+
+static commandResult_t CMD_XiaomiCompact4_SetFavSpeed(const void *context, const char *cmd, const char *args, int cmdFlags) {
+ Tokenizer_TokenizeString(args, 0);
+ if (Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS;
+ g_favSpeed = XiaomiCompact4_ClampInt(Tokenizer_GetArgInteger(0), 0, 100);
+ XiaomiCompact4_ApplyState(1);
+ return CMD_RES_OK;
+}
+
+static commandResult_t CMD_XiaomiCompact4_SetNightSpeed(const void *context, const char *cmd, const char *args, int cmdFlags) {
+ Tokenizer_TokenizeString(args, 0);
+ if (Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS;
+ g_nightSpeed = XiaomiCompact4_ClampInt(Tokenizer_GetArgInteger(0), 0, 100);
+ XiaomiCompact4_ApplyState(1);
+ return CMD_RES_OK;
+}
+
+static commandResult_t CMD_XiaomiCompact4_SetPFactor(const void *context, const char *cmd, const char *args, int cmdFlags) {
+ Tokenizer_TokenizeString(args, 0);
+ if (Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS;
+ g_pFactorX100 = XiaomiCompact4_ClampInt((int)(Tokenizer_GetArgFloat(0) * 100.0f), 1, 1000);
+ XiaomiCompact4_ApplyState(1);
+ return CMD_RES_OK;
+}
+
+static commandResult_t CMD_XiaomiCompact4_SetFilterLifespan(const void *context, const char *cmd, const char *args, int cmdFlags) {
+ Tokenizer_TokenizeString(args, 0);
+ if (Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS;
+ g_filterLifespanDays = XiaomiCompact4_ClampInt(Tokenizer_GetArgInteger(0), 30, 1000);
+ XiaomiCompact4_ApplyState(1);
+ return CMD_RES_OK;
+}
+
+void XiaomiCompact4_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState) {
+ if (bPreState) return;
+ hprintf255(request, "
Xiaomi Compact 4
");
+ hprintf255(request, "Power: %s, Mode: %s, Brightness: %s, PM2.5: %i ug/m3, RPM: %i
",
+ g_power ? "on" : "off", XiaomiCompact4_ModeToStr(g_mode),
+ XiaomiCompact4_BrightnessToStr(g_brightness), g_lastPm25, g_lastMotorRpm);
+}
+
+static void XiaomiCompact4_SetupChannels(void) {
+ CHANNEL_SetLabel(XIAOMI_C4_CH_POWER, "Power", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_MODE, "Mode", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_BRIGHTNESS, "Brightness", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_CHILD_LOCK, "Child Lock", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_PM25, "PM 2.5", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_MOTOR_RPM, "Motor Speed", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_FILTER_USAGE, "Filter Usage", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_FILTER_HEALTH, "Filter Health", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_REPLACE_FILTER, "Replace Filter", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_FAV_SPEED, "Motor Speed Favorite Mode", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_NIGHT_SPEED, "Motor Speed at Night Mode", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_P_FACTOR, "Motor Speed P Factor x100", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_FILTER_LIFESPAN, "Filter Lifespan", 1);
+ CHANNEL_SetType(XIAOMI_C4_CH_POWER, ChType_Toggle);
+ CHANNEL_SetType(XIAOMI_C4_CH_MODE, ChType_Enum);
+ CHANNEL_SetType(XIAOMI_C4_CH_BRIGHTNESS, ChType_Enum);
+ CHANNEL_SetType(XIAOMI_C4_CH_CHILD_LOCK, ChType_Toggle);
+ CHANNEL_SetType(XIAOMI_C4_CH_PM25, ChType_ReadOnly);
+ CHANNEL_SetType(XIAOMI_C4_CH_MOTOR_RPM, ChType_ReadOnly);
+ CHANNEL_SetType(XIAOMI_C4_CH_FILTER_USAGE, ChType_ReadOnly);
+ CHANNEL_SetType(XIAOMI_C4_CH_FILTER_HEALTH, ChType_ReadOnly);
+ CHANNEL_SetType(XIAOMI_C4_CH_REPLACE_FILTER, ChType_ReadOnly);
+ CHANNEL_SetType(XIAOMI_C4_CH_FAV_SPEED, ChType_Percent);
+ CHANNEL_SetType(XIAOMI_C4_CH_NIGHT_SPEED, ChType_Percent);
+ CHANNEL_SetType(XIAOMI_C4_CH_P_FACTOR, ChType_Default);
+ CHANNEL_SetType(XIAOMI_C4_CH_FILTER_LIFESPAN, ChType_Default);
+ CMD_ExecuteCommand("SetChannelEnum 1 0:FAV 1:NIGHT 2:AUTO", 0);
+ CMD_ExecuteCommand("SetChannelEnum 2 0:FULL 1:MID 2:ZERO", 0);
+}
+
+void XiaomiCompact4_Init(void) {
+ //cmddetail:{"name":"XiaomiCompact4_SetPower","args":"[0/1]","descr":"Set Xiaomi Compact 4 power","fn":"CMD_XiaomiCompact4_SetPower","file":"driver/drv_xiaomi_compact4.c","requires":"XiaomiCompact4","examples":"XiaomiCompact4_SetPower 1"}
+ CMD_RegisterCommand("XiaomiCompact4_SetPower", CMD_XiaomiCompact4_SetPower, NULL);
+ CMD_RegisterCommand("XiaomiCompact4_SetMode", CMD_XiaomiCompact4_SetMode, NULL);
+ CMD_RegisterCommand("XiaomiCompact4_SetBrightness", CMD_XiaomiCompact4_SetBrightness, NULL);
+ CMD_RegisterCommand("XiaomiCompact4_SetChildLock", CMD_XiaomiCompact4_SetChildLock, NULL);
+ CMD_RegisterCommand("XiaomiCompact4_ResetFilter", CMD_XiaomiCompact4_ResetFilter, NULL);
+ CMD_RegisterCommand("XiaomiCompact4_SetFavSpeed", CMD_XiaomiCompact4_SetFavSpeed, NULL);
+ CMD_RegisterCommand("XiaomiCompact4_SetNightSpeed", CMD_XiaomiCompact4_SetNightSpeed, NULL);
+ CMD_RegisterCommand("XiaomiCompact4_SetPFactor", CMD_XiaomiCompact4_SetPFactor, NULL);
+ CMD_RegisterCommand("XiaomiCompact4_SetFilterLifespan", CMD_XiaomiCompact4_SetFilterLifespan, NULL);
+
+ g_power = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_POWER, 0, 0, 1);
+ g_mode = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_MODE, XIAOMI_C4_MODE_AUTO, XIAOMI_C4_MODE_FAV, XIAOMI_C4_MODE_AUTO);
+ g_brightness = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_BRIGHTNESS, XIAOMI_C4_BRIGHTNESS_FULL, XIAOMI_C4_BRIGHTNESS_FULL, XIAOMI_C4_BRIGHTNESS_ZERO);
+ g_childLock = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_CHILD_LOCK, 0, 0, 1);
+ g_favSpeed = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_FAV_SPEED, 30, 0, 100);
+ g_nightSpeed = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_NIGHT_SPEED, 10, 0, 100);
+ g_pFactorX100 = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_P_FACTOR_X100, 100, 1, 1000);
+ g_filterLifespanDays = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_FILTER_LIFESPAN, 365, 30, 1000);
+ g_filterUsageSeconds = (uint32_t)XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_FILTER_USAGE, 0, 0, 0x7FFFFFFF);
+ g_pm25PollCountdown = 1;
+ g_filterCountdown = 60;
+
+ HAL_PIN_Setup_Output(XIAOMI_C4_PIN_MOTOR_EN);
+ HAL_PIN_SetOutputValue(XIAOMI_C4_PIN_MOTOR_EN, g_power ? 1 : 0);
+ HAL_PIN_Setup_Input_Pullup(XIAOMI_C4_PIN_BUTTON_POWER);
+ HAL_PIN_Setup_Input_Pullup(XIAOMI_C4_PIN_BUTTON_LIGHT);
+ HAL_PIN_Setup_Input_Pullup(XIAOMI_C4_PIN_BUTTON_MODE);
+ HAL_PIN_Setup_Input(XIAOMI_C4_PIN_TACH);
+ HAL_AttachInterrupt(XIAOMI_C4_PIN_TACH, INTERRUPT_RISING, XiaomiCompact4_TachISR);
+
+ HAL_PIN_PWM_Start(XIAOMI_C4_PIN_BUZZER, 1000);
+ HAL_PIN_PWM_Start(XIAOMI_C4_PIN_LED_RED, 1000);
+ HAL_PIN_PWM_Start(XIAOMI_C4_PIN_LED_ORANGE, 1000);
+ HAL_PIN_PWM_Start(XIAOMI_C4_PIN_LED_GREEN, 1000);
+ HAL_PIN_PWM_Update(XIAOMI_C4_PIN_BUZZER, 0);
+ XiaomiCompact4_MotorPWMInit();
+
+ XiaomiCompact4_I2CInit();
+
+ XiaomiCompact4_UARTInit();
+
+ XiaomiCompact4_SetupChannels();
+ g_initialized = 1;
+ XiaomiCompact4_ApplyState(0);
+ ADDLOG_INFO(LOG_FEATURE_DRV, "XiaomiCompact4 initialized for ESP32-WROOM-32D hardware");
+}
+
+#endif
diff --git a/src/driver/drv_xiaomi_compact4.h b/src/driver/drv_xiaomi_compact4.h
new file mode 100644
index 0000000000..ec6569f536
--- /dev/null
+++ b/src/driver/drv_xiaomi_compact4.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "../httpserver/new_http.h"
+
+void XiaomiCompact4_Init(void);
+void XiaomiCompact4_RunEverySecond(void);
+void XiaomiCompact4_RunQuickTick(void);
+void XiaomiCompact4_Stop(void);
+void XiaomiCompact4_OnChannelChanged(int ch, int value);
+void XiaomiCompact4_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState);
diff --git a/src/hal/espidf/hal_uart_espidf.c b/src/hal/espidf/hal_uart_espidf.c
index 7cbb331dd6..51f482f5bc 100644
--- a/src/hal/espidf/hal_uart_espidf.c
+++ b/src/hal/espidf/hal_uart_espidf.c
@@ -144,9 +144,10 @@ int HAL_UART_Init(int baud, int parity, bool hwflowc, int txOverride, int rxOver
uart_enable_rx_intr(uartnum);
#if PLATFORM_ESPIDF
- if (txOverride != -1) {
+ if (txOverride != -1 || rxOverride != -1) {
uart_set_pin(uartnum,
- txOverride, 21,
+ txOverride != -1 ? txOverride : UART_PIN_NO_CHANGE,
+ rxOverride != -1 ? rxOverride : UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}
#endif
@@ -172,4 +173,4 @@ int HAL_UART_Init(int baud, int parity, bool hwflowc, int txOverride, int rxOver
return 1;
}
-#endif
\ No newline at end of file
+#endif
diff --git a/src/hal/espidf/hal_wifi_espidf.c b/src/hal/espidf/hal_wifi_espidf.c
index 9e01383acf..0228feba53 100644
--- a/src/hal/espidf/hal_wifi_espidf.c
+++ b/src/hal/espidf/hal_wifi_espidf.c
@@ -235,11 +235,16 @@ void HAL_DisconnectFromWifi()
int HAL_SetupWiFiOpenAccessPoint(const char* ssid)
{
g_bOpenAccessPointMode = 1;
+ ADDLOG_INFO(LOG_FEATURE_MAIN, "WiFi AP init start");
ap_netif = esp_netif_create_default_wifi_ap();
- sta_netif = esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
cfg.nvs_enable = false;
- esp_wifi_init(&cfg);
+ esp_err_t err = esp_wifi_init(&cfg);
+ ADDLOG_INFO(LOG_FEATURE_MAIN, "WiFi AP esp_wifi_init: %s", esp_err_to_name(err));
+ if(err != ESP_OK)
+ {
+ return 0;
+ }
esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
@@ -264,18 +269,33 @@ int HAL_SetupWiFiOpenAccessPoint(const char* ssid)
},
};
- wifi_config_t wifi_sta_config =
- {
- .sta = { },
- };
strncpy((char*)wifi_ap_config.ap.ssid, (char*)ssid, 32);
esp_netif_set_hostname(ap_netif, CFG_GetDeviceName());
- esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config);
- esp_wifi_set_config(WIFI_IF_STA, &wifi_sta_config);
- esp_wifi_set_mode(WIFI_MODE_APSTA);
- esp_wifi_set_max_tx_power(10 * 4);
- esp_wifi_start();
+ err = esp_wifi_set_mode(WIFI_MODE_AP);
+ ADDLOG_INFO(LOG_FEATURE_MAIN, "WiFi AP set_mode: %s", esp_err_to_name(err));
+ if(err != ESP_OK)
+ {
+ return 0;
+ }
+ err = esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config);
+ ADDLOG_INFO(LOG_FEATURE_MAIN, "WiFi AP set_config: %s", esp_err_to_name(err));
+ if(err != ESP_OK)
+ {
+ return 0;
+ }
+ err = esp_wifi_set_max_tx_power(4 * 4);
+ ADDLOG_INFO(LOG_FEATURE_MAIN, "WiFi AP set_tx_power: %s", esp_err_to_name(err));
+ if(err != ESP_OK)
+ {
+ return 0;
+ }
+ err = esp_wifi_start();
+ ADDLOG_INFO(LOG_FEATURE_MAIN, "WiFi AP start: %s", esp_err_to_name(err));
+ if(err != ESP_OK)
+ {
+ return 0;
+ }
//esp_netif_set_default_netif(ap_netif);
diff --git a/src/httpserver/http_fns.c b/src/httpserver/http_fns.c
index 63c941e5fe..52f37cc1db 100644
--- a/src/httpserver/http_fns.c
+++ b/src/httpserver/http_fns.c
@@ -2037,6 +2037,7 @@ void doHomeAssistantDiscovery(const char* topic, http_request_t* request) {
HassDeviceInfo* dev_info = NULL;
bool measuringPower = false;
bool measuringBattery = false;
+ bool selfStateDiscoveryQueued = false;
struct cJSON_Hooks hooks;
bool discoveryQueued = false;
int type;
@@ -2597,10 +2598,11 @@ void doHomeAssistantDiscovery(const char* topic, http_request_t* request) {
MQTT_QueuePublish(topic, dev_info->channel, hass_build_discovery_json(dev_info), OBK_PUBLISH_FLAG_RETAIN);
hass_free_device_info(dev_info);
discoveryQueued = true;
+ selfStateDiscoveryQueued = true;
}
if (discoveryQueued) {
- MQTT_InvokeCommandAtEnd(PublishChannels);
+ MQTT_InvokeCommandAtEnd(selfStateDiscoveryQueued ? PublishAll : PublishChannels);
}
else {
const char* msg = "No relay, PWM, sensor or power driver running.";
diff --git a/src/httpserver/rest_interface.c b/src/httpserver/rest_interface.c
index e46f14c1cf..31ccc20a8f 100644
--- a/src/httpserver/rest_interface.c
+++ b/src/httpserver/rest_interface.c
@@ -30,7 +30,7 @@ extern uint8_t flash_size_8720;
#include "../cmnds/cmd_public.h"
#ifndef OBK_DISABLE_ALL_DRIVERS
-#include "../driver/drv_local.h"
+#include "../driver/drv_public.h"
#endif
#define MAX_JSON_VALUE_LENGTH 128
@@ -201,6 +201,9 @@ static int http_rest_post(http_request_t* request) {
OTA_IncrementProgress(1);
#if ENABLE_BT_PROXY
HAL_BTProxy_StopScan();
+#endif
+#ifndef OBK_DISABLE_ALL_DRIVERS
+ DRV_ShutdownAllDrivers();
#endif
int r = 0;
#if PLATFORM_BEKEN
diff --git a/src/obk_config.h b/src/obk_config.h
index 279290df9d..70dfacb734 100644
--- a/src/obk_config.h
+++ b/src/obk_config.h
@@ -500,6 +500,9 @@
#define ENABLE_DRIVER_SM16703P 1
#define ENABLE_DRIVER_PIXELANIM 1
#define ENABLE_DRIVER_TINYIR_NEC 1
+#if CONFIG_IDF_TARGET_ESP32
+#define ENABLE_DRIVER_XIAOMI_COMPACT4 1
+#endif
#if (OBK_VARIANT == OBK_VARIANT_ESP4M || OBK_VARIANT == OBK_VARIANT_ESP2M_BERRY)
#define ENABLE_OBK_BERRY 1
From 599bac233cb2b4b1b1926ce8b472f411ae84e8b3 Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 02:38:33 +0200
Subject: [PATCH 02/19] Polish Xiaomi Compact 4 HA discovery
---
src/driver/drv_local.h | 4 +
src/driver/drv_main.c | 4 +
src/driver/drv_xiaomi_compact4.c | 152 ++++++++++++++++++++++++++++---
src/httpserver/http_fns.c | 6 ++
4 files changed, 155 insertions(+), 11 deletions(-)
diff --git a/src/driver/drv_local.h b/src/driver/drv_local.h
index 5538e07618..d344829a32 100644
--- a/src/driver/drv_local.h
+++ b/src/driver/drv_local.h
@@ -257,6 +257,10 @@ void XiaomiCompact4_RunQuickTick(void);
void XiaomiCompact4_Stop(void);
void XiaomiCompact4_OnChannelChanged(int ch, int value);
void XiaomiCompact4_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState);
+#if ENABLE_HA_DISCOVERY
+void XiaomiCompact4_OnHassDiscovery(const char *topic);
+bool XiaomiCompact4_ShouldSkipGenericHassDiscovery(int ch);
+#endif
void UART_TCP_Init(void);
void UART_TCP_Deinit(void);
diff --git a/src/driver/drv_main.c b/src/driver/drv_main.c
index 6e5191018b..e6148cbd0b 100644
--- a/src/driver/drv_main.c
+++ b/src/driver/drv_main.c
@@ -188,7 +188,11 @@ static driver_t g_drivers[] = {
XiaomiCompact4_RunQuickTick, // runQuickTick
XiaomiCompact4_Stop, // stopFunction
XiaomiCompact4_OnChannelChanged, // onChannelChanged
+#if ENABLE_HA_DISCOVERY
+ XiaomiCompact4_OnHassDiscovery, // onHassDiscovery
+#else
NULL, // onHassDiscovery
+#endif
false, // loaded
},
#endif
diff --git a/src/driver/drv_xiaomi_compact4.c b/src/driver/drv_xiaomi_compact4.c
index e2712c3caf..f2c2dbe619 100644
--- a/src/driver/drv_xiaomi_compact4.c
+++ b/src/driver/drv_xiaomi_compact4.c
@@ -12,6 +12,9 @@
#include "../hal/hal_pins.h"
#include "../hal/hal_wifi.h"
#include "../httpserver/new_http.h"
+#if ENABLE_HA_DISCOVERY
+#include "../httpserver/hass.h"
+#endif
#include "drv_local.h"
#include "driver/i2c.h"
@@ -92,7 +95,10 @@
#define XIAOMI_C4_PM25_FRAME_LEN 20
#define XIAOMI_C4_PM25_MAX_VALID 2000
-#define XIAOMI_C4_PM25_POLL_SECONDS 1
+#define XIAOMI_C4_PM25_POLL_SECONDS 5
+#define XIAOMI_C4_FILTER_LIFESPAN_MIN_DAYS 0
+#define XIAOMI_C4_FILTER_LIFESPAN_MAX_DAYS 365
+#define XIAOMI_C4_FILTER_LIFESPAN_DEFAULT_DAYS 365
#define XIAOMI_C4_LONG_PRESS_MS 7000
static int g_power;
@@ -300,10 +306,20 @@ static int XiaomiCompact4_IsWiFiConnected(void) {
}
static int XiaomiCompact4_ReplaceFilter(void) {
+ if (g_filterLifespanDays <= 0) {
+ return 1;
+ }
uint32_t limit = (uint32_t)g_filterLifespanDays * 24U * 3600U;
return g_filterUsageSeconds > limit;
}
+static int XiaomiCompact4_FilterHealth(void) {
+ if (g_filterLifespanDays <= 0) {
+ return 0;
+ }
+ return XiaomiCompact4_ClampInt(100 - (int)((100U * g_filterUsageSeconds) / ((uint32_t)g_filterLifespanDays * 24U * 3600U)), 0, 100);
+}
+
static void XiaomiCompact4_SetChannels(void) {
g_ignoreChannelChange = 1;
CHANNEL_Set(XIAOMI_C4_CH_POWER, g_power, CHANNEL_SET_FLAG_SILENT);
@@ -315,7 +331,7 @@ static void XiaomiCompact4_SetChannels(void) {
}
CHANNEL_Set(XIAOMI_C4_CH_MOTOR_RPM, g_lastMotorRpm, CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_FILTER_USAGE, (int)g_filterUsageSeconds, CHANNEL_SET_FLAG_SILENT);
- CHANNEL_Set(XIAOMI_C4_CH_FILTER_HEALTH, XiaomiCompact4_ClampInt(100 - (int)((100U * g_filterUsageSeconds) / ((uint32_t)g_filterLifespanDays * 24U * 3600U)), 0, 100), CHANNEL_SET_FLAG_SILENT);
+ CHANNEL_Set(XIAOMI_C4_CH_FILTER_HEALTH, XiaomiCompact4_FilterHealth(), CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_REPLACE_FILTER, XiaomiCompact4_ReplaceFilter(), CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_FAV_SPEED, g_favSpeed, CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_NIGHT_SPEED, g_nightSpeed, CHANNEL_SET_FLAG_SILENT);
@@ -612,7 +628,7 @@ void XiaomiCompact4_OnChannelChanged(int ch, int value) {
XiaomiCompact4_ApplyState(1);
break;
case XIAOMI_C4_CH_FILTER_LIFESPAN:
- g_filterLifespanDays = XiaomiCompact4_ClampInt(value, 30, 1000);
+ g_filterLifespanDays = XiaomiCompact4_ClampInt(value, XIAOMI_C4_FILTER_LIFESPAN_MIN_DAYS, XIAOMI_C4_FILTER_LIFESPAN_MAX_DAYS);
XiaomiCompact4_ApplyState(1);
break;
}
@@ -679,7 +695,7 @@ static commandResult_t CMD_XiaomiCompact4_SetPFactor(const void *context, const
static commandResult_t CMD_XiaomiCompact4_SetFilterLifespan(const void *context, const char *cmd, const char *args, int cmdFlags) {
Tokenizer_TokenizeString(args, 0);
if (Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS;
- g_filterLifespanDays = XiaomiCompact4_ClampInt(Tokenizer_GetArgInteger(0), 30, 1000);
+ g_filterLifespanDays = XiaomiCompact4_ClampInt(Tokenizer_GetArgInteger(0), XIAOMI_C4_FILTER_LIFESPAN_MIN_DAYS, XIAOMI_C4_FILTER_LIFESPAN_MAX_DAYS);
XiaomiCompact4_ApplyState(1);
return CMD_RES_OK;
}
@@ -697,14 +713,14 @@ static void XiaomiCompact4_SetupChannels(void) {
CHANNEL_SetLabel(XIAOMI_C4_CH_MODE, "Mode", 1);
CHANNEL_SetLabel(XIAOMI_C4_CH_BRIGHTNESS, "Brightness", 1);
CHANNEL_SetLabel(XIAOMI_C4_CH_CHILD_LOCK, "Child Lock", 1);
- CHANNEL_SetLabel(XIAOMI_C4_CH_PM25, "PM 2.5", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_PM25, "PM2.5", 1);
CHANNEL_SetLabel(XIAOMI_C4_CH_MOTOR_RPM, "Motor Speed", 1);
CHANNEL_SetLabel(XIAOMI_C4_CH_FILTER_USAGE, "Filter Usage", 1);
CHANNEL_SetLabel(XIAOMI_C4_CH_FILTER_HEALTH, "Filter Health", 1);
CHANNEL_SetLabel(XIAOMI_C4_CH_REPLACE_FILTER, "Replace Filter", 1);
- CHANNEL_SetLabel(XIAOMI_C4_CH_FAV_SPEED, "Motor Speed Favorite Mode", 1);
- CHANNEL_SetLabel(XIAOMI_C4_CH_NIGHT_SPEED, "Motor Speed at Night Mode", 1);
- CHANNEL_SetLabel(XIAOMI_C4_CH_P_FACTOR, "Motor Speed P Factor x100", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_FAV_SPEED, "Favorite Motor Speed", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_NIGHT_SPEED, "Night Motor Speed", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_P_FACTOR, "Auto Motor Speed x100", 1);
CHANNEL_SetLabel(XIAOMI_C4_CH_FILTER_LIFESPAN, "Filter Lifespan", 1);
CHANNEL_SetType(XIAOMI_C4_CH_POWER, ChType_Toggle);
CHANNEL_SetType(XIAOMI_C4_CH_MODE, ChType_Enum);
@@ -717,12 +733,126 @@ static void XiaomiCompact4_SetupChannels(void) {
CHANNEL_SetType(XIAOMI_C4_CH_REPLACE_FILTER, ChType_ReadOnly);
CHANNEL_SetType(XIAOMI_C4_CH_FAV_SPEED, ChType_Percent);
CHANNEL_SetType(XIAOMI_C4_CH_NIGHT_SPEED, ChType_Percent);
- CHANNEL_SetType(XIAOMI_C4_CH_P_FACTOR, ChType_Default);
- CHANNEL_SetType(XIAOMI_C4_CH_FILTER_LIFESPAN, ChType_Default);
+ CHANNEL_SetType(XIAOMI_C4_CH_P_FACTOR, ChType_ReadOnly);
+ CHANNEL_SetType(XIAOMI_C4_CH_FILTER_LIFESPAN, ChType_ReadOnly);
CMD_ExecuteCommand("SetChannelEnum 1 0:FAV 1:NIGHT 2:AUTO", 0);
CMD_ExecuteCommand("SetChannelEnum 2 0:FULL 1:MID 2:ZERO", 0);
}
+#if ENABLE_HA_DISCOVERY
+static void XiaomiCompact4_HassSetName(HassDeviceInfo *dev_info, const char *name) {
+ cJSON_ReplaceItemInObject(dev_info->root, "name", cJSON_CreateString(name));
+}
+
+static void XiaomiCompact4_HassSetCommandTopic(HassDeviceInfo *dev_info, const char *command) {
+ char topic[96];
+ snprintf(topic, sizeof(topic), "cmnd/%s/%s", CFG_GetMQTTClientId(), command);
+ cJSON_ReplaceItemInObject(dev_info->root, "cmd_t", cJSON_CreateString(topic));
+}
+
+static void XiaomiCompact4_HassPublish(const char *topic, HassDeviceInfo *dev_info) {
+ if (dev_info == NULL) {
+ return;
+ }
+ MQTT_QueuePublish(topic, dev_info->channel, hass_build_discovery_json(dev_info), OBK_PUBLISH_FLAG_RETAIN);
+ hass_free_device_info(dev_info);
+}
+
+static void XiaomiCompact4_HassClearOldSensor(const char *topic, int ch) {
+ HassDeviceInfo *dev_info = hass_init_sensor_device_info(CUSTOM_SENSOR, ch, -1, -1, 1);
+ if (dev_info == NULL) {
+ return;
+ }
+ MQTT_QueuePublish(topic, dev_info->channel, "", OBK_PUBLISH_FLAG_RETAIN);
+ hass_free_device_info(dev_info);
+}
+
+static void XiaomiCompact4_HassPublishSensor(const char *topic, int ch, const char *name, const char *unit, const char *deviceClass, const char *icon) {
+ HassDeviceInfo *dev_info = hass_init_sensor_device_info(CUSTOM_SENSOR, ch, -1, -1, 1);
+ if (dev_info == NULL) {
+ return;
+ }
+ XiaomiCompact4_HassSetName(dev_info, name);
+ cJSON_DeleteItemFromObject(dev_info->root, "pl_on");
+ cJSON_DeleteItemFromObject(dev_info->root, "pl_off");
+ if (unit) {
+ cJSON_AddStringToObject(dev_info->root, "unit_of_meas", unit);
+ }
+ if (deviceClass) {
+ cJSON_AddStringToObject(dev_info->root, "dev_cla", deviceClass);
+ }
+ if (icon) {
+ cJSON_AddStringToObject(dev_info->root, "icon", icon);
+ }
+ XiaomiCompact4_HassPublish(topic, dev_info);
+}
+
+static void XiaomiCompact4_HassPublishNumber(const char *topic, int ch, const char *name, const char *command, float min, float max, float step, const char *unit, const char *deviceClass, const char *valueTemplate) {
+ HassDeviceInfo *dev_info = hass_init_sensor_device_info(HASS_PERCENT, ch, -1, -1, 1);
+ if (dev_info == NULL) {
+ return;
+ }
+ XiaomiCompact4_HassSetName(dev_info, name);
+ XiaomiCompact4_HassSetCommandTopic(dev_info, command);
+ cJSON_DeleteItemFromObject(dev_info->root, "pl_on");
+ cJSON_DeleteItemFromObject(dev_info->root, "pl_off");
+ cJSON_ReplaceItemInObject(dev_info->root, "min", cJSON_CreateNumber(min));
+ cJSON_ReplaceItemInObject(dev_info->root, "max", cJSON_CreateNumber(max));
+ cJSON_ReplaceItemInObject(dev_info->root, "step", cJSON_CreateNumber(step));
+ cJSON_DeleteItemFromObject(dev_info->root, "stat_cla");
+ if (unit) {
+ cJSON_ReplaceItemInObject(dev_info->root, "unit_of_meas", cJSON_CreateString(unit));
+ } else {
+ cJSON_DeleteItemFromObject(dev_info->root, "unit_of_meas");
+ }
+ if (valueTemplate) {
+ cJSON_AddStringToObject(dev_info->root, "val_tpl", valueTemplate);
+ }
+ if (deviceClass) {
+ cJSON_AddStringToObject(dev_info->root, "dev_cla", deviceClass);
+ }
+ cJSON_DeleteItemFromObject(dev_info->root, "entity_category");
+ cJSON_AddStringToObject(dev_info->root, "entity_category", "config");
+ XiaomiCompact4_HassPublish(topic, dev_info);
+}
+
+void XiaomiCompact4_OnHassDiscovery(const char *topic) {
+ HassDeviceInfo *dev_info;
+
+ XiaomiCompact4_HassPublishSensor(topic, XIAOMI_C4_CH_PM25, "PM2.5", "ug/m3", NULL, "mdi:air-filter");
+ XiaomiCompact4_HassPublishSensor(topic, XIAOMI_C4_CH_MOTOR_RPM, "Motor Speed", "rpm", NULL, "mdi:fan");
+ XiaomiCompact4_HassPublishSensor(topic, XIAOMI_C4_CH_FILTER_USAGE, "Filter Usage", "s", "duration", "mdi:air-filter");
+ XiaomiCompact4_HassPublishSensor(topic, XIAOMI_C4_CH_FILTER_HEALTH, "Filter Health", "%", NULL, "mdi:air-filter");
+
+ XiaomiCompact4_HassClearOldSensor(topic, XIAOMI_C4_CH_REPLACE_FILTER);
+ dev_info = hass_init_binary_sensor_device_info(XIAOMI_C4_CH_REPLACE_FILTER, true);
+ if (dev_info) {
+ XiaomiCompact4_HassSetName(dev_info, "Replace Filter");
+ cJSON_AddStringToObject(dev_info->root, "dev_cla", "problem");
+ cJSON_AddStringToObject(dev_info->root, "icon", "mdi:air-filter");
+ XiaomiCompact4_HassPublish(topic, dev_info);
+ }
+
+ XiaomiCompact4_HassPublishNumber(topic, XIAOMI_C4_CH_FAV_SPEED, "Favorite Motor Speed", "XiaomiCompact4_SetFavSpeed", 0, 100, 1, NULL, NULL, NULL);
+ XiaomiCompact4_HassPublishNumber(topic, XIAOMI_C4_CH_NIGHT_SPEED, "Night Motor Speed", "XiaomiCompact4_SetNightSpeed", 0, 100, 1, NULL, NULL, NULL);
+ XiaomiCompact4_HassPublishNumber(topic, XIAOMI_C4_CH_P_FACTOR, "Auto Motor Speed", "XiaomiCompact4_SetPFactor", 0.01f, 10.0f, 0.01f, NULL, NULL, "{{ ((value | float(0)) / 100) | round(2) }}");
+ XiaomiCompact4_HassPublishNumber(topic, XIAOMI_C4_CH_FILTER_LIFESPAN, "Filter Lifespan", "XiaomiCompact4_SetFilterLifespan", XIAOMI_C4_FILTER_LIFESPAN_MIN_DAYS, XIAOMI_C4_FILTER_LIFESPAN_MAX_DAYS, 1, "d", "duration", NULL);
+
+ dev_info = hass_init_button_device_info("reset", "XiaomiCompact4_ResetFilter", "1", HASS_CATEGORY_CONFIG);
+ if (dev_info) {
+ XiaomiCompact4_HassSetName(dev_info, "Reset Filter");
+ XiaomiCompact4_HassPublish(topic, dev_info);
+ }
+}
+
+bool XiaomiCompact4_ShouldSkipGenericHassDiscovery(int ch) {
+ if (!g_initialized) {
+ return false;
+ }
+ return ch >= XIAOMI_C4_CH_PM25 && ch <= XIAOMI_C4_CH_FILTER_LIFESPAN;
+}
+#endif
+
void XiaomiCompact4_Init(void) {
//cmddetail:{"name":"XiaomiCompact4_SetPower","args":"[0/1]","descr":"Set Xiaomi Compact 4 power","fn":"CMD_XiaomiCompact4_SetPower","file":"driver/drv_xiaomi_compact4.c","requires":"XiaomiCompact4","examples":"XiaomiCompact4_SetPower 1"}
CMD_RegisterCommand("XiaomiCompact4_SetPower", CMD_XiaomiCompact4_SetPower, NULL);
@@ -742,7 +872,7 @@ void XiaomiCompact4_Init(void) {
g_favSpeed = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_FAV_SPEED, 30, 0, 100);
g_nightSpeed = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_NIGHT_SPEED, 10, 0, 100);
g_pFactorX100 = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_P_FACTOR_X100, 100, 1, 1000);
- g_filterLifespanDays = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_FILTER_LIFESPAN, 365, 30, 1000);
+ g_filterLifespanDays = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_FILTER_LIFESPAN, XIAOMI_C4_FILTER_LIFESPAN_DEFAULT_DAYS, XIAOMI_C4_FILTER_LIFESPAN_MIN_DAYS, XIAOMI_C4_FILTER_LIFESPAN_MAX_DAYS);
g_filterUsageSeconds = (uint32_t)XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_FILTER_USAGE, 0, 0, 0x7FFFFFFF);
g_pm25PollCountdown = 1;
g_filterCountdown = 60;
diff --git a/src/httpserver/http_fns.c b/src/httpserver/http_fns.c
index 52f37cc1db..47f4689d01 100644
--- a/src/httpserver/http_fns.c
+++ b/src/httpserver/http_fns.c
@@ -2281,6 +2281,12 @@ void doHomeAssistantDiscovery(const char* topic, http_request_t* request) {
if (BIT_CHECK(flagsChannelPublished, i)) {
continue;
}
+#if ENABLE_DRIVER_XIAOMI_COMPACT4
+ if (XiaomiCompact4_ShouldSkipGenericHassDiscovery(i)) {
+ BIT_SET(flagsChannelPublished, i);
+ continue;
+ }
+#endif
dev_info = 0;
switch (type)
{
From 0e17c7e38fffb149e10a379ae06422dd8ea8e1ef Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 02:49:32 +0200
Subject: [PATCH 03/19] Fix Xiaomi HA discovery link guard
---
src/driver/drv_local.h | 2 +-
src/driver/drv_xiaomi_compact4.c | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/driver/drv_local.h b/src/driver/drv_local.h
index d344829a32..f106dd045a 100644
--- a/src/driver/drv_local.h
+++ b/src/driver/drv_local.h
@@ -257,9 +257,9 @@ void XiaomiCompact4_RunQuickTick(void);
void XiaomiCompact4_Stop(void);
void XiaomiCompact4_OnChannelChanged(int ch, int value);
void XiaomiCompact4_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState);
+bool XiaomiCompact4_ShouldSkipGenericHassDiscovery(int ch);
#if ENABLE_HA_DISCOVERY
void XiaomiCompact4_OnHassDiscovery(const char *topic);
-bool XiaomiCompact4_ShouldSkipGenericHassDiscovery(int ch);
#endif
void UART_TCP_Init(void);
diff --git a/src/driver/drv_xiaomi_compact4.c b/src/driver/drv_xiaomi_compact4.c
index f2c2dbe619..2106045c9d 100644
--- a/src/driver/drv_xiaomi_compact4.c
+++ b/src/driver/drv_xiaomi_compact4.c
@@ -845,13 +845,14 @@ void XiaomiCompact4_OnHassDiscovery(const char *topic) {
}
}
+#endif
+
bool XiaomiCompact4_ShouldSkipGenericHassDiscovery(int ch) {
if (!g_initialized) {
return false;
}
return ch >= XIAOMI_C4_CH_PM25 && ch <= XIAOMI_C4_CH_FILTER_LIFESPAN;
}
-#endif
void XiaomiCompact4_Init(void) {
//cmddetail:{"name":"XiaomiCompact4_SetPower","args":"[0/1]","descr":"Set Xiaomi Compact 4 power","fn":"CMD_XiaomiCompact4_SetPower","file":"driver/drv_xiaomi_compact4.c","requires":"XiaomiCompact4","examples":"XiaomiCompact4_SetPower 1"}
From 56a4d00af0cc4d17be3ae2c2ca8ec328ba47d0bf Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 02:58:43 +0200
Subject: [PATCH 04/19] Fix Xiaomi ESP-IDF config include order
---
src/driver/drv_xiaomi_compact4.c | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/driver/drv_xiaomi_compact4.c b/src/driver/drv_xiaomi_compact4.c
index 2106045c9d..7cb9275bef 100644
--- a/src/driver/drv_xiaomi_compact4.c
+++ b/src/driver/drv_xiaomi_compact4.c
@@ -1,8 +1,7 @@
-#include "../obk_config.h"
+#include "../new_common.h"
#if ENABLE_DRIVER_XIAOMI_COMPACT4
-#include "../new_common.h"
#include "../new_cfg.h"
#include "../new_pins.h"
#include "../cmnds/cmd_public.h"
From ec60650e480e00dd6b4b6877ea140091e1163f59 Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 03:00:10 +0200
Subject: [PATCH 05/19] Remove local Xiaomi notes ignore
---
.gitignore | 3 ---
1 file changed, 3 deletions(-)
diff --git a/.gitignore b/.gitignore
index 75c5cb3db2..d22fec2403 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,6 +44,3 @@ enc_temp_folder/**
*.i
*.ii
*.s
-
-# Local planning/reference notes
-/Xiaomi/
From 1aaf5ee601f6eee992b9ddf75c5a2373a1b642df Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 03:15:07 +0200
Subject: [PATCH 06/19] Use ESP-IDF v5 I2C master API
---
src/driver/drv_xiaomi_compact4.c | 63 +++++++++++++++++++++++++++-----
1 file changed, 54 insertions(+), 9 deletions(-)
diff --git a/src/driver/drv_xiaomi_compact4.c b/src/driver/drv_xiaomi_compact4.c
index 7cb9275bef..588cecef82 100644
--- a/src/driver/drv_xiaomi_compact4.c
+++ b/src/driver/drv_xiaomi_compact4.c
@@ -16,7 +16,7 @@
#endif
#include "drv_local.h"
-#include "driver/i2c.h"
+#include "driver/i2c_master.h"
#include "driver/ledc.h"
#include "driver/uart.h"
@@ -128,6 +128,12 @@ static int g_ignoreChannelChange;
static int g_initialized;
static uint8_t g_pm25RxBuf[XIAOMI_C4_UART_BUF_SIZE];
static int g_pm25RxLen;
+static i2c_master_bus_handle_t g_i2cBus;
+static i2c_master_dev_handle_t g_i2cBrightness;
+static i2c_master_dev_handle_t g_i2cStatusLeds;
+static i2c_master_dev_handle_t g_i2cButtonPower;
+static i2c_master_dev_handle_t g_i2cButtonBrightness;
+static i2c_master_dev_handle_t g_i2cButtonMode;
static int XiaomiCompact4_ClampInt(int value, int min, int max) {
if (value < min) return min;
@@ -188,23 +194,62 @@ static int XiaomiCompact4_ParseBrightness(const char *s) {
}
static void XiaomiCompact4_I2CInit(void) {
- i2c_config_t conf = {
- .mode = I2C_MODE_MASTER,
+ i2c_master_bus_config_t busConfig = {
+ .clk_source = I2C_CLK_SRC_DEFAULT,
+ .i2c_port = XIAOMI_C4_I2C_PORT,
.sda_io_num = XIAOMI_C4_PIN_I2C_SDA,
.scl_io_num = XIAOMI_C4_PIN_I2C_SCL,
- .sda_pullup_en = GPIO_PULLUP_ENABLE,
- .scl_pullup_en = GPIO_PULLUP_ENABLE,
- .master.clk_speed = XIAOMI_C4_I2C_FREQ_HZ,
+ .flags.enable_internal_pullup = true,
};
- i2c_param_config(XIAOMI_C4_I2C_PORT, &conf);
- if (i2c_driver_install(XIAOMI_C4_I2C_PORT, I2C_MODE_MASTER, 0, 0, 0) != ESP_OK) {
+ if (i2c_new_master_bus(&busConfig, &g_i2cBus) != ESP_OK) {
ADDLOG_WARN(LOG_FEATURE_DRV, "XiaomiCompact4 I2C driver install failed");
+ return;
}
+
+ i2c_device_config_t devConfig = {
+ .dev_addr_length = I2C_ADDR_BIT_LEN_7,
+ .scl_speed_hz = XIAOMI_C4_I2C_FREQ_HZ,
+ };
+
+ devConfig.device_address = XIAOMI_C4_I2C_BRIGHTNESS;
+ i2c_master_bus_add_device(g_i2cBus, &devConfig, &g_i2cBrightness);
+ devConfig.device_address = XIAOMI_C4_I2C_STATUS_LEDS;
+ i2c_master_bus_add_device(g_i2cBus, &devConfig, &g_i2cStatusLeds);
+ devConfig.device_address = XIAOMI_C4_I2C_BUTTON_POWER;
+ i2c_master_bus_add_device(g_i2cBus, &devConfig, &g_i2cButtonPower);
+ devConfig.device_address = XIAOMI_C4_I2C_BUTTON_BRIGHTNESS;
+ i2c_master_bus_add_device(g_i2cBus, &devConfig, &g_i2cButtonBrightness);
+ devConfig.device_address = XIAOMI_C4_I2C_BUTTON_MODE;
+ i2c_master_bus_add_device(g_i2cBus, &devConfig, &g_i2cButtonMode);
}
static void XiaomiCompact4_I2CWrite1(int addr7, int value) {
uint8_t data = (uint8_t)value;
- esp_err_t err = i2c_master_write_to_device(XIAOMI_C4_I2C_PORT, addr7, &data, 1, 100 / portTICK_PERIOD_MS);
+ i2c_master_dev_handle_t dev;
+ switch (addr7) {
+ case XIAOMI_C4_I2C_BRIGHTNESS:
+ dev = g_i2cBrightness;
+ break;
+ case XIAOMI_C4_I2C_STATUS_LEDS:
+ dev = g_i2cStatusLeds;
+ break;
+ case XIAOMI_C4_I2C_BUTTON_POWER:
+ dev = g_i2cButtonPower;
+ break;
+ case XIAOMI_C4_I2C_BUTTON_BRIGHTNESS:
+ dev = g_i2cButtonBrightness;
+ break;
+ case XIAOMI_C4_I2C_BUTTON_MODE:
+ dev = g_i2cButtonMode;
+ break;
+ default:
+ dev = NULL;
+ break;
+ }
+ if (dev == NULL) {
+ return;
+ }
+ esp_err_t err = i2c_master_transmit(dev, &data, 1, 100);
if (err != ESP_OK) {
ADDLOG_DEBUG(LOG_FEATURE_DRV, "XiaomiCompact4 I2C write failed addr 0x%02X err %i", addr7, err);
}
From 75e247f8a013bbffde21e6cafd4d2fdbceb5f6e0 Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 03:27:33 +0200
Subject: [PATCH 07/19] Keep HA discovery channel publish narrow
---
src/httpserver/http_fns.c | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/httpserver/http_fns.c b/src/httpserver/http_fns.c
index 47f4689d01..f185eb02cc 100644
--- a/src/httpserver/http_fns.c
+++ b/src/httpserver/http_fns.c
@@ -2037,7 +2037,6 @@ void doHomeAssistantDiscovery(const char* topic, http_request_t* request) {
HassDeviceInfo* dev_info = NULL;
bool measuringPower = false;
bool measuringBattery = false;
- bool selfStateDiscoveryQueued = false;
struct cJSON_Hooks hooks;
bool discoveryQueued = false;
int type;
@@ -2604,11 +2603,10 @@ void doHomeAssistantDiscovery(const char* topic, http_request_t* request) {
MQTT_QueuePublish(topic, dev_info->channel, hass_build_discovery_json(dev_info), OBK_PUBLISH_FLAG_RETAIN);
hass_free_device_info(dev_info);
discoveryQueued = true;
- selfStateDiscoveryQueued = true;
}
if (discoveryQueued) {
- MQTT_InvokeCommandAtEnd(selfStateDiscoveryQueued ? PublishAll : PublishChannels);
+ MQTT_InvokeCommandAtEnd(PublishChannels);
}
else {
const char* msg = "No relay, PWM, sensor or power driver running.";
From c211156f95385a9f3c531adba6cdb342b0b418ea Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 15:24:14 +0200
Subject: [PATCH 08/19] Finish Xiaomi Compact 4 HA controls and PM2.5 handling
---
src/driver/drv_xiaomi_compact4.c | 202 ++++++++++++++++++++++++++++---
src/httpserver/http_fns.c | 12 ++
2 files changed, 199 insertions(+), 15 deletions(-)
diff --git a/src/driver/drv_xiaomi_compact4.c b/src/driver/drv_xiaomi_compact4.c
index 588cecef82..c08a0fe500 100644
--- a/src/driver/drv_xiaomi_compact4.c
+++ b/src/driver/drv_xiaomi_compact4.c
@@ -64,6 +64,7 @@
#define XIAOMI_C4_CH_NIGHT_SPEED 10
#define XIAOMI_C4_CH_P_FACTOR 11
#define XIAOMI_C4_CH_FILTER_LIFESPAN 12
+#define XIAOMI_C4_CH_BUZZER 13
#define XIAOMI_C4_FLASH_BASE 48
#define XIAOMI_C4_VAR_POWER (XIAOMI_C4_FLASH_BASE + 0)
@@ -75,6 +76,7 @@
#define XIAOMI_C4_VAR_P_FACTOR_X100 (XIAOMI_C4_FLASH_BASE + 6)
#define XIAOMI_C4_VAR_FILTER_LIFESPAN (XIAOMI_C4_FLASH_BASE + 7)
#define XIAOMI_C4_VAR_FILTER_USAGE (XIAOMI_C4_FLASH_BASE + 8)
+#define XIAOMI_C4_VAR_BUZZER (XIAOMI_C4_FLASH_BASE + 9)
#define XIAOMI_C4_MODE_FAV 0
#define XIAOMI_C4_MODE_NIGHT 1
@@ -94,7 +96,10 @@
#define XIAOMI_C4_PM25_FRAME_LEN 20
#define XIAOMI_C4_PM25_MAX_VALID 2000
+#define XIAOMI_C4_PM25_HIGH_LOG_THRESHOLD 300
#define XIAOMI_C4_PM25_POLL_SECONDS 5
+#define XIAOMI_C4_MOTOR_RAMP_UP_PERCENT_PER_SEC 6
+#define XIAOMI_C4_MOTOR_RAMP_DOWN_PERCENT_PER_SEC 12
#define XIAOMI_C4_FILTER_LIFESPAN_MIN_DAYS 0
#define XIAOMI_C4_FILTER_LIFESPAN_MAX_DAYS 365
#define XIAOMI_C4_FILTER_LIFESPAN_DEFAULT_DAYS 365
@@ -108,9 +113,12 @@ static int g_favSpeed;
static int g_nightSpeed;
static int g_pFactorX100;
static int g_filterLifespanDays;
+static int g_buzzer;
static uint32_t g_filterUsageSeconds;
static int g_lastPm25 = -1;
static int g_lastMotorRpm;
+static int g_motorTargetPercent;
+static int g_motorCurrentPercent;
static int g_lastButtonPower;
static int g_lastButtonLight;
static int g_lastButtonMode;
@@ -128,6 +136,11 @@ static int g_ignoreChannelChange;
static int g_initialized;
static uint8_t g_pm25RxBuf[XIAOMI_C4_UART_BUF_SIZE];
static int g_pm25RxLen;
+static uint8_t g_pm25LastFrame[XIAOMI_C4_PM25_FRAME_LEN];
+static int g_pm25LastRaw = -1;
+static int g_pm25AcceptedFrames;
+static int g_pm25RejectedChecksum;
+static int g_pm25RejectedRange;
static i2c_master_bus_handle_t g_i2cBus;
static i2c_master_dev_handle_t g_i2cBrightness;
static i2c_master_dev_handle_t g_i2cStatusLeds;
@@ -305,6 +318,22 @@ static void XiaomiCompact4_UARTConsume(int count) {
g_pm25RxLen -= count;
}
+static int XiaomiCompact4_PM25FrameChecksumValid(const uint8_t *frame) {
+ uint8_t sum = 0;
+ for (int i = 0; i < XIAOMI_C4_PM25_FRAME_LEN; i++) {
+ sum += frame[i];
+ }
+ return sum == 0;
+}
+
+static void XiaomiCompact4_PM25FrameToHex(const uint8_t *frame, char *out, int outLen) {
+ int pos = 0;
+ for (int i = 0; i < XIAOMI_C4_PM25_FRAME_LEN && pos < outLen - 1; i++) {
+ pos += snprintf(out + pos, outLen - pos, "%02X%s", frame[i], i == XIAOMI_C4_PM25_FRAME_LEN - 1 ? "" : " ");
+ }
+ out[outLen - 1] = '\0';
+}
+
static void XiaomiCompact4_MotorPWMInit(void) {
ledc_timer_config_t timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
@@ -336,6 +365,30 @@ static void XiaomiCompact4_MotorPWMSet(int frequency) {
ledc_update_duty(LEDC_LOW_SPEED_MODE, XIAOMI_C4_MOTOR_LEDC_CHANNEL);
}
+static int XiaomiCompact4_MotorPercentToFrequency(int percent) {
+ return (int)((float)percent * 4.12f + 100.0f);
+}
+
+static void XiaomiCompact4_ApplyMotorRamp(void) {
+ if (!g_power) {
+ g_motorCurrentPercent = 0;
+ XiaomiCompact4_MotorPWMSet(0);
+ return;
+ }
+ if (g_motorCurrentPercent < g_motorTargetPercent) {
+ g_motorCurrentPercent += XIAOMI_C4_MOTOR_RAMP_UP_PERCENT_PER_SEC;
+ if (g_motorCurrentPercent > g_motorTargetPercent) {
+ g_motorCurrentPercent = g_motorTargetPercent;
+ }
+ } else if (g_motorCurrentPercent > g_motorTargetPercent) {
+ g_motorCurrentPercent -= XIAOMI_C4_MOTOR_RAMP_DOWN_PERCENT_PER_SEC;
+ if (g_motorCurrentPercent < g_motorTargetPercent) {
+ g_motorCurrentPercent = g_motorTargetPercent;
+ }
+ }
+ XiaomiCompact4_MotorPWMSet(XiaomiCompact4_MotorPercentToFrequency(g_motorCurrentPercent));
+}
+
static void XiaomiCompact4_SetLedPWM(int pin, float level) {
if (level <= 0.0f) {
HAL_PIN_PWM_Update(pin, 0);
@@ -364,6 +417,10 @@ static int XiaomiCompact4_FilterHealth(void) {
return XiaomiCompact4_ClampInt(100 - (int)((100U * g_filterUsageSeconds) / ((uint32_t)g_filterLifespanDays * 24U * 3600U)), 0, 100);
}
+static int XiaomiCompact4_FilterUsageDays(void) {
+ return (int)(g_filterUsageSeconds / (24U * 3600U));
+}
+
static void XiaomiCompact4_SetChannels(void) {
g_ignoreChannelChange = 1;
CHANNEL_Set(XIAOMI_C4_CH_POWER, g_power, CHANNEL_SET_FLAG_SILENT);
@@ -374,13 +431,14 @@ static void XiaomiCompact4_SetChannels(void) {
CHANNEL_Set(XIAOMI_C4_CH_PM25, g_lastPm25, CHANNEL_SET_FLAG_SILENT);
}
CHANNEL_Set(XIAOMI_C4_CH_MOTOR_RPM, g_lastMotorRpm, CHANNEL_SET_FLAG_SILENT);
- CHANNEL_Set(XIAOMI_C4_CH_FILTER_USAGE, (int)g_filterUsageSeconds, CHANNEL_SET_FLAG_SILENT);
+ CHANNEL_Set(XIAOMI_C4_CH_FILTER_USAGE, XiaomiCompact4_FilterUsageDays(), CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_FILTER_HEALTH, XiaomiCompact4_FilterHealth(), CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_REPLACE_FILTER, XiaomiCompact4_ReplaceFilter(), CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_FAV_SPEED, g_favSpeed, CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_NIGHT_SPEED, g_nightSpeed, CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_P_FACTOR, g_pFactorX100, CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_FILTER_LIFESPAN, g_filterLifespanDays, CHANNEL_SET_FLAG_SILENT);
+ CHANNEL_Set(XIAOMI_C4_CH_BUZZER, g_buzzer, CHANNEL_SET_FLAG_SILENT);
g_ignoreChannelChange = 0;
}
@@ -394,13 +452,14 @@ static void XiaomiCompact4_SaveState(void) {
XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_P_FACTOR_X100, g_pFactorX100);
XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_FILTER_LIFESPAN, g_filterLifespanDays);
XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_FILTER_USAGE, (int)g_filterUsageSeconds);
+ XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_BUZZER, g_buzzer);
}
static void XiaomiCompact4_StartChildLockNotification(void) {
g_childLockNotificationTicks = 1;
g_childLockNotificationFlips = 8;
g_childLockNotificationState = 0;
- HAL_PIN_PWM_Update(XIAOMI_C4_PIN_BUZZER, 50);
+ HAL_PIN_PWM_Update(XIAOMI_C4_PIN_BUZZER, g_buzzer ? 50 : 0);
}
static void XiaomiCompact4_UpdateHID(void) {
@@ -461,6 +520,8 @@ static void XiaomiCompact4_UpdateHID(void) {
static void XiaomiCompact4_UpdateMotor(void) {
if (!g_power) {
+ g_motorTargetPercent = 0;
+ g_motorCurrentPercent = 0;
XiaomiCompact4_MotorPWMSet(0);
return;
}
@@ -476,7 +537,7 @@ static void XiaomiCompact4_UpdateMotor(void) {
} else {
motorSetpoint = g_nightSpeed;
}
- XiaomiCompact4_MotorPWMSet((int)(motorSetpoint * 4.12f + 100.0f));
+ g_motorTargetPercent = XiaomiCompact4_ClampInt((int)(motorSetpoint + 0.5f), 0, 100);
}
static void XiaomiCompact4_ApplyState(int save) {
@@ -579,14 +640,36 @@ static void XiaomiCompact4_ProcessPM25UART(void) {
if (g_pm25RxLen < XIAOMI_C4_PM25_FRAME_LEN) {
break;
}
- int pm25 = (((int)XiaomiCompact4_UARTPeek(15)) << 8) | XiaomiCompact4_UARTPeek(16);
+ uint8_t frame[XIAOMI_C4_PM25_FRAME_LEN];
+ memcpy(frame, g_pm25RxBuf, XIAOMI_C4_PM25_FRAME_LEN);
XiaomiCompact4_UARTConsume(XIAOMI_C4_PM25_FRAME_LEN);
- if (pm25 <= XIAOMI_C4_PM25_MAX_VALID) {
- g_lastPm25 = pm25;
- XiaomiCompact4_UpdateHID();
- XiaomiCompact4_UpdateMotor();
- XiaomiCompact4_SetChannels();
+ if (!XiaomiCompact4_PM25FrameChecksumValid(frame)) {
+ g_pm25RejectedChecksum++;
+ if ((g_pm25RejectedChecksum % 20) == 1) {
+ char hex[XIAOMI_C4_PM25_FRAME_LEN * 3];
+ XiaomiCompact4_PM25FrameToHex(frame, hex, sizeof(hex));
+ ADDLOG_WARN(LOG_FEATURE_DRV, "XiaomiCompact4 PM25 bad checksum frame: %s", hex);
+ }
+ continue;
+ }
+ int pm25 = (((int)frame[15]) << 8) | frame[16];
+ memcpy(g_pm25LastFrame, frame, sizeof(g_pm25LastFrame));
+ g_pm25LastRaw = pm25;
+ if (pm25 > XIAOMI_C4_PM25_MAX_VALID) {
+ g_pm25RejectedRange++;
+ ADDLOG_WARN(LOG_FEATURE_DRV, "XiaomiCompact4 PM25 out of range %i", pm25);
+ continue;
}
+ if (pm25 >= XIAOMI_C4_PM25_HIGH_LOG_THRESHOLD) {
+ char hex[XIAOMI_C4_PM25_FRAME_LEN * 3];
+ XiaomiCompact4_PM25FrameToHex(frame, hex, sizeof(hex));
+ ADDLOG_WARN(LOG_FEATURE_DRV, "XiaomiCompact4 PM25 high %i frame: %s", pm25, hex);
+ }
+ g_pm25AcceptedFrames++;
+ g_lastPm25 = pm25;
+ XiaomiCompact4_UpdateHID();
+ XiaomiCompact4_UpdateMotor();
+ XiaomiCompact4_SetChannels();
}
}
@@ -602,6 +685,8 @@ static void XiaomiCompact4_ServiceNotification(void) {
g_childLockNotificationFlips--;
if (g_childLockNotificationFlips == 0) {
HAL_PIN_PWM_Update(XIAOMI_C4_PIN_BUZZER, 0);
+ } else {
+ HAL_PIN_PWM_Update(XIAOMI_C4_PIN_BUZZER, (g_buzzer && g_childLockNotificationState) ? 50 : 0);
}
XiaomiCompact4_UpdateHID();
}
@@ -633,11 +718,14 @@ void XiaomiCompact4_RunEverySecond(void) {
}
g_lastMotorRpm = (int)((g_tachPulses * 60U) / 15U);
g_tachPulses = 0;
+ XiaomiCompact4_ApplyMotorRamp();
XiaomiCompact4_UpdateHID();
XiaomiCompact4_SetChannels();
}
void XiaomiCompact4_Stop(void) {
+ g_motorTargetPercent = 0;
+ g_motorCurrentPercent = 0;
XiaomiCompact4_MotorPWMSet(0);
HAL_PIN_SetOutputValue(XIAOMI_C4_PIN_MOTOR_EN, 0);
HAL_DetachInterrupt(XIAOMI_C4_PIN_TACH);
@@ -675,6 +763,13 @@ void XiaomiCompact4_OnChannelChanged(int ch, int value) {
g_filterLifespanDays = XiaomiCompact4_ClampInt(value, XIAOMI_C4_FILTER_LIFESPAN_MIN_DAYS, XIAOMI_C4_FILTER_LIFESPAN_MAX_DAYS);
XiaomiCompact4_ApplyState(1);
break;
+ case XIAOMI_C4_CH_BUZZER:
+ g_buzzer = value ? 1 : 0;
+ if (!g_buzzer) {
+ HAL_PIN_PWM_Update(XIAOMI_C4_PIN_BUZZER, 0);
+ }
+ XiaomiCompact4_ApplyState(1);
+ break;
}
}
@@ -744,12 +839,21 @@ static commandResult_t CMD_XiaomiCompact4_SetFilterLifespan(const void *context,
return CMD_RES_OK;
}
+static commandResult_t CMD_XiaomiCompact4_PM25Stats(const void *context, const char *cmd, const char *args, int cmdFlags) {
+ char hex[XIAOMI_C4_PM25_FRAME_LEN * 3];
+ XiaomiCompact4_PM25FrameToHex(g_pm25LastFrame, hex, sizeof(hex));
+ ADDLOG_INFO(LOG_FEATURE_CMD, "PM25 raw=%i accepted=%i rejected_checksum=%i rejected_range=%i rx_len=%i last_frame=%s",
+ g_pm25LastRaw, g_pm25AcceptedFrames, g_pm25RejectedChecksum, g_pm25RejectedRange, g_pm25RxLen, hex);
+ return CMD_RES_OK;
+}
+
void XiaomiCompact4_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState) {
if (bPreState) return;
hprintf255(request, "Xiaomi Compact 4
");
- hprintf255(request, "Power: %s, Mode: %s, Brightness: %s, PM2.5: %i ug/m3, RPM: %i
",
- g_power ? "on" : "off", XiaomiCompact4_ModeToStr(g_mode),
- XiaomiCompact4_BrightnessToStr(g_brightness), g_lastPm25, g_lastMotorRpm);
+ hprintf255(request, "PM2.5: %i ug/m3, Motor: %i rpm, Output/Target: %i%%/%i%%
",
+ g_lastPm25, g_lastMotorRpm, g_motorCurrentPercent, g_motorTargetPercent);
+ hprintf255(request, "Filter: %i%% health, %i/%i d used, Replace: %s
",
+ XiaomiCompact4_FilterHealth(), XiaomiCompact4_FilterUsageDays(), g_filterLifespanDays, XiaomiCompact4_ReplaceFilter() ? "yes" : "no");
}
static void XiaomiCompact4_SetupChannels(void) {
@@ -764,8 +868,9 @@ static void XiaomiCompact4_SetupChannels(void) {
CHANNEL_SetLabel(XIAOMI_C4_CH_REPLACE_FILTER, "Replace Filter", 1);
CHANNEL_SetLabel(XIAOMI_C4_CH_FAV_SPEED, "Favorite Motor Speed", 1);
CHANNEL_SetLabel(XIAOMI_C4_CH_NIGHT_SPEED, "Night Motor Speed", 1);
- CHANNEL_SetLabel(XIAOMI_C4_CH_P_FACTOR, "Auto Motor Speed x100", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_P_FACTOR, "Auto Motor Speed", 1);
CHANNEL_SetLabel(XIAOMI_C4_CH_FILTER_LIFESPAN, "Filter Lifespan", 1);
+ CHANNEL_SetLabel(XIAOMI_C4_CH_BUZZER, "Buzzer", 1);
CHANNEL_SetType(XIAOMI_C4_CH_POWER, ChType_Toggle);
CHANNEL_SetType(XIAOMI_C4_CH_MODE, ChType_Enum);
CHANNEL_SetType(XIAOMI_C4_CH_BRIGHTNESS, ChType_Enum);
@@ -777,8 +882,9 @@ static void XiaomiCompact4_SetupChannels(void) {
CHANNEL_SetType(XIAOMI_C4_CH_REPLACE_FILTER, ChType_ReadOnly);
CHANNEL_SetType(XIAOMI_C4_CH_FAV_SPEED, ChType_Percent);
CHANNEL_SetType(XIAOMI_C4_CH_NIGHT_SPEED, ChType_Percent);
- CHANNEL_SetType(XIAOMI_C4_CH_P_FACTOR, ChType_ReadOnly);
+ CHANNEL_SetType(XIAOMI_C4_CH_P_FACTOR, ChType_Dimmer1000);
CHANNEL_SetType(XIAOMI_C4_CH_FILTER_LIFESPAN, ChType_ReadOnly);
+ CHANNEL_SetType(XIAOMI_C4_CH_BUZZER, ChType_Toggle);
CMD_ExecuteCommand("SetChannelEnum 1 0:FAV 1:NIGHT 2:AUTO", 0);
CMD_ExecuteCommand("SetChannelEnum 2 0:FULL 1:MID 2:ZERO", 0);
}
@@ -811,6 +917,52 @@ static void XiaomiCompact4_HassClearOldSensor(const char *topic, int ch) {
hass_free_device_info(dev_info);
}
+static void XiaomiCompact4_HassClearOldRelay(const char *topic, int ch, ENTITY_TYPE type) {
+ HassDeviceInfo *dev_info = hass_init_relay_device_info(ch, type, false);
+ if (dev_info == NULL) {
+ return;
+ }
+ MQTT_QueuePublish(topic, dev_info->channel, "", OBK_PUBLISH_FLAG_RETAIN);
+ hass_free_device_info(dev_info);
+}
+
+static void XiaomiCompact4_HassPublishSwitch(const char *topic, int ch, const char *name) {
+ HassDeviceInfo *dev_info = hass_init_relay_device_info(ch, RELAY, false);
+ if (dev_info == NULL) {
+ return;
+ }
+ XiaomiCompact4_HassSetName(dev_info, name);
+ XiaomiCompact4_HassPublish(topic, dev_info);
+}
+
+static void XiaomiCompact4_HassQueueChannelState(int ch, int value) {
+ char channel[16];
+ char state[4];
+ snprintf(channel, sizeof(channel), "%i/get", ch);
+ snprintf(state, sizeof(state), "%i", value ? 1 : 0);
+ MQTT_QueuePublish(CFG_GetMQTTClientId(), channel, state, 0);
+}
+
+static void XiaomiCompact4_HassQueueChannelValue(int ch, int value) {
+ char channel[16];
+ char state[8];
+ snprintf(channel, sizeof(channel), "%i/get", ch);
+ snprintf(state, sizeof(state), "%i", value);
+ MQTT_QueuePublish(CFG_GetMQTTClientId(), channel, state, 0);
+}
+
+static void XiaomiCompact4_HassPublishSelect(const char *topic, int ch, const char *name, const char **options, int optionCount) {
+ char stateTopic[16];
+ char commandTopic[16];
+ snprintf(stateTopic, sizeof(stateTopic), "~/%i/get", ch);
+ snprintf(commandTopic, sizeof(commandTopic), "~/%i/set", ch);
+ HassDeviceInfo *dev_info = hass_createSelectEntityIndexed(stateTopic, commandTopic, optionCount, options, name);
+ if (dev_info == NULL) {
+ return;
+ }
+ XiaomiCompact4_HassPublish(topic, dev_info);
+}
+
static void XiaomiCompact4_HassPublishSensor(const char *topic, int ch, const char *name, const char *unit, const char *deviceClass, const char *icon) {
HassDeviceInfo *dev_info = hass_init_sensor_device_info(CUSTOM_SENSOR, ch, -1, -1, 1);
if (dev_info == NULL) {
@@ -862,10 +1014,22 @@ static void XiaomiCompact4_HassPublishNumber(const char *topic, int ch, const ch
void XiaomiCompact4_OnHassDiscovery(const char *topic) {
HassDeviceInfo *dev_info;
+ static const char *modeOptions[] = { "FAV", "NIGHT", "AUTO" };
+ static const char *brightnessOptions[] = { "FULL", "MID", "ZERO" };
+
+ XiaomiCompact4_HassClearOldRelay(topic, XIAOMI_C4_CH_POWER, LIGHT_ON_OFF);
+ XiaomiCompact4_HassPublishSwitch(topic, XIAOMI_C4_CH_POWER, "Power");
+ XiaomiCompact4_HassPublishSwitch(topic, XIAOMI_C4_CH_BUZZER, "Buzzer");
+ XiaomiCompact4_HassPublishSelect(topic, XIAOMI_C4_CH_MODE, "Mode", modeOptions, 3);
+ XiaomiCompact4_HassPublishSelect(topic, XIAOMI_C4_CH_BRIGHTNESS, "Brightness", brightnessOptions, 3);
+ XiaomiCompact4_HassQueueChannelState(XIAOMI_C4_CH_POWER, g_power);
+ XiaomiCompact4_HassQueueChannelState(XIAOMI_C4_CH_BUZZER, g_buzzer);
+ XiaomiCompact4_HassQueueChannelValue(XIAOMI_C4_CH_MODE, g_mode);
+ XiaomiCompact4_HassQueueChannelValue(XIAOMI_C4_CH_BRIGHTNESS, g_brightness);
XiaomiCompact4_HassPublishSensor(topic, XIAOMI_C4_CH_PM25, "PM2.5", "ug/m3", NULL, "mdi:air-filter");
XiaomiCompact4_HassPublishSensor(topic, XIAOMI_C4_CH_MOTOR_RPM, "Motor Speed", "rpm", NULL, "mdi:fan");
- XiaomiCompact4_HassPublishSensor(topic, XIAOMI_C4_CH_FILTER_USAGE, "Filter Usage", "s", "duration", "mdi:air-filter");
+ XiaomiCompact4_HassPublishSensor(topic, XIAOMI_C4_CH_FILTER_USAGE, "Filter Usage", "d", "duration", "mdi:air-filter");
XiaomiCompact4_HassPublishSensor(topic, XIAOMI_C4_CH_FILTER_HEALTH, "Filter Health", "%", NULL, "mdi:air-filter");
XiaomiCompact4_HassClearOldSensor(topic, XIAOMI_C4_CH_REPLACE_FILTER);
@@ -895,6 +1059,12 @@ bool XiaomiCompact4_ShouldSkipGenericHassDiscovery(int ch) {
if (!g_initialized) {
return false;
}
+ if (ch == XIAOMI_C4_CH_POWER || ch == XIAOMI_C4_CH_BUZZER) {
+ return true;
+ }
+ if (ch == XIAOMI_C4_CH_MODE || ch == XIAOMI_C4_CH_BRIGHTNESS) {
+ return true;
+ }
return ch >= XIAOMI_C4_CH_PM25 && ch <= XIAOMI_C4_CH_FILTER_LIFESPAN;
}
@@ -909,6 +1079,7 @@ void XiaomiCompact4_Init(void) {
CMD_RegisterCommand("XiaomiCompact4_SetNightSpeed", CMD_XiaomiCompact4_SetNightSpeed, NULL);
CMD_RegisterCommand("XiaomiCompact4_SetPFactor", CMD_XiaomiCompact4_SetPFactor, NULL);
CMD_RegisterCommand("XiaomiCompact4_SetFilterLifespan", CMD_XiaomiCompact4_SetFilterLifespan, NULL);
+ CMD_RegisterCommand("XiaomiCompact4_PM25Stats", CMD_XiaomiCompact4_PM25Stats, NULL);
g_power = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_POWER, 0, 0, 1);
g_mode = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_MODE, XIAOMI_C4_MODE_AUTO, XIAOMI_C4_MODE_FAV, XIAOMI_C4_MODE_AUTO);
@@ -919,6 +1090,7 @@ void XiaomiCompact4_Init(void) {
g_pFactorX100 = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_P_FACTOR_X100, 100, 1, 1000);
g_filterLifespanDays = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_FILTER_LIFESPAN, XIAOMI_C4_FILTER_LIFESPAN_DEFAULT_DAYS, XIAOMI_C4_FILTER_LIFESPAN_MIN_DAYS, XIAOMI_C4_FILTER_LIFESPAN_MAX_DAYS);
g_filterUsageSeconds = (uint32_t)XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_FILTER_USAGE, 0, 0, 0x7FFFFFFF);
+ g_buzzer = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_BUZZER, 1, 0, 1);
g_pm25PollCountdown = 1;
g_filterCountdown = 60;
diff --git a/src/httpserver/http_fns.c b/src/httpserver/http_fns.c
index f185eb02cc..3149ca3090 100644
--- a/src/httpserver/http_fns.c
+++ b/src/httpserver/http_fns.c
@@ -2098,6 +2098,12 @@ void doHomeAssistantDiscovery(const char* topic, http_request_t* request) {
if (BIT_CHECK(flagsChannelPublished, i)) {
continue;
}
+#if ENABLE_DRIVER_XIAOMI_COMPACT4
+ if (XiaomiCompact4_ShouldSkipGenericHassDiscovery(i)) {
+ BIT_SET(flagsChannelPublished, i);
+ continue;
+ }
+#endif
if (type == ChType_Dimmer) {
brightness_scale = 100;
dimmer = i;
@@ -2121,6 +2127,12 @@ void doHomeAssistantDiscovery(const char* topic, http_request_t* request) {
if (BIT_CHECK(flagsChannelPublished, i)) {
continue;
}
+#if ENABLE_DRIVER_XIAOMI_COMPACT4
+ if (XiaomiCompact4_ShouldSkipGenericHassDiscovery(i)) {
+ BIT_SET(flagsChannelPublished, i);
+ continue;
+ }
+#endif
if (type == ChType_Toggle) {
toggle = i;
break;
From ecc66719b72d9a1ac73912e5b9d4fd4accb689d0 Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 17:33:20 +0200
Subject: [PATCH 09/19] Fix Xiaomi Compact 4 filter defaults after config clear
---
src/driver/drv_xiaomi_compact4.c | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/driver/drv_xiaomi_compact4.c b/src/driver/drv_xiaomi_compact4.c
index c08a0fe500..c08328117b 100644
--- a/src/driver/drv_xiaomi_compact4.c
+++ b/src/driver/drv_xiaomi_compact4.c
@@ -172,6 +172,13 @@ static void XiaomiCompact4_SaveInt(int index, int value) {
HAL_FlashVars_SaveChannel(index, value);
}
+static void XiaomiCompact4_EnsureFilterLifespan(void) {
+ if (g_filterLifespanDays <= 0) {
+ g_filterLifespanDays = XIAOMI_C4_FILTER_LIFESPAN_DEFAULT_DAYS;
+ XiaomiCompact4_SaveInt(XIAOMI_C4_VAR_FILTER_LIFESPAN, g_filterLifespanDays);
+ }
+}
+
static const char *XiaomiCompact4_ModeToStr(int mode) {
switch (mode) {
case XIAOMI_C4_MODE_FAV:
@@ -802,6 +809,7 @@ static commandResult_t CMD_XiaomiCompact4_SetChildLock(const void *context, cons
}
static commandResult_t CMD_XiaomiCompact4_ResetFilter(const void *context, const char *cmd, const char *args, int cmdFlags) {
+ XiaomiCompact4_EnsureFilterLifespan();
g_filterUsageSeconds = 0;
XiaomiCompact4_ApplyState(1);
return CMD_RES_OK;
@@ -1089,6 +1097,7 @@ void XiaomiCompact4_Init(void) {
g_nightSpeed = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_NIGHT_SPEED, 10, 0, 100);
g_pFactorX100 = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_P_FACTOR_X100, 100, 1, 1000);
g_filterLifespanDays = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_FILTER_LIFESPAN, XIAOMI_C4_FILTER_LIFESPAN_DEFAULT_DAYS, XIAOMI_C4_FILTER_LIFESPAN_MIN_DAYS, XIAOMI_C4_FILTER_LIFESPAN_MAX_DAYS);
+ XiaomiCompact4_EnsureFilterLifespan();
g_filterUsageSeconds = (uint32_t)XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_FILTER_USAGE, 0, 0, 0x7FFFFFFF);
g_buzzer = XiaomiCompact4_LoadInt(XIAOMI_C4_VAR_BUZZER, 1, 0, 1);
g_pm25PollCountdown = 1;
From 457393ce6a9b91398e4ec99a67078a8fb1161faa Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 18:03:46 +0200
Subject: [PATCH 10/19] Tab indent check and added reset filter button
---
src/driver/drv_xiaomi_compact4.c | 23 +++++++++++++++++++----
1 file changed, 19 insertions(+), 4 deletions(-)
diff --git a/src/driver/drv_xiaomi_compact4.c b/src/driver/drv_xiaomi_compact4.c
index c08328117b..7234d4c384 100644
--- a/src/driver/drv_xiaomi_compact4.c
+++ b/src/driver/drv_xiaomi_compact4.c
@@ -148,6 +148,8 @@ static i2c_master_dev_handle_t g_i2cButtonPower;
static i2c_master_dev_handle_t g_i2cButtonBrightness;
static i2c_master_dev_handle_t g_i2cButtonMode;
+static void XiaomiCompact4_ApplyState(int save);
+
static int XiaomiCompact4_ClampInt(int value, int min, int max) {
if (value < min) return min;
if (value > max) return max;
@@ -428,6 +430,12 @@ static int XiaomiCompact4_FilterUsageDays(void) {
return (int)(g_filterUsageSeconds / (24U * 3600U));
}
+static void XiaomiCompact4_ResetFilter(void) {
+ XiaomiCompact4_EnsureFilterLifespan();
+ g_filterUsageSeconds = 0;
+ XiaomiCompact4_ApplyState(1);
+}
+
static void XiaomiCompact4_SetChannels(void) {
g_ignoreChannelChange = 1;
CHANNEL_Set(XIAOMI_C4_CH_POWER, g_power, CHANNEL_SET_FLAG_SILENT);
@@ -809,9 +817,7 @@ static commandResult_t CMD_XiaomiCompact4_SetChildLock(const void *context, cons
}
static commandResult_t CMD_XiaomiCompact4_ResetFilter(const void *context, const char *cmd, const char *args, int cmdFlags) {
- XiaomiCompact4_EnsureFilterLifespan();
- g_filterUsageSeconds = 0;
- XiaomiCompact4_ApplyState(1);
+ XiaomiCompact4_ResetFilter();
return CMD_RES_OK;
}
@@ -856,12 +862,21 @@ static commandResult_t CMD_XiaomiCompact4_PM25Stats(const void *context, const c
}
void XiaomiCompact4_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState) {
- if (bPreState) return;
+ char tmpA[8];
+ if (bPreState) {
+ if (http_getArg(request->url, "x4resetfilter", tmpA, sizeof(tmpA))) {
+ XiaomiCompact4_ResetFilter();
+ }
+ return;
+ }
hprintf255(request, "Xiaomi Compact 4
");
hprintf255(request, "PM2.5: %i ug/m3, Motor: %i rpm, Output/Target: %i%%/%i%%
",
g_lastPm25, g_lastMotorRpm, g_motorCurrentPercent, g_motorTargetPercent);
hprintf255(request, "Filter: %i%% health, %i/%i d used, Replace: %s
",
XiaomiCompact4_FilterHealth(), XiaomiCompact4_FilterUsageDays(), g_filterLifespanDays, XiaomiCompact4_ReplaceFilter() ? "yes" : "no");
+ poststr(request, "");
}
static void XiaomiCompact4_SetupChannels(void) {
From 2d822da30df7c39bf1b95c7f539ab00744aa4923 Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 20:40:38 +0200
Subject: [PATCH 11/19] Restore ESP32 fallback AP startup flow
---
src/hal/espidf/hal_wifi_espidf.c | 43 ++++++++++----------------------
1 file changed, 13 insertions(+), 30 deletions(-)
diff --git a/src/hal/espidf/hal_wifi_espidf.c b/src/hal/espidf/hal_wifi_espidf.c
index 0228feba53..d5f5fc2160 100644
--- a/src/hal/espidf/hal_wifi_espidf.c
+++ b/src/hal/espidf/hal_wifi_espidf.c
@@ -235,16 +235,11 @@ void HAL_DisconnectFromWifi()
int HAL_SetupWiFiOpenAccessPoint(const char* ssid)
{
g_bOpenAccessPointMode = 1;
- ADDLOG_INFO(LOG_FEATURE_MAIN, "WiFi AP init start");
ap_netif = esp_netif_create_default_wifi_ap();
+ sta_netif = esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
cfg.nvs_enable = false;
- esp_err_t err = esp_wifi_init(&cfg);
- ADDLOG_INFO(LOG_FEATURE_MAIN, "WiFi AP esp_wifi_init: %s", esp_err_to_name(err));
- if(err != ESP_OK)
- {
- return 0;
- }
+ esp_wifi_init(&cfg);
esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
@@ -272,30 +267,18 @@ int HAL_SetupWiFiOpenAccessPoint(const char* ssid)
strncpy((char*)wifi_ap_config.ap.ssid, (char*)ssid, 32);
esp_netif_set_hostname(ap_netif, CFG_GetDeviceName());
- err = esp_wifi_set_mode(WIFI_MODE_AP);
- ADDLOG_INFO(LOG_FEATURE_MAIN, "WiFi AP set_mode: %s", esp_err_to_name(err));
- if(err != ESP_OK)
- {
- return 0;
- }
- err = esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config);
- ADDLOG_INFO(LOG_FEATURE_MAIN, "WiFi AP set_config: %s", esp_err_to_name(err));
- if(err != ESP_OK)
- {
- return 0;
- }
- err = esp_wifi_set_max_tx_power(4 * 4);
- ADDLOG_INFO(LOG_FEATURE_MAIN, "WiFi AP set_tx_power: %s", esp_err_to_name(err));
- if(err != ESP_OK)
+ wifi_config_t wifi_sta_config =
{
- return 0;
- }
- err = esp_wifi_start();
- ADDLOG_INFO(LOG_FEATURE_MAIN, "WiFi AP start: %s", esp_err_to_name(err));
- if(err != ESP_OK)
- {
- return 0;
- }
+ .sta = { },
+ };
+
+ strncpy((char*)wifi_sta_config.sta.ssid, (char*)ssid, 32);
+
+ esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config);
+ esp_wifi_set_config(WIFI_IF_STA, &wifi_sta_config);
+ esp_wifi_set_mode(WIFI_MODE_APSTA);
+ esp_wifi_set_max_tx_power(10 * 4);
+ esp_wifi_start();
//esp_netif_set_default_netif(ap_netif);
From 587b7d8543f869e0c31198b01402253c6589e6fc Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 21:39:49 +0200
Subject: [PATCH 12/19] Add Xiaomi Compact 4 ESP32 build variant
---
.github/workflows/workflow.yaml | 4 ++++
Makefile | 16 ++++++++++++----
build_scripts/build_esp32.bat | 13 ++++++++++++-
platforms/ESP-IDF/CMakeLists.txt | 3 +++
platforms/ESP-IDF/sdkconfig.defaults.esp32 | 2 --
.../ESP-IDF/sdkconfig.defaults.xiaomi_compact4 | 2 ++
src/obk_config.h | 3 ++-
7 files changed, 35 insertions(+), 8 deletions(-)
create mode 100644 platforms/ESP-IDF/sdkconfig.defaults.xiaomi_compact4
diff --git a/.github/workflows/workflow.yaml b/.github/workflows/workflow.yaml
index d9c943a97f..b28f108a0c 100644
--- a/.github/workflows/workflow.yaml
+++ b/.github/workflows/workflow.yaml
@@ -354,6 +354,10 @@ jobs:
variant: 4M
version: v5.5.2
sdk: esp-idf
+ - platform: OpenESP32
+ variant: xiaomiCompact4
+ version: v5.5.2
+ sdk: esp-idf
- platform: OpenESP32C2
variant: 4M
version: v5.5.2
diff --git a/Makefile b/Makefile
index b9b4f0f059..18e986d944 100644
--- a/Makefile
+++ b/Makefile
@@ -33,6 +33,9 @@ else ifeq ($(VARIANT),battery)
OBK_VARIANT = 7
else ifeq ($(VARIANT),btproxy)
OBK_VARIANT = 8
+else ifeq ($(VARIANT),xiaomiCompact4)
+OBK_VARIANT = 10
+ESP_FSIZE = 4MB
else ifeq ($(VARIANT),2M)
OBK_VARIANT = 1
ESP_FSIZE = 2MB
@@ -47,6 +50,11 @@ OBK_VARIANT = 0
endif
$(info VARIANT is $(VARIANT), OBK_VARIANT is $(OBK_VARIANT))
+ESP32_BUILD_DIR = platforms/ESP-IDF/build-32
+ifeq ($(OBK_VARIANT), 10)
+ESP32_BUILD_DIR = platforms/ESP-IDF/build-32-xiaomi
+endif
+
#TARGET_PLATFORM ?= bk7231t
#APPS_BUILD_PATH ?= ../bk7231t_os
APPS_BUILD_CMD ?= build.sh
@@ -562,11 +570,11 @@ OpenLN8825: prebuild_OpenLN8825 sdk/OpenLN8825/project/OpenBeken/app
.PHONY: OpenESP32
OpenESP32: prebuild_ESPIDF
- IDF_TARGET="esp32" APP_VERSION=$(APP_VERSION) OBK_VARIANT=$(OBK_VARIANT) cmake platforms/ESP-IDF -B platforms/ESP-IDF/build-32
- IDF_TARGET="esp32" APP_VERSION=$(APP_VERSION) OBK_VARIANT=$(OBK_VARIANT) cmake --build ./platforms/ESP-IDF/build-32 -j $(shell nproc)
+ IDF_TARGET="esp32" APP_VERSION=$(APP_VERSION) OBK_VARIANT=$(OBK_VARIANT) cmake platforms/ESP-IDF -B $(ESP32_BUILD_DIR)
+ IDF_TARGET="esp32" APP_VERSION=$(APP_VERSION) OBK_VARIANT=$(OBK_VARIANT) cmake --build ./$(ESP32_BUILD_DIR) -j $(shell nproc)
mkdir -p output/$(APP_VERSION)
- python3 -m esptool -c esp32 merge_bin -o output/$(APP_VERSION)/OpenESP32_$(APP_VERSION).factory.bin --flash_mode dio --flash_size $(ESP_FSIZE) 0x1000 ./platforms/ESP-IDF/build-32/bootloader/bootloader.bin 0x8000 ./platforms/ESP-IDF/build-32/partition_table/partition-table.bin 0x10000 ./platforms/ESP-IDF/build-32/OpenBeken.bin
- cp ./platforms/ESP-IDF/build-32/OpenBeken.bin output/$(APP_VERSION)/OpenESP32_$(APP_VERSION).img
+ python3 -m esptool -c esp32 merge_bin -o output/$(APP_VERSION)/OpenESP32_$(APP_VERSION).factory.bin --flash_mode dio --flash_size $(ESP_FSIZE) 0x1000 ./$(ESP32_BUILD_DIR)/bootloader/bootloader.bin 0x8000 ./$(ESP32_BUILD_DIR)/partition_table/partition-table.bin 0x10000 ./$(ESP32_BUILD_DIR)/OpenBeken.bin
+ cp ./$(ESP32_BUILD_DIR)/OpenBeken.bin output/$(APP_VERSION)/OpenESP32_$(APP_VERSION).img
.PHONY: OpenESP32C3
OpenESP32C3: prebuild_ESPIDF
diff --git a/build_scripts/build_esp32.bat b/build_scripts/build_esp32.bat
index a258b314d7..25a10fe8f0 100644
--- a/build_scripts/build_esp32.bat
+++ b/build_scripts/build_esp32.bat
@@ -8,13 +8,23 @@ set OBK_VARIANT=2
set ESP_FSIZE=4MB
set TARGET=esp32
set ACTION=build
+set BUILD_VARIANT=4M
:: Check for target argument
if not "%~1"=="" set TARGET=%~1
if not "%~2"=="" set ACTION=%~2
+if not "%~3"=="" set BUILD_VARIANT=%~3
+
+if "%BUILD_VARIANT%"=="xiaomiCompact4" (
+ set OBK_VARIANT=10
+)
if "%TARGET%"=="esp32" (
- set BUILD_DIR=platforms\ESP-IDF\build-32
+ if "%BUILD_VARIANT%"=="xiaomiCompact4" (
+ set BUILD_DIR=platforms\ESP-IDF\build-32-xiaomi
+ ) else (
+ set BUILD_DIR=platforms\ESP-IDF\build-32
+ )
set BOOTLOADER_ADDR=0x1000
) else if "%TARGET%"=="esp32c3" (
set BUILD_DIR=platforms\ESP-IDF\build-c3
@@ -47,6 +57,7 @@ echo Building OpenBeken ESP32 on Windows natively
echo Target: %TARGET%
echo Size: %ESP_FSIZE%
echo Variant (2=tuyaMCU/4MB): %OBK_VARIANT%
+echo Build Variant: %BUILD_VARIANT%
echo Bootloader Addr: %BOOTLOADER_ADDR%
echo Build Directory: %BUILD_DIR%
echo Action: %ACTION%
diff --git a/platforms/ESP-IDF/CMakeLists.txt b/platforms/ESP-IDF/CMakeLists.txt
index 5e3c411706..7b65aa83b1 100644
--- a/platforms/ESP-IDF/CMakeLists.txt
+++ b/platforms/ESP-IDF/CMakeLists.txt
@@ -1,4 +1,7 @@
cmake_minimum_required(VERSION 3.16.0)
+if("$ENV{IDF_TARGET}" STREQUAL "esp32" AND "$ENV{OBK_VARIANT}" STREQUAL "10")
+ set(SDKCONFIG_DEFAULTS "sdkconfig.defaults;sdkconfig.defaults.esp32;sdkconfig.defaults.xiaomi_compact4")
+endif()
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
add_compile_definitions(PLATFORM_ESPIDF=1)
add_compile_definitions(LWIP_NOASSERT=1)
diff --git a/platforms/ESP-IDF/sdkconfig.defaults.esp32 b/platforms/ESP-IDF/sdkconfig.defaults.esp32
index 668cd46d3e..4d2477be48 100644
--- a/platforms/ESP-IDF/sdkconfig.defaults.esp32
+++ b/platforms/ESP-IDF/sdkconfig.defaults.esp32
@@ -1,7 +1,5 @@
CONFIG_PM_ENABLE=y
CONFIG_PM_DFS_INIT_AUTO=y
-CONFIG_FREERTOS_UNICORE=y
-CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_ESPTOOLPY_FLASHFREQ_40M=y
CONFIG_ESPTOOLPY_FLASHFREQ="40m"
diff --git a/platforms/ESP-IDF/sdkconfig.defaults.xiaomi_compact4 b/platforms/ESP-IDF/sdkconfig.defaults.xiaomi_compact4
new file mode 100644
index 0000000000..c44fae8e08
--- /dev/null
+++ b/platforms/ESP-IDF/sdkconfig.defaults.xiaomi_compact4
@@ -0,0 +1,2 @@
+CONFIG_FREERTOS_UNICORE=y
+CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR=y
diff --git a/src/obk_config.h b/src/obk_config.h
index 70dfacb734..4bf2513a0a 100644
--- a/src/obk_config.h
+++ b/src/obk_config.h
@@ -14,6 +14,7 @@
#define OBK_VARIANT_BATTERY 7
#define OBK_VARIANT_BTPROXY 8
#define OBK_VARIANT_XR806_DCDC 9
+#define OBK_VARIANT_XIAOMI_COMPACT4 10
#define OBK_VARIANT_ESP2M 1
#define OBK_VARIANT_ESP4M 2
#define OBK_VARIANT_ESP2M_BERRY 3
@@ -500,7 +501,7 @@
#define ENABLE_DRIVER_SM16703P 1
#define ENABLE_DRIVER_PIXELANIM 1
#define ENABLE_DRIVER_TINYIR_NEC 1
-#if CONFIG_IDF_TARGET_ESP32
+#if CONFIG_IDF_TARGET_ESP32 && (OBK_VARIANT == OBK_VARIANT_XIAOMI_COMPACT4)
#define ENABLE_DRIVER_XIAOMI_COMPACT4 1
#endif
From d1e7808c56dc2606ec861d852f0b9ad38bc4ca09 Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 21:56:22 +0200
Subject: [PATCH 13/19] Fix Xiaomi ESP32 variant partition setup
---
Makefile | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Makefile b/Makefile
index 18e986d944..edf7d3faa0 100644
--- a/Makefile
+++ b/Makefile
@@ -246,6 +246,8 @@ else ifeq ($(OBK_VARIANT), 2)
cp platforms/ESP-IDF/partitions-4mb.csv platforms/ESP-IDF/partitions.csv
else ifeq ($(OBK_VARIANT), 3)
cp platforms/ESP-IDF/partitions-2mb.csv platforms/ESP-IDF/partitions.csv
+else ifeq ($(OBK_VARIANT), 10)
+ cp platforms/ESP-IDF/partitions-4mb.csv platforms/ESP-IDF/partitions.csv
endif
@if [ -e platforms/ESP-IDF/pre_build.sh ]; then \
echo "prebuild found for ESP-IDF"; \
From f5915372ce7edabe4eb6c42247b7ea293642ea44 Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 22:39:43 +0200
Subject: [PATCH 14/19] Fix Xiaomi driver variant gate
---
src/obk_config.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/obk_config.h b/src/obk_config.h
index 4bf2513a0a..c095dcfaa2 100644
--- a/src/obk_config.h
+++ b/src/obk_config.h
@@ -501,7 +501,7 @@
#define ENABLE_DRIVER_SM16703P 1
#define ENABLE_DRIVER_PIXELANIM 1
#define ENABLE_DRIVER_TINYIR_NEC 1
-#if CONFIG_IDF_TARGET_ESP32 && (OBK_VARIANT == OBK_VARIANT_XIAOMI_COMPACT4)
+#if (OBK_VARIANT == OBK_VARIANT_XIAOMI_COMPACT4)
#define ENABLE_DRIVER_XIAOMI_COMPACT4 1
#endif
From 2eb484e77bac3271d83cd2c6d803139489169e8b Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 22:48:19 +0200
Subject: [PATCH 15/19] Limit Xiaomi driver gate to ESP-IDF variant
---
src/obk_config.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/obk_config.h b/src/obk_config.h
index c095dcfaa2..6a635182d1 100644
--- a/src/obk_config.h
+++ b/src/obk_config.h
@@ -501,7 +501,7 @@
#define ENABLE_DRIVER_SM16703P 1
#define ENABLE_DRIVER_PIXELANIM 1
#define ENABLE_DRIVER_TINYIR_NEC 1
-#if (OBK_VARIANT == OBK_VARIANT_XIAOMI_COMPACT4)
+#if PLATFORM_ESPIDF && (OBK_VARIANT == OBK_VARIANT_XIAOMI_COMPACT4)
#define ENABLE_DRIVER_XIAOMI_COMPACT4 1
#endif
From 69ebbc19d4066ce92596eee70f31f4893c397f56 Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Tue, 23 Jun 2026 23:17:26 +0200
Subject: [PATCH 16/19] Publish Xiaomi filter usage in seconds
---
src/driver/drv_xiaomi_compact4.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/driver/drv_xiaomi_compact4.c b/src/driver/drv_xiaomi_compact4.c
index 7234d4c384..773ec9c739 100644
--- a/src/driver/drv_xiaomi_compact4.c
+++ b/src/driver/drv_xiaomi_compact4.c
@@ -446,7 +446,7 @@ static void XiaomiCompact4_SetChannels(void) {
CHANNEL_Set(XIAOMI_C4_CH_PM25, g_lastPm25, CHANNEL_SET_FLAG_SILENT);
}
CHANNEL_Set(XIAOMI_C4_CH_MOTOR_RPM, g_lastMotorRpm, CHANNEL_SET_FLAG_SILENT);
- CHANNEL_Set(XIAOMI_C4_CH_FILTER_USAGE, XiaomiCompact4_FilterUsageDays(), CHANNEL_SET_FLAG_SILENT);
+ CHANNEL_Set(XIAOMI_C4_CH_FILTER_USAGE, (int)g_filterUsageSeconds, CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_FILTER_HEALTH, XiaomiCompact4_FilterHealth(), CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_REPLACE_FILTER, XiaomiCompact4_ReplaceFilter(), CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_FAV_SPEED, g_favSpeed, CHANNEL_SET_FLAG_SILENT);
@@ -1052,7 +1052,7 @@ void XiaomiCompact4_OnHassDiscovery(const char *topic) {
XiaomiCompact4_HassPublishSensor(topic, XIAOMI_C4_CH_PM25, "PM2.5", "ug/m3", NULL, "mdi:air-filter");
XiaomiCompact4_HassPublishSensor(topic, XIAOMI_C4_CH_MOTOR_RPM, "Motor Speed", "rpm", NULL, "mdi:fan");
- XiaomiCompact4_HassPublishSensor(topic, XIAOMI_C4_CH_FILTER_USAGE, "Filter Usage", "d", "duration", "mdi:air-filter");
+ XiaomiCompact4_HassPublishSensor(topic, XIAOMI_C4_CH_FILTER_USAGE, "Filter Usage", "s", "duration", "mdi:air-filter");
XiaomiCompact4_HassPublishSensor(topic, XIAOMI_C4_CH_FILTER_HEALTH, "Filter Health", "%", NULL, "mdi:air-filter");
XiaomiCompact4_HassClearOldSensor(topic, XIAOMI_C4_CH_REPLACE_FILTER);
From e02e9891c1ff4c1d8ff4c1f726ab42505aff5ef9 Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Wed, 24 Jun 2026 19:24:47 +0200
Subject: [PATCH 17/19] WDT implementation
---
src/hal/espidf/hal_generic_espidf.c | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/src/hal/espidf/hal_generic_espidf.c b/src/hal/espidf/hal_generic_espidf.c
index e487b882cf..bddc07742d 100644
--- a/src/hal/espidf/hal_generic_espidf.c
+++ b/src/hal/espidf/hal_generic_espidf.c
@@ -8,8 +8,11 @@
#include "freertos/FreeRTOS.h"
#if PLATFORM_ESPIDF
#include "hal/wdt_hal.h"
+#include "soc/rtc.h"
#endif
+#define OBK_ESPIDF_RTC_WDT_TIMEOUT_MS 10000
+
static int bFlashReady = 0;
void InitFlashIfNeeded()
@@ -37,6 +40,21 @@ void HAL_Delay_us(int delay)
usleep(delay);
}
+void HAL_Configure_WDT()
+{
+#if PLATFORM_ESPIDF
+ wdt_hal_context_t rtc_wdt_ctx = RWDT_HAL_CONTEXT_DEFAULT();
+ uint32_t stage_timeout_ticks = (uint32_t)((uint64_t)OBK_ESPIDF_RTC_WDT_TIMEOUT_MS * rtc_clk_slow_freq_get_hz() / 1000ULL);
+
+ wdt_hal_init(&rtc_wdt_ctx, WDT_RWDT, 0, false);
+ wdt_hal_write_protect_disable(&rtc_wdt_ctx);
+ wdt_hal_config_stage(&rtc_wdt_ctx, WDT_STAGE0, stage_timeout_ticks, WDT_STAGE_ACTION_RESET_SYSTEM);
+ wdt_hal_config_stage(&rtc_wdt_ctx, WDT_STAGE1, stage_timeout_ticks, WDT_STAGE_ACTION_RESET_RTC);
+ wdt_hal_enable(&rtc_wdt_ctx);
+ wdt_hal_write_protect_enable(&rtc_wdt_ctx);
+#endif
+}
+
void HAL_Run_WDT()
{
#if PLATFORM_ESPIDF
From bee61739a0eb64e86605d2193bf9fd2be6adca48 Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Wed, 24 Jun 2026 21:37:25 +0200
Subject: [PATCH 18/19] Tune Xiaomi motor ramp and RPM reporting
---
src/driver/drv_xiaomi_compact4.c | 25 +++++++++++++++++++------
1 file changed, 19 insertions(+), 6 deletions(-)
diff --git a/src/driver/drv_xiaomi_compact4.c b/src/driver/drv_xiaomi_compact4.c
index 773ec9c739..3f3d6ddb72 100644
--- a/src/driver/drv_xiaomi_compact4.c
+++ b/src/driver/drv_xiaomi_compact4.c
@@ -98,8 +98,11 @@
#define XIAOMI_C4_PM25_MAX_VALID 2000
#define XIAOMI_C4_PM25_HIGH_LOG_THRESHOLD 300
#define XIAOMI_C4_PM25_POLL_SECONDS 5
+// Keep normal fan changes smooth in both directions; power-off still stops immediately.
#define XIAOMI_C4_MOTOR_RAMP_UP_PERCENT_PER_SEC 6
-#define XIAOMI_C4_MOTOR_RAMP_DOWN_PERCENT_PER_SEC 12
+#define XIAOMI_C4_MOTOR_RAMP_DOWN_PERCENT_PER_SEC 6
+// RPM is sampled every second for control, but reported less often to avoid MQTT churn.
+#define XIAOMI_C4_MOTOR_RPM_PUBLISH_SECONDS 3
#define XIAOMI_C4_FILTER_LIFESPAN_MIN_DAYS 0
#define XIAOMI_C4_FILTER_LIFESPAN_MAX_DAYS 365
#define XIAOMI_C4_FILTER_LIFESPAN_DEFAULT_DAYS 365
@@ -436,7 +439,7 @@ static void XiaomiCompact4_ResetFilter(void) {
XiaomiCompact4_ApplyState(1);
}
-static void XiaomiCompact4_SetChannels(void) {
+static void XiaomiCompact4_SetChannels(int publishMotorRpm) {
g_ignoreChannelChange = 1;
CHANNEL_Set(XIAOMI_C4_CH_POWER, g_power, CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_MODE, g_mode, CHANNEL_SET_FLAG_SILENT);
@@ -445,7 +448,9 @@ static void XiaomiCompact4_SetChannels(void) {
if (g_lastPm25 >= 0) {
CHANNEL_Set(XIAOMI_C4_CH_PM25, g_lastPm25, CHANNEL_SET_FLAG_SILENT);
}
- CHANNEL_Set(XIAOMI_C4_CH_MOTOR_RPM, g_lastMotorRpm, CHANNEL_SET_FLAG_SILENT);
+ if (publishMotorRpm) {
+ CHANNEL_Set(XIAOMI_C4_CH_MOTOR_RPM, g_lastMotorRpm, CHANNEL_SET_FLAG_SILENT);
+ }
CHANNEL_Set(XIAOMI_C4_CH_FILTER_USAGE, (int)g_filterUsageSeconds, CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_FILTER_HEALTH, XiaomiCompact4_FilterHealth(), CHANNEL_SET_FLAG_SILENT);
CHANNEL_Set(XIAOMI_C4_CH_REPLACE_FILTER, XiaomiCompact4_ReplaceFilter(), CHANNEL_SET_FLAG_SILENT);
@@ -559,7 +564,7 @@ static void XiaomiCompact4_ApplyState(int save) {
HAL_PIN_SetOutputValue(XIAOMI_C4_PIN_MOTOR_EN, g_power ? 1 : 0);
XiaomiCompact4_UpdateHID();
XiaomiCompact4_UpdateMotor();
- XiaomiCompact4_SetChannels();
+ XiaomiCompact4_SetChannels(1);
if (save) {
XiaomiCompact4_SaveState();
}
@@ -684,7 +689,7 @@ static void XiaomiCompact4_ProcessPM25UART(void) {
g_lastPm25 = pm25;
XiaomiCompact4_UpdateHID();
XiaomiCompact4_UpdateMotor();
- XiaomiCompact4_SetChannels();
+ XiaomiCompact4_SetChannels(0);
}
}
@@ -717,6 +722,9 @@ void XiaomiCompact4_RunQuickTick(void) {
}
void XiaomiCompact4_RunEverySecond(void) {
+ static int motorRpmPublishCountdown = 0;
+ int publishMotorRpm = 0;
+
if (!g_initialized) return;
g_pm25PollCountdown--;
if (g_pm25PollCountdown <= 0) {
@@ -733,9 +741,14 @@ void XiaomiCompact4_RunEverySecond(void) {
}
g_lastMotorRpm = (int)((g_tachPulses * 60U) / 15U);
g_tachPulses = 0;
+ motorRpmPublishCountdown--;
+ if (motorRpmPublishCountdown <= 0) {
+ motorRpmPublishCountdown = XIAOMI_C4_MOTOR_RPM_PUBLISH_SECONDS;
+ publishMotorRpm = 1;
+ }
XiaomiCompact4_ApplyMotorRamp();
XiaomiCompact4_UpdateHID();
- XiaomiCompact4_SetChannels();
+ XiaomiCompact4_SetChannels(publishMotorRpm);
}
void XiaomiCompact4_Stop(void) {
From 237bf84652da2ad3e19c570250526a9a87a9189e Mon Sep 17 00:00:00 2001
From: DeDaMrAzR <43572083+DeDaMrAzR@users.noreply.github.com>
Date: Sat, 27 Jun 2026 01:57:37 +0200
Subject: [PATCH 19/19] Gate Xiaomi WDT feed on driver health
Keep the generic WDT feed path unchanged for non-Xiaomi builds, while letting the Xiaomi Compact 4 driver withhold the feed if its QuickTick path stalls.
Also expose Xiaomi-local diagnostics and force the motor failsafe when the driver detects a stale QuickTick path.
---
src/driver/drv_xiaomi_compact4.c | 65 +++++++++++++++++++++++++++++---
src/driver/drv_xiaomi_compact4.h | 1 +
src/user_main.c | 9 +++++
3 files changed, 70 insertions(+), 5 deletions(-)
diff --git a/src/driver/drv_xiaomi_compact4.c b/src/driver/drv_xiaomi_compact4.c
index 3f3d6ddb72..ec305fe209 100644
--- a/src/driver/drv_xiaomi_compact4.c
+++ b/src/driver/drv_xiaomi_compact4.c
@@ -107,6 +107,8 @@
#define XIAOMI_C4_FILTER_LIFESPAN_MAX_DAYS 365
#define XIAOMI_C4_FILTER_LIFESPAN_DEFAULT_DAYS 365
#define XIAOMI_C4_LONG_PRESS_MS 7000
+#define XIAOMI_C4_QUICKTICK_STALE_FAILSAFE_SECONDS 3
+#define XIAOMI_C4_FAILSAFE_REASON_QUICKTICK_STALE 1
static int g_power;
static int g_mode;
@@ -144,6 +146,12 @@ static int g_pm25LastRaw = -1;
static int g_pm25AcceptedFrames;
static int g_pm25RejectedChecksum;
static int g_pm25RejectedRange;
+static int g_failsafeActive;
+static int g_failsafeReason;
+static int g_quickTickStaleSeconds;
+static uint32_t g_diagQuickTicks;
+static uint32_t g_diagEverySeconds;
+static uint32_t g_diagPm25RxBytes;
static i2c_master_bus_handle_t g_i2cBus;
static i2c_master_dev_handle_t g_i2cBrightness;
static i2c_master_dev_handle_t g_i2cStatusLeds;
@@ -304,6 +312,7 @@ static void XiaomiCompact4_UARTReadAvailable(void) {
if (len <= 0) {
return;
}
+ g_diagPm25RxBytes += len;
if (g_pm25RxLen + len > (int)sizeof(g_pm25RxBuf)) {
g_pm25RxLen = 0;
}
@@ -377,12 +386,24 @@ static void XiaomiCompact4_MotorPWMSet(int frequency) {
ledc_update_duty(LEDC_LOW_SPEED_MODE, XIAOMI_C4_MOTOR_LEDC_CHANNEL);
}
+static void XiaomiCompact4_ForceMotorFailsafe(int reason) {
+ if (!g_failsafeActive || g_failsafeReason != reason) {
+ ADDLOG_WARN(LOG_FEATURE_DRV, "XiaomiCompact4 motor failsafe active, reason %i", reason);
+ }
+ g_failsafeActive = 1;
+ g_failsafeReason = reason;
+ g_motorTargetPercent = 0;
+ g_motorCurrentPercent = 0;
+ XiaomiCompact4_MotorPWMSet(0);
+ HAL_PIN_SetOutputValue(XIAOMI_C4_PIN_MOTOR_EN, 0);
+}
+
static int XiaomiCompact4_MotorPercentToFrequency(int percent) {
return (int)((float)percent * 4.12f + 100.0f);
}
static void XiaomiCompact4_ApplyMotorRamp(void) {
- if (!g_power) {
+ if (!g_power || g_failsafeActive) {
g_motorCurrentPercent = 0;
XiaomiCompact4_MotorPWMSet(0);
return;
@@ -539,7 +560,7 @@ static void XiaomiCompact4_UpdateHID(void) {
}
static void XiaomiCompact4_UpdateMotor(void) {
- if (!g_power) {
+ if (!g_power || g_failsafeActive) {
g_motorTargetPercent = 0;
g_motorCurrentPercent = 0;
XiaomiCompact4_MotorPWMSet(0);
@@ -561,7 +582,7 @@ static void XiaomiCompact4_UpdateMotor(void) {
}
static void XiaomiCompact4_ApplyState(int save) {
- HAL_PIN_SetOutputValue(XIAOMI_C4_PIN_MOTOR_EN, g_power ? 1 : 0);
+ HAL_PIN_SetOutputValue(XIAOMI_C4_PIN_MOTOR_EN, (g_power && !g_failsafeActive) ? 1 : 0);
XiaomiCompact4_UpdateHID();
XiaomiCompact4_UpdateMotor();
XiaomiCompact4_SetChannels(1);
@@ -572,6 +593,9 @@ static void XiaomiCompact4_ApplyState(int save) {
static void XiaomiCompact4_SetPower(int value) {
g_power = value ? 1 : 0;
+ g_failsafeActive = 0;
+ g_failsafeReason = 0;
+ g_quickTickStaleSeconds = 0;
XiaomiCompact4_ApplyState(1);
}
@@ -719,13 +743,25 @@ void XiaomiCompact4_RunQuickTick(void) {
XiaomiCompact4_ServiceButton(XIAOMI_C4_PIN_BUTTON_LIGHT, &g_lastButtonLight, &g_lightPressTicks, NULL);
XiaomiCompact4_ServiceButton(XIAOMI_C4_PIN_BUTTON_MODE, &g_lastButtonMode, &g_modePressTicks, &g_modeLongHandled);
XiaomiCompact4_ServiceNotification();
+ g_diagQuickTicks++;
}
void XiaomiCompact4_RunEverySecond(void) {
static int motorRpmPublishCountdown = 0;
+ static uint32_t lastQuickTicks = 0;
int publishMotorRpm = 0;
if (!g_initialized) return;
+ g_diagEverySeconds++;
+ if (g_diagQuickTicks == lastQuickTicks) {
+ g_quickTickStaleSeconds++;
+ if (g_power && g_quickTickStaleSeconds >= XIAOMI_C4_QUICKTICK_STALE_FAILSAFE_SECONDS) {
+ XiaomiCompact4_ForceMotorFailsafe(XIAOMI_C4_FAILSAFE_REASON_QUICKTICK_STALE);
+ }
+ } else {
+ lastQuickTicks = g_diagQuickTicks;
+ g_quickTickStaleSeconds = 0;
+ }
g_pm25PollCountdown--;
if (g_pm25PollCountdown <= 0) {
g_pm25PollCountdown = XIAOMI_C4_PM25_POLL_SECONDS;
@@ -751,6 +787,19 @@ void XiaomiCompact4_RunEverySecond(void) {
XiaomiCompact4_SetChannels(publishMotorRpm);
}
+int XiaomiCompact4_ShouldFeedWDT(void) {
+ if (!g_initialized) {
+ return 1;
+ }
+ if (g_quickTickStaleSeconds <= XIAOMI_C4_QUICKTICK_STALE_FAILSAFE_SECONDS) {
+ return 1;
+ }
+ if ((g_quickTickStaleSeconds % 5) == 4) {
+ ADDLOG_WARN(LOG_FEATURE_DRV, "XiaomiCompact4 QuickTick stalled for %i seconds; withholding WDT feed", g_quickTickStaleSeconds);
+ }
+ return 0;
+}
+
void XiaomiCompact4_Stop(void) {
g_motorTargetPercent = 0;
g_motorCurrentPercent = 0;
@@ -869,8 +918,10 @@ static commandResult_t CMD_XiaomiCompact4_SetFilterLifespan(const void *context,
static commandResult_t CMD_XiaomiCompact4_PM25Stats(const void *context, const char *cmd, const char *args, int cmdFlags) {
char hex[XIAOMI_C4_PM25_FRAME_LEN * 3];
XiaomiCompact4_PM25FrameToHex(g_pm25LastFrame, hex, sizeof(hex));
- ADDLOG_INFO(LOG_FEATURE_CMD, "PM25 raw=%i accepted=%i rejected_checksum=%i rejected_range=%i rx_len=%i last_frame=%s",
- g_pm25LastRaw, g_pm25AcceptedFrames, g_pm25RejectedChecksum, g_pm25RejectedRange, g_pm25RxLen, hex);
+ ADDLOG_INFO(LOG_FEATURE_CMD, "PM25 raw=%i accepted=%i rejected_checksum=%i rejected_range=%i rx_len=%i rx_bytes=%u qt=%u sec=%u qt_stale=%i failsafe=%i reason=%i last_frame=%s",
+ g_pm25LastRaw, g_pm25AcceptedFrames, g_pm25RejectedChecksum, g_pm25RejectedRange, g_pm25RxLen,
+ (unsigned int)g_diagPm25RxBytes, (unsigned int)g_diagQuickTicks, (unsigned int)g_diagEverySeconds,
+ g_quickTickStaleSeconds, g_failsafeActive, g_failsafeReason, hex);
return CMD_RES_OK;
}
@@ -885,6 +936,10 @@ void XiaomiCompact4_AppendInformationToHTTPIndexPage(http_request_t *request, in
hprintf255(request, "Xiaomi Compact 4
");
hprintf255(request, "PM2.5: %i ug/m3, Motor: %i rpm, Output/Target: %i%%/%i%%
",
g_lastPm25, g_lastMotorRpm, g_motorCurrentPercent, g_motorTargetPercent);
+ if (g_failsafeActive) {
+ hprintf255(request, "Motor failsafe: active, reason %i, quick tick stale %i s
",
+ g_failsafeReason, g_quickTickStaleSeconds);
+ }
hprintf255(request, "Filter: %i%% health, %i/%i d used, Replace: %s
",
XiaomiCompact4_FilterHealth(), XiaomiCompact4_FilterUsageDays(), g_filterLifespanDays, XiaomiCompact4_ReplaceFilter() ? "yes" : "no");
poststr(request, "