/* * Furnace call relays. See relay.h for the pin assignment and why heat is on * IO20 rather than IO19. */ #include "driver/gpio.h" #include "esp_check.h" #include "esp_log.h" #include "relay.h" static const char *TAG = "relay"; #define RELAY_HEAT_GPIO 20 /* white wire on the UART1-OUT plug */ #define RELAY_FAN_GPIO 19 /* yellow wire */ /* * Standard opto-isolated relay boards are active low: pulling IN to 0V lights * the optocoupler and energizes the coil. Set to 0 if you fit an active-high * module -- and re-check the boot state below, because the safe direction * flips with it. */ #define RELAY_ACTIVE_LOW 1 #if RELAY_ACTIVE_LOW #define RELAY_LEVEL_ON 0 #define RELAY_LEVEL_OFF 1 #else #define RELAY_LEVEL_ON 1 #define RELAY_LEVEL_OFF 0 #endif static const int relay_gpio[RELAY_COUNT] = { [RELAY_HEAT] = RELAY_HEAT_GPIO, [RELAY_FAN] = RELAY_FAN_GPIO, }; static bool relay_state[RELAY_COUNT]; esp_err_t relay_init(void) { for (int i = 0; i < RELAY_COUNT; i++) { const int pin = relay_gpio[i]; /* * IO19/IO20 are the S3's native USB D-/D+ and are also wired to the * onboard I2S mic. gpio_reset_pin() detaches whatever peripheral * currently owns the pad; the DIP switches must be in wireless-module * mode (S1=0 S0=1) so the mic is not driving them, and * CONFIG_ESP_PHY_ENABLE_USB / CONFIG_USJ_ENABLE_USB_SERIAL_JTAG are * off so the USB PHY is not either. */ gpio_reset_pin(pin); /* * Drive the de-energized level BEFORE the pad becomes an output. * Configuring the direction first would briefly present whatever was * in the output register -- and on the heat channel that is a burst * of gas. */ ESP_RETURN_ON_ERROR(gpio_set_level(pin, RELAY_LEVEL_OFF), TAG, "gpio_set_level(%d) failed", pin); const gpio_config_t cfg = { .pin_bit_mask = 1ULL << pin, /* INPUT_OUTPUT so relay_get() can read the actual pad rather than * the output latch -- a readback that disagrees means something * else is driving the line. */ .mode = GPIO_MODE_INPUT_OUTPUT, #if RELAY_ACTIVE_LOW /* Holds the de-energized level through reset and reflash, before * any of our code runs. */ .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, #else .pull_up_en = GPIO_PULLUP_DISABLE, .pull_down_en = GPIO_PULLDOWN_ENABLE, #endif .intr_type = GPIO_INTR_DISABLE, }; ESP_RETURN_ON_ERROR(gpio_config(&cfg), TAG, "gpio_config(%d) failed", pin); ESP_RETURN_ON_ERROR(gpio_set_level(pin, RELAY_LEVEL_OFF), TAG, "gpio_set_level(%d) failed", pin); relay_state[i] = false; } ESP_LOGI(TAG, "Relays idle. heat=IO%d fan=IO%d, active %s (idle reads %s)", RELAY_HEAT_GPIO, RELAY_FAN_GPIO, RELAY_ACTIVE_LOW ? "low" : "high", RELAY_ACTIVE_LOW ? "3.3V" : "0V"); return ESP_OK; } void relay_set(relay_id_t id, bool energized) { if (id < 0 || id >= RELAY_COUNT) { return; } if (relay_state[id] == energized) { return; } relay_state[id] = energized; gpio_set_level(relay_gpio[id], energized ? RELAY_LEVEL_ON : RELAY_LEVEL_OFF); /* * Logged on every change, deliberately. With no relay board fitted yet * this log plus a multimeter on the plug is the whole test rig, and once * one is fitted it is the record of what the firmware asked the furnace * to do. */ ESP_LOGI(TAG, "%s -> %s (IO%d now %s)", relay_name(id), energized ? "ON" : "OFF", relay_gpio[id], (energized ? RELAY_LEVEL_ON : RELAY_LEVEL_OFF) ? "3.3V" : "0V"); } bool relay_get(relay_id_t id) { if (id < 0 || id >= RELAY_COUNT) { return false; } return relay_state[id]; } const char *relay_name(relay_id_t id) { switch (id) { case RELAY_HEAT: return "heat(W)"; case RELAY_FAN: return "fan(G)"; default: return "?"; } }