#include "pcf8574.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "i2c_bus.h" static const char *TAG = "pcf8574"; static i2c_master_dev_handle_t s_dev; static SemaphoreHandle_t s_lock; static bool s_ready; /* * Power-on state: all pins released high. Critically this is also * relay-off for the active-low module on P1, so the shadow is safe from the * first instruction, before init has even run. */ static uint8_t s_shadow = 0xFF; #define LOCK() xSemaphoreTakeRecursive(s_lock, portMAX_DELAY) #define UNLOCK() xSemaphoreGiveRecursive(s_lock) static esp_err_t flush_locked(void) { return i2c_master_transmit(s_dev, &s_shadow, 1, 100); } esp_err_t pcf8574_init(void) { if (s_ready) { return ESP_OK; } if (!s_lock) { s_lock = xSemaphoreCreateRecursiveMutex(); if (!s_lock) { return ESP_ERR_NO_MEM; } } esp_err_t err = i2c_bus_add_device(PCF8574_I2C_ADDR, &s_dev); if (err != ESP_OK) { ESP_LOGE(TAG, "add device 0x%02X failed: %s", PCF8574_I2C_ADDR, esp_err_to_name(err)); return err; } /* Push the known-safe shadow out before anything else can ask for a pin * change. If the ESP32 rebooted while the relay was closed, the expander * still has that old latch — this is the write that clears it. */ LOCK(); err = flush_locked(); UNLOCK(); if (err != ESP_OK) { ESP_LOGE(TAG, "not responding at 0x%02X: %s", PCF8574_I2C_ADDR, esp_err_to_name(err)); return err; } s_ready = true; ESP_LOGI(TAG, "ready at 0x%02X, port=0x%02X", PCF8574_I2C_ADDR, s_shadow); return ESP_OK; } bool pcf8574_is_ready(void) { return s_ready; } esp_err_t pcf8574_write(uint8_t pin, bool level) { if (pin > 7) { return ESP_ERR_INVALID_ARG; } if (!s_lock) { return ESP_ERR_INVALID_STATE; } LOCK(); uint8_t before = s_shadow; if (level) { s_shadow |= (uint8_t)(1u << pin); } else { s_shadow &= (uint8_t)~(1u << pin); } esp_err_t err = flush_locked(); if (err != ESP_OK) { /* Roll the shadow back so it keeps describing the chip rather than * our intent — the relay layer reasons about it. */ s_shadow = before; } UNLOCK(); return err; } esp_err_t pcf8574_read(uint8_t pin, bool *level) { if (pin > 7 || !level) { return ESP_ERR_INVALID_ARG; } if (!s_lock) { return ESP_ERR_INVALID_STATE; } uint8_t port = 0; LOCK(); esp_err_t err = i2c_master_receive(s_dev, &port, 1, 100); UNLOCK(); if (err == ESP_OK) { *level = (port & (1u << pin)) != 0; } return err; } esp_err_t pcf8574_write_port(uint8_t value) { if (!s_lock) { return ESP_ERR_INVALID_STATE; } LOCK(); uint8_t before = s_shadow; s_shadow = value; esp_err_t err = flush_locked(); if (err != ESP_OK) { s_shadow = before; } UNLOCK(); return err; } uint8_t pcf8574_shadow(void) { return s_shadow; }