I know there are already a bunch of topics about garage door controllers, but this is my first post and I’d like to finally contribute to this community. Additionally, I am definitely a beginner here, so maybe this could function as a guide for the other noobies like me .
The goal was to integrate an old Chamberlain Liftmaster 2575 into my OpenHAB system. From briefly looking through other topics, it seemed the common approach to IoT-enizing garage doors would be to have a simple relay as the activation button and to have reed switches to detect the Open/Close state. While I adopted this into my design, I also wanted to utilize the Chamberlain Safety Sensors that were already installed in my garage, particularly because the GarageDoorOpener accessory in the Apple HomeKit Add-on supports Obstruction Status. To do this, it actually wasn’t as straightforward as I initially presumed. When tripped, the sensors emit a static HIGH signal at around 6-7 Volts. When the break-beam is uninterrupted – no obstruction – the signal would be a PWM signal with a period of roughly 7 ms and a duty cycle of around 90%. This video helped.
I selected an ESP8266 DevKit (NodeMCU) as the device’s controller. I also had a 3.3 Volt Relay board lying around that I would use to activate the door by shorting the Door Control Terminals together. To get the ~6V Safety Sensor’s signal down to 3v3 logic level, I simply used a logic-level buffer. I finally included an RGB led for indication and debugging. Ideally I would have had the entire device into one small package, but I already had the 3.3V relay board, and to my knowledge, most mechanical relay’s don’t come in uniform through-hole layouts. Thus, my final device was the relay board connected to a perfboard that I soldered everything else up to, which is right here:
To program the MCU, I used the ESP8266 RTOS SDK which is based on FreeRTOS. I based my design off of the SDK’s MQTT example, which already handles Wifi configuration and lays out MQTT implementation. Here is my program:
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "freertos/queue.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "protocol_examples_common.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "lwip/sockets.h"
#include "lwip/dns.h"
#include "lwip/netdb.h"
// MQTT REFS
#include "mqtt_client.h"
// GPIO REFS
#include "esp8266/gpio_register.h"
#include "esp8266/pin_mux_register.h"
#include "driver/gpio.h"
#include "driver/pwm.h"
// TIMER REFS
#include "esp_timer.h"
// TYPEDEFS: -------------------------------------------------------------------
typedef struct {
int red;
int green;
int blue;
} RGB_t;
// -----------------------------------------------------------------------------
// GPIO PARAMS: ----------------------------------------------------------------
#define GPIO_INPUT_SENSOR_OPEN 2
#define GPIO_INPUT_SENSOR_CLOSED 0
#define GPIO_INPUT_SENSOR_OBSTRUCTION 5
#define GPIO_INPUT_PIN_SEL ((1ULL<<GPIO_INPUT_SENSOR_OPEN) | \
(1ULL<<GPIO_INPUT_SENSOR_CLOSED) | \
(1ULL<<GPIO_INPUT_SENSOR_OBSTRUCTION))
#define GPIO_OUTPUT_DOORBUTTON 13
#define GPIO_OUTPUT_PIN_SEL ((1ULL<<GPIO_OUTPUT_DOORBUTTON))
#define PWM_OUTPUT_LED_BLUE 4
#define PWM_OUTPUT_LED_RED 14
#define PWM_OUTPUT_LED_GREEN 12
// PWM period 1000us(1KHz), same as depth
#define PWM_PERIOD (1000)
const uint32_t pin_num[3] = {
PWM_OUTPUT_LED_RED,
PWM_OUTPUT_LED_GREEN,
PWM_OUTPUT_LED_BLUE
};
// duty cycles table, real_duty = duties[x]/PERIOD
uint32_t duties[3] = { 500, 500, 500 };
// phase table, delay = (phase[x]/360)*PERIOD
int16_t phase[3] = { 0, 0, 0 };
// -----------------------------------------------------------------------------
static const char *TAG = "GarageDoor";
// GLOBAL HANDLES --------------------------------------------------------------
esp_timer_handle_t pwm_read_timer = NULL;
esp_mqtt_client_handle_t gd_client = NULL;
TaskHandle_t xObstacleDetectedTask = NULL;
TaskHandle_t xSendObstructionStatusTask = NULL;
TaskHandle_t xSendDoorStatusTask = NULL;
TaskHandle_t xActivateDoorTask = NULL;
TaskHandle_t xSetRGBLEDTask = NULL;
TaskHandle_t xbegin_the_appTask = NULL;
TaskHandle_t xCheckStatusTask = NULL;
TaskHandle_t xRestartDeviceTask = NULL;
QueueHandle_t xRGBQueue = NULL;
QueueHandle_t xCheckStatusQueue = NULL; // full of topics to check
// -----------------------------------------------------------------------------
// TASK HANDLERS: --------------------------------------------------------------
/* A task that blocks waiting to be notified that the peripheral needs
servicing. */
static uint64_t prev_time = 0; // microseconds
static uint64_t pwm_value = 0; // microseconds
static uint8_t is_tripped = 1; // boolean
static void vObstacleDetectedTask( void *pvParameters )
{
const TickType_t xBlockTime = pdMS_TO_TICKS( 600000 ); // 10 minutes
uint32_t ulNotifiedValue;
for( ;; )
{
/* Block to wait for a notification. Here the RTOS task notification
is being used as a counting semaphore. The task's notification value
is incremented each time the ISR calls vTaskNotifyGiveFromISR(), and
decremented each time the RTOS task calls ulTaskNotifyTake() - so in
effect holds a count of the number of outstanding interrupts. The
first parameter is set to pdTRUE, clearing the task's notification
value to 0, meaning each outstanding outstanding deferred interrupt
event must be processed before ulTaskNotifyTake() is called again. */
ulNotifiedValue = ulTaskNotifyTake( pdTRUE,
xBlockTime );
if( ulNotifiedValue > 0 )
{
/* Perform any processing necessitated by the interrupt. */
if (is_tripped == 0) {
is_tripped = 1;
xTaskNotifyGive( xSendObstructionStatusTask );
}
/* ESP_LOGI(TAG,"DEBUG: Obstacle deteced.\t Number of times triggered: %i\n", */
/* ulNotifiedValue); */
}
else
{
/* Did not receive a notification within the expected time. */
ESP_LOGI(TAG,"NOTICE: Obstacle Detection task timeout\n");
}
// ESP_LOGI(TAG,"DEBUG: pwm_value: %i\n", (int)pwm_value);
}
}
/* Simple task to send out the obstruction status */
static void vSendObstructionStatusTask( void *pvParameters )
{
const TickType_t xBlockTime = pdMS_TO_TICKS( 600000 ); // 10 minutes
uint32_t ulNotifiedValue;
int msg_id;
for( ;; )
{
/* Block to wait for a notification. Here the RTOS task notification
is being used as a counting semaphore. The task's notification value
is incremented each time the ISR calls vTaskNotifyGiveFromISR(), and
decremented each time the RTOS task calls ulTaskNotifyTake() - so in
effect holds a count of the number of outstanding interrupts. The
first parameter is set to pdTRUE, clearing the task's notification
value to 0, meaning each outstanding outstanding deferred interrupt
event must be processed before ulTaskNotifyTake() is called again. */
ulNotifiedValue = ulTaskNotifyTake( pdTRUE,
xBlockTime );
if( ulNotifiedValue > 0 )
{
/* Perform any processing necessitated by the interrupt. */
}
else
{
/* Did not receive a notification within the expected time. */
ESP_LOGI(TAG,"NOTICE: 10 minute update for obstruction status\n");
}
if (is_tripped == 1) {
msg_id = esp_mqtt_client_publish(gd_client, "stat/garagedoor/OBSTRUCTION", "YES", 3, 0, 0);
}
else { // not tripped
msg_id = esp_mqtt_client_publish(gd_client, "stat/garagedoor/OBSTRUCTION", "NO", 2, 0, 0);
}
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
}
}
static void vSendDoorStatusTask( void *pvParameters )
{
const TickType_t xBlockTime = pdMS_TO_TICKS( 600000 ); // 10 minutes
uint32_t ulNotifiedValue;
int msg_id;
for( ;; )
{
/* Block to wait for a notification. Here the RTOS task notification
is being used as a counting semaphore. The task's notification value
is incremented each time the ISR calls vTaskNotifyGiveFromISR(), and
decremented each time the RTOS task calls ulTaskNotifyTake() - so in
effect holds a count of the number of outstanding interrupts. The
first parameter is set to pdTRUE, clearing the task's notification
value to 0, meaning each outstanding outstanding deferred interrupt
event must be processed before ulTaskNotifyTake() is called again. */
ulNotifiedValue = ulTaskNotifyTake( pdTRUE,
xBlockTime );
if( ulNotifiedValue > 0 )
{
/* Perform any processing necessitated by the interrupt. */
}
else
{
/* Did not receive a notification within the expected time. */
ESP_LOGI(TAG,"NOTICE: 10 minute update for door status\n");
}
vTaskDelay(pdMS_TO_TICKS(500));
if (gpio_get_level(GPIO_INPUT_SENSOR_OPEN) == 0) {
msg_id = esp_mqtt_client_publish(gd_client, "stat/garagedoor/STATE", "OPEN", 4, 2, 0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
}
else if (gpio_get_level(GPIO_INPUT_SENSOR_CLOSED) == 0) {
msg_id = esp_mqtt_client_publish(gd_client, "stat/garagedoor/STATE", "CLOSED", 6, 2, 0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
}
else {
msg_id = esp_mqtt_client_publish(gd_client, "stat/garagedoor/STATE", "MOVING", 6, 2, 0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
}
}
}
static void vActivateDoorTask( void *pvParameters )
{
uint32_t ulNotifiedValue;
int msg_id;
for( ;; )
{
/* Block to wait for a notification. Here the RTOS task notification
is being used as a counting semaphore. The task's notification value
is incremented each time the ISR calls vTaskNotifyGiveFromISR(), and
decremented each time the RTOS task calls ulTaskNotifyTake() - so in
effect holds a count of the number of outstanding interrupts. The
first parameter is set to pdTRUE, clearing the task's notification
value to 0, meaning each outstanding outstanding deferred interrupt
event must be processed before ulTaskNotifyTake() is called again. */
ulNotifiedValue = ulTaskNotifyTake( pdTRUE,
portMAX_DELAY );
if( ulNotifiedValue > 0 )
{
/* Perform any processing necessitated by the interrupt. */
gpio_set_level(GPIO_OUTPUT_DOORBUTTON, 1);
vTaskDelay(500 / portTICK_RATE_MS);
gpio_set_level(GPIO_OUTPUT_DOORBUTTON, 0);
msg_id = esp_mqtt_client_publish(gd_client, "stat/garagedoor/BUTTON", "UNPRESSED", 9, 2, 0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
}
else
{
/* Did not receive a notification within the expected time. */
}
}
}
static void vSetRGBLEDTask( void *pvParameters )
{
/* Declare the variable that will hold the values received from the queue. */
RGB_t lReceivedValue = {
.red = 0,
.green = 0,
.blue = 0,
};
char RGB_str[12];
BaseType_t xStatus;
const TickType_t xTicksToWait = portMAX_DELAY;
/* This task is also defined within an infinite loop. */
for( ;; ) {
/* This call should always find the queue empty because this task will
immediately remove any data that is written to the queue. */
if( uxQueueMessagesWaiting( xRGBQueue ) != 0 ) {
ESP_LOGI(TAG, "ERROR: RGB Queue should have been empty!\n" );
}
/* Receive data from the queue.
The first parameter is the queue from which data is to be received. The
queue is created before the scheduler is started, and therefore before
this task runs for the first time.
The second parameter is the buffer into which the received data will be
placed. In this case the buffer is simply the address of a variable that
has the required size to hold the received data.
The last parameter is the block time – the maximum amount of time that
the task will remain in the Blocked state to wait for data to be
available should the queue already be empty. */
xStatus = xQueueReceive( xRGBQueue, &lReceivedValue, xTicksToWait );
if( xStatus == pdPASS ) {
/* Data was successfully received from the queue, print out the received
value. */
duties[0] = ((float)lReceivedValue.red / 255) * PWM_PERIOD;
duties[1] = ((float)lReceivedValue.green / 255) * PWM_PERIOD;
duties[2] = ((float)lReceivedValue.blue / 255) * PWM_PERIOD;
pwm_set_duties(duties);
pwm_start();
sprintf(RGB_str, "%03i,%03i,%03i", lReceivedValue.red, lReceivedValue.green, lReceivedValue.blue);
int msg_id = esp_mqtt_client_publish(gd_client, "stat/garagedoor/COLOR", RGB_str, 11, 1, 0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
}
else {
/* Data was not received from the queue even after waiting for 100ms.
This must be an error as the sending tasks are free running and will
be continuously writing to the queue. */
ESP_LOGI(TAG, "Could not receive from the RGB Queue.\n" );
}
}
}
static void vCheckStatusTask( void *pvParameters )
{
/* Declare the variable that will hold the values received from the queue. */
/* -- TOPIC OPTIONS: --
ALL - all topics
OBS - obstruction
DOR - door
*/
char lReceivedValue[4] = "ALL\0";
BaseType_t xStatus;
const TickType_t xTicksToWait = portMAX_DELAY; // 10 minutes
/* This task is also defined within an infinite loop. */
for( ;; ) {
/* This call should always find the queue empty because this task will
immediately remove any data that is written to the queue. */
if( uxQueueMessagesWaiting( xCheckStatusQueue ) != 0 ) {
ESP_LOGI(TAG, "ERROR: Check Status Queue should have been empty!\n" );
}
/* Receive data from the queue.
The first parameter is the queue from which data is to be received. The
queue is created before the scheduler is started, and therefore before
this task runs for the first time.
The second parameter is the buffer into which the received data will be
placed. In this case the buffer is simply the address of a variable that
has the required size to hold the received data.
The last parameter is the block time – the maximum amount of time that
the task will remain in the Blocked state to wait for data to be
available should the queue already be empty. */
xStatus = xQueueReceive( xCheckStatusQueue, &lReceivedValue, xTicksToWait );
if( xStatus == pdPASS ) {
/* Data was successfully received from the queue, print out the received
value. */
if (strncmp(lReceivedValue, "OBS", 3) == 0) {
xTaskNotifyGive( xSendObstructionStatusTask );
}
else if (strncmp(lReceivedValue, "DOR", 3) == 0) {
xTaskNotifyGive( xSendDoorStatusTask );
}
else if (strncmp(lReceivedValue, "ALL", 3) == 0) {
xTaskNotifyGive( xSendObstructionStatusTask );
xTaskNotifyGive( xSendDoorStatusTask );
}
else {
ESP_LOGI(TAG, "ERROR: Invalid topic sent to CHECKSTATUS\n" );
}
}
else {
/* Data was not received from the queue even after waiting for 100ms.
This must be an error as the sending tasks are free running and will
be continuously writing to the queue. */
ESP_LOGI(TAG, "Could not receive from the Check Status Queue.\n" );
}
}
}
static void vRestartDeviceTask( void *pvParameters )
{
uint32_t ulNotifiedValue;
int msg_id;
for( ;; )
{
/* Block to wait for a notification. Here the RTOS task notification
is being used as a counting semaphore. The task's notification value
is incremented each time the ISR calls vTaskNotifyGiveFromISR(), and
decremented each time the RTOS task calls ulTaskNotifyTake() - so in
effect holds a count of the number of outstanding interrupts. The
first parameter is set to pdTRUE, clearing the task's notification
value to 0, meaning each outstanding outstanding deferred interrupt
event must be processed before ulTaskNotifyTake() is called again. */
ulNotifiedValue = ulTaskNotifyTake( pdTRUE,
portMAX_DELAY );
if( ulNotifiedValue > 0 )
{
/* Perform any processing necessitated by the interrupt. */
msg_id = esp_mqtt_client_publish(gd_client, "stat/garagedoor/RESTART", "UNPRESSED", 9, 2, 0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
esp_restart();
}
else
{
/* Did not receive a notification within the expected time. */
}
}
}
// -----------------------------------------------------------------------------
// TIMER PARAMS: ---------------------------------------------------------------
const uint64_t PWM_SIG_TIMEOUT = 20000; // microseconds ?
// for some reason, it only works with increments of 10000
const uint64_t PWM_SIG_UBOUND = 6700; // microseconds
const uint64_t PWM_SIG_LBOUND = 5900; // microseconds
// generic one-time event callback
static void event_callback(void *p) {
TaskHandle_t xHandlingTask = (TaskHandle_t)p;
BaseType_t xHigherPriorityTaskWoken;
// IF HERE, TIMER HAS EXPIRED
/* xHigherPriorityTaskWoken must be initialised to pdFALSE.
If calling vTaskNotifyGiveFromISR() unblocks the handling
task, and the priority of the handling task is higher than
the priority of the currently running task, then
xHigherPriorityTaskWoken will be automatically set to pdTRUE. */
xHigherPriorityTaskWoken = pdFALSE;
/* Unblock the handling task so the task can perform any processing
necessitated by the interrupt. xHandlingTask is the task's handle, which
was obtained when the task was created. vTaskNotifyGiveFromISR() also
increments the receiving task's notification value. */
vTaskNotifyGiveFromISR( xHandlingTask, &xHigherPriorityTaskWoken );
/* Force a context switch if xHigherPriorityTaskWoken is now set to pdTRUE.
The macro used to do this is dependent on the port and may be called
portEND_SWITCHING_ISR. */
portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
}
// -----------------------------------------------------------------------------
// ISR HANDLERS: ---------------------------------------------------------------
static void PWMHandler() {
if (gpio_get_level(GPIO_INPUT_SENSOR_OBSTRUCTION) == 1) { // falling edge
// start the timer
esp_timer_start_once(pwm_read_timer, PWM_SIG_TIMEOUT);
//ESP_ERROR_CHECK(esp_timer_start_once(pwm_read_timer, PWM_SIG_TIMEOUT));
prev_time = esp_timer_get_time(); // TIMER MICROS
}
else { // rising edge
// stop the timer
// the callback should not happen
esp_timer_stop(pwm_read_timer);
//ESP_ERROR_CHECK(esp_timer_stop(pwm_read_timer));
pwm_value = esp_timer_get_time() - prev_time; // obtain the PWM value
// test for valid pwm_value -- only need to test LOWER BOUND
if (pwm_value < PWM_SIG_LBOUND || pwm_value > PWM_SIG_UBOUND) {
// INVALID SENSOR SIG
event_callback(xObstacleDetectedTask); // function that same as if timer expired
}
else {
if (is_tripped == 1) {
is_tripped = 0;
event_callback(xSendObstructionStatusTask);
}
}
}
}
static void state_isr_handler(void *arg) {
event_callback(xSendDoorStatusTask);
}
// -----------------------------------------------------------------------------
// function to run to begin the app once all necessary backend init is finished
static void vbegin_the_appTask( void *pvParameters ) {
uint32_t ulNotifiedValue;
for( ;; )
{
/* Block to wait for a notification. Here the RTOS task notification
is being used as a counting semaphore. The task's notification value
is incremented each time the ISR calls vTaskNotifyGiveFromISR(), and
decremented each time the RTOS task calls ulTaskNotifyTake() - so in
effect holds a count of the number of outstanding interrupts. The
first parameter is set to pdTRUE, clearing the task's notification
value to 0, meaning each outstanding outstanding deferred interrupt
event must be processed before ulTaskNotifyTake() is called again. */
ulNotifiedValue = ulTaskNotifyTake( pdTRUE,
portMAX_DELAY );
if( ulNotifiedValue > 0 )
{
/* Perform any processing necessitated by the interrupt. */
// Task Creation -------------------------------------------------------------
xTaskCreate(
vObstacleDetectedTask, /* Function that implements the task. */
"ObstacleDetectedTask", /* Text name for the task. */
2048, /* Stack size in words, not bytes. */
NULL, /* Parameter passed into the task. */
1, /* Priority at which the task is created. */
&xObstacleDetectedTask ); /* Used to pass out the created task's handle. */
xTaskCreate(
vSendObstructionStatusTask, /* Function that implements the task. */
"SendObstructionStatusTask", /* Text name for the task. */
2048, /* Stack size in words, not bytes. */
NULL, /* Parameter passed into the task. */
2, /* Priority at which the task is created. */
&xSendObstructionStatusTask ); /* Used to pass out the created task's handle. */
xTaskCreate(
vSendDoorStatusTask, /* Function that implements the task. */
"SendDoorStatusTask", /* Text name for the task. */
2048, /* Stack size in words, not bytes. */
NULL, /* Parameter passed into the task. */
4, /* Priority at which the task is created. */
&xSendDoorStatusTask ); /* Used to pass out the created task's handle. */
xTaskCreate(
vActivateDoorTask, /* Function that implements the task. */
"ActivateDoorTask", /* Text name for the task. */
2048, /* Stack size in words, not bytes. */
NULL, /* Parameter passed into the task. */
3, /* Priority at which the task is created. */
&xActivateDoorTask ); /* Used to pass out the created task's handle. */
xRGBQueue = xQueueCreate( 5, sizeof(RGB_t) );
if ( xRGBQueue != NULL ) {
xTaskCreate(
vSetRGBLEDTask, /* Function that implements the task. */
"SetRGBLEDTask", /* Text name for the task. */
2048, /* Stack size in words, not bytes. */
NULL, /* Parameter passed into the task. */
2, /* Priority at which the task is created. */
&xSetRGBLEDTask ); /* Used to pass out the created task's handle. */
}
else {
ESP_LOGI(TAG,"ERROR: RGB Queue could not be created\n");
}
xCheckStatusQueue = xQueueCreate( 6, sizeof(char[4]) );
if ( xCheckStatusQueue != NULL ) {
xTaskCreate(
vCheckStatusTask, /* Function that implements the task. */
"CheckStatusTask", /* Text name for the task. */
2048, /* Stack size in words, not bytes. */
NULL, /* Parameter passed into the task. */
1, /* Priority at which the task is created. */
&xCheckStatusTask ); /* Used to pass out the created task's handle. */
}
else {
ESP_LOGI(TAG,"ERROR: Check Status Queue could not be created\n");
}
xTaskCreate(
vRestartDeviceTask, /* Function that implements the task. */
"RestartDeviceTask", /* Text name for the task. */
2048, /* Stack size in words, not bytes. */
NULL, /* Parameter passed into the task. */
5, /* Priority at which the task is created. */
&xRestartDeviceTask ); /* Used to pass out the created task's handle. */
ESP_LOGI(TAG, "Tasks created");
// Timer Stuff ---------------------------------------------------------------
esp_timer_create_args_t pwm_timer_args = {
.callback = event_callback,
.arg = xObstacleDetectedTask,
.dispatch_method = ESP_TIMER_TASK,
.name = "pwm_read_timer",
};
pwm_read_timer = NULL;
// create the timer
ESP_ERROR_CHECK(esp_timer_create(&pwm_timer_args, &pwm_read_timer));
ESP_LOGI(TAG, "PWM read timer created");
// GPIO Stuff ----------------------------------------------------------------
// init the gpio
gpio_config_t io_conf;
// FIRST: Output GPIO
// disable interrupt
io_conf.intr_type = GPIO_INTR_DISABLE;
// set as output mode
io_conf.mode = GPIO_MODE_OUTPUT;
// bitmask of the pins that you want to set
io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;
// disable pull-down mode
io_conf.pull_down_en = 0;
// disable pull-up mode
io_conf.pull_up_en = 0;
// configure GPIO with the given settings
ESP_ERROR_CHECK(gpio_config(&io_conf));
// init the door button to low
ESP_ERROR_CHECK(gpio_set_level(GPIO_OUTPUT_DOORBUTTON, 0));
// SECOND: PWM Output for RGB LED
ESP_ERROR_CHECK(pwm_init(PWM_PERIOD, duties, 3, pin_num));
ESP_ERROR_CHECK(pwm_set_phases(phase));
ESP_ERROR_CHECK(pwm_start());
// THIRD: Input GPIO and set to trigger ISR on any edge
//disable pull-down mode
io_conf.pull_down_en = 0;
//enable pull-up mode
io_conf.pull_up_en = 1;
//interrupt of any edge
io_conf.intr_type = GPIO_INTR_ANYEDGE;
//bit mask of the pins, use GPIO4/5 here
io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL;
//set as input mode
io_conf.mode = GPIO_MODE_INPUT;
ESP_ERROR_CHECK(gpio_config(&io_conf));
//install gpio isr service
ESP_ERROR_CHECK(gpio_install_isr_service(0));
// add ISR Handler for PWM read interrupt
ESP_ERROR_CHECK(gpio_isr_handler_add(
GPIO_INPUT_SENSOR_OBSTRUCTION,
PWMHandler,
(void *) GPIO_INPUT_SENSOR_OBSTRUCTION));
// add ISR Handler for OPEN reed switch
ESP_ERROR_CHECK(gpio_isr_handler_add(
GPIO_INPUT_SENSOR_OPEN,
state_isr_handler,
(void *) GPIO_INPUT_SENSOR_OPEN));
// add ISR Handler for CLOSE reed switch
ESP_ERROR_CHECK(gpio_isr_handler_add(
GPIO_INPUT_SENSOR_CLOSED,
state_isr_handler,
(void *) GPIO_INPUT_SENSOR_CLOSED));
ESP_LOGI(TAG, "GPIO setup complete");
}
else
{
/* Did not receive a notification within the expected time. */
}
ESP_LOGI(TAG, "App has begun");
}
}
// MQTT STUFF ------------------------------------------------------------------
// small func to subscribe to all necessary events, return msg_id of last event
void subscribe_gd_events() {
int msg_id;
msg_id = esp_mqtt_client_subscribe(gd_client, "cmnd/garagedoor/COLOR", 1);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_subscribe(gd_client, "cmnd/garagedoor/BUTTON", 2);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_subscribe(gd_client, "cmnd/garagedoor/CHECKSTATUS", 1);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_subscribe(gd_client, "cmnd/garagedoor/RESTART", 1);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
}
// handler for mqtt events
static esp_err_t mqtt_event_handler(esp_mqtt_event_handle_t event)
{
RGB_t RGB_val;
char CheckStatusTopic_str[4] = "TOP\0";
BaseType_t xRGBQueueStatus;
BaseType_t xCheckStatusQueueStatus;
switch (event->event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
subscribe_gd_events();
xTaskNotifyGive( xbegin_the_appTask );
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
ESP_LOGI(TAG, "MQTT_EVENT_DATA");
printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
printf("DATA=%.*s\r\n", event->data_len, event->data);
if (strncmp(event->topic, "cmnd/garagedoor/COLOR", event->topic_len) == 0) {
// change rgb led color
sscanf( event->data, "%i,%i,%i", &(RGB_val.red), &(RGB_val.green), &(RGB_val.blue) );
xRGBQueueStatus = xQueueSendToBack( xRGBQueue, &RGB_val, 0);
if ( xRGBQueueStatus != pdPASS ) {
ESP_LOGI(TAG,"ERROR: Could not send RGB str to queue.\n");
}
}
else if (strncmp(event->topic, "cmnd/garagedoor/BUTTON", event->topic_len) == 0) {
if (strncmp(event->data, "PRESSED", event->data_len) == 0) {
// activate door
xTaskNotifyGive( xActivateDoorTask );
}
}
else if (strncmp(event->topic, "cmnd/garagedoor/CHECKSTATUS", event->topic_len) == 0) {
// send back the status of specified topics
memcpy(CheckStatusTopic_str, event->data, 3);
xCheckStatusQueueStatus = xQueueSendToBack( xCheckStatusQueue, &CheckStatusTopic_str, 0);
if ( xCheckStatusQueueStatus != pdPASS ) {
ESP_LOGI(TAG,"ERROR: Could not send CheckStatus topic(s) to queue.\n");
}
}
else if (strncmp(event->topic, "cmnd/garagedoor/RESTART", event->topic_len) == 0) {
if (strncmp(event->data, "PRESSED", event->data_len) == 0) {
// restart the system
xTaskNotifyGive( xRestartDeviceTask );
}
}
break;
case MQTT_EVENT_ERROR:
ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
break;
}
return ESP_OK;
}
// func to init the mqtt app
static void mqtt_app_start(void)
{
esp_mqtt_client_config_t mqtt_cfg = {
.uri = CONFIG_BROKER_URL,
.event_handle = mqtt_event_handler,
.port = 1883,
.username = CONFIG_BROKER_USERNAME,
.password = CONFIG_BROKER_PASSWORD,
// .user_context = (void *)your_context
};
gd_client = esp_mqtt_client_init(&mqtt_cfg);
esp_mqtt_client_start(gd_client);
}
// -----------------------------------------------------------------------------
// MAIN ------------------------------------------------------------------------
void app_main()
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(example_connect());
ESP_LOGI(TAG, "[APP] Startup..");
ESP_LOGI(TAG, "[APP] Free memory: %d bytes", esp_get_free_heap_size());
ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());
esp_log_level_set("*", ESP_LOG_INFO);
esp_log_level_set("MQTT_CLIENT", ESP_LOG_VERBOSE);
esp_log_level_set("TRANSPORT_TCP", ESP_LOG_VERBOSE);
esp_log_level_set("TRANSPORT_SSL", ESP_LOG_VERBOSE);
esp_log_level_set("TRANSPORT", ESP_LOG_VERBOSE);
esp_log_level_set("OUTBOX", ESP_LOG_VERBOSE);
// create the begin the app task, but don't run it
xTaskCreate(
vbegin_the_appTask, /* Function that implements the task. */
"begin_the_appTask", /* Text name for the task. */
2048, /* Stack size in words, not bytes. */
NULL, /* Parameter passed into the task. */
4, /* Priority at which the task is created. */
&xbegin_the_appTask ); /* Used to pass out the created task's handle. */
// enable mqtt
mqtt_app_start();
}
I then created things for each MQTT topic:
Bridge mqtt:broker:mosquitto "Mosquitto MQTT Broker"
[
host="HOST",
port=PORT,
clientid="OPENHAB",
secure=BOOL,
username="USER",
password="PASS"
] {
Thing topic garagedoor "Garage Door MQTT Thing" {
Channels:
Type switch : button
[
stateTopic = "stat/garagedoor/BUTTON",
commandTopic = "cmnd/garagedoor/BUTTON",
on = "PRESSED",
off = "UNPRESSED",
qos = 2
]
Type string : state
[
stateTopic = "stat/garagedoor/STATE",
commandTopic = "cmnd/garagedoor/STATE"
]
Type switch : obstruction
[
stateTopic = "stat/garagedoor/OBSTRUCTION",
on = "YES",
off = "NO"
]
Type colorRGB : color
[
stateTopic = "stat/garagedoor/COLOR",
commandTopic = "cmnd/garagedoor/COLOR"
]
Type string : checkstatus
[
commandTopic = "cmnd/garagedoor/CHECKSTATUS"
]
Type switch : restart
[
stateTopic = "stat/garagedoor/RESTART",
commandTopic = "cmnd/garagedoor/RESTART",
on = "PRESSED",
off = "UNPRESSED"
]
}
}
And the items for each MQTT thing, along with the Apple HomeKit items:
Group gGarageDoor "Garage Door" { homekit="GarageDoorOpener" }
Switch gd_obstruction "Garage Door Obstruction Status" (gGarageDoor, gMQTT) { homekit="GarageDoorOpener.ObstructionStatus" }
String gd_currstate "Garage Door Current Door State" (gGarageDoor, gMQTT) { homekit="GarageDoorOpener.CurrentDoorState" }
String gd_targetstate "Garage Door Target Door State" (gGarageDoor, gMQTT) { homekit="GarageDoorOpener.TargetDoorState" }
String gd_name "Garage Door Name" (gGarageDoor, gMQTT) { homekit="GarageDoorOpener.Name" }
Group gESPGarage "ESP Garage"
Switch ESP_Button "ESP Button" (gESPGarage) { channel="mqtt:topic:mosquitto:garagedoor:button" }
String ESP_State "ESP State" (gESPGarage) { channel="mqtt:topic:mosquitto:garagedoor:state" }
Switch ESP_Obstruction "ESP Obstruction" (gESPGarage) { channel="mqtt:topic:mosquitto:garagedoor:obstruction" }
Color ESP_Color "ESP Color" (gESPGarage) { channel="mqtt:topic:mosquitto:garagedoor:color" }
String ESP_CheckStatus "ESP Check Status" (gESPGarage) { channel="mqtt:topic:mosquitto:garagedoor:checkstatus" }
Switch ESP_Restart "ESP Restart" (gESPGarage) { channel="mqtt:topic:mosquitto:garagedoor:restart" }
Finally the rules to connect everything together:
var Timer close_timer = null
var String previous_state
rule "Door Opened"
when
Item ESP_State received update "OPEN"
then
gd_targetstate.postUpdate("OPEN")
gd_currstate.postUpdate("OPEN")
previous_state = "OPEN"
ESP_Color.sendCommand(HSBType.fromRGB(0, 0, 0))
end
rule "Door Closed"
when
Item ESP_State received update "CLOSED"
then
gd_targetstate.postUpdate("CLOSED")
gd_currstate.postUpdate("CLOSED")
previous_state = "CLOSED"
ESP_Color.sendCommand(HSBType.fromRGB(0, 0, 0))
end
rule "Activate Open Door"
when
Item gd_targetstate received command "OPEN"
then
ESP_Button.sendCommand(ON)
end
rule "Activate Close Door"
when
Item gd_targetstate received command "CLOSED"
then
if (ESP_Obstruction.state == ON) {
}
else {
ESP_Button.sendCommand(ON)
if (close_timer === null) {
close_timer = createTimer(now.plusSeconds(10)) [|
ESP_CheckStatus.sendCommand("DOR")
]
} else {
close_timer.reschedule(now.plusSeconds(5))
}
}
end
rule "Obstruction Occured"
when
Item ESP_Obstruction received update ON
then
gd_obstruction.postUpdate(ON)
if ((ESP_State.state == "MOVING") && (previous_state == "OPEN")) {
gd_currstate.postUpdate("STOPPED")
gd_targetstate.postUpdate("OPEN")
}
ESP_Color.sendCommand(HSBType.fromRGB(255, 100, 0))
end
rule "Obstruction Cleared"
when
Item ESP_Obstruction received update OFF
then
gd_obstruction.postUpdate(OFF)
if (ESP_State.state != "MOVING") {
ESP_Color.sendCommand(HSBType.fromRGB(0, 0, 0))
}
end
rule "Moving Started"
when
Item ESP_State received update "MOVING"
then
if (previous_state == "OPEN") {
gd_targetstate.postUpdate("CLOSED")
gd_currstate.postUpdate("CLOSING")
ESP_Color.sendCommand(HSBType.fromRGB(255, 0, 255))
}
else if (previous_state == "CLOSED") {
gd_targetstate.postUpdate("OPEN")
gd_currstate.postUpdate("OPENING")
ESP_Color.sendCommand(HSBType.fromRGB(0, 255, 255))
}
end
It’s all working pretty well right now, but I am concerned about the task scheduling for the Obstruction Detection on the ESP MCU. Currently the way I’m doing it is having an interrupt be driven for every rising/falling edge for the Safety Sensor signal. From this, I am verifying a valid non-obstruction status in two ways: comparing the time of the high signal to 6200 microseconds (which is the time I measured on my set of Safety Sensors), and having a timer go off after two software ticks from any falling edge. This works very well for its job, but I’m worried that running two ISRs every 7ms eats up a lot of processing time, in effect blocking all the other tasks, namely MQTT events. If anyone has any insights or recommendations in regards to this, I would love to hear it!
If anyone has any questions on design choices or clarifying what I did, I’d be more than happy to discuss! Thanks!