Garage Door Controller with Obstruction Detection and Apple HomeKit Integration

Tags: #<Tag:0x00007f616f631ca8> #<Tag:0x00007f616f631b40> #<Tag:0x00007f616f6316b8>

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 :laughing:.

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!

1 Like