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, "
"); + poststr(request, ""); + 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, "
"); diff --git a/src/driver/drv_xiaomi_compact4.h b/src/driver/drv_xiaomi_compact4.h index ec6569f536..abdaba0edf 100644 --- a/src/driver/drv_xiaomi_compact4.h +++ b/src/driver/drv_xiaomi_compact4.h @@ -5,6 +5,7 @@ void XiaomiCompact4_Init(void); void XiaomiCompact4_RunEverySecond(void); void XiaomiCompact4_RunQuickTick(void); +int XiaomiCompact4_ShouldFeedWDT(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/user_main.c b/src/user_main.c index 18ac50a96b..0d0b7321ff 100644 --- a/src/user_main.c +++ b/src/user_main.c @@ -45,6 +45,9 @@ uint8_t g_log2lfs; #include "driver/drv_mdns.h" #include "driver/drv_ssdp.h" #include "driver/drv_uart.h" +#if ENABLE_DRIVER_XIAOMI_COMPACT4 +#include "driver/drv_xiaomi_compact4.h" +#endif #if PLATFORM_BEKEN #include @@ -1097,7 +1100,13 @@ void Main_OnEverySecond() } } #endif +#if ENABLE_DRIVER_XIAOMI_COMPACT4 + if (bSafeMode || XiaomiCompact4_ShouldFeedWDT()) { + HAL_Run_WDT(); + } +#else HAL_Run_WDT(); +#endif // force it to sleep... we MUST have some idle task processing // else task memory doesn't get freed rtos_delay_milliseconds(1);