I've had an ESP32-WROOM-32 sitting around for a while, and I thought I'd throw it back into action as an MQTT data ingress point for my home system. It's been a LONG time (about 8 years, I think) since I was implementing one of these things. I needed the following :
Base it on MQTT (message brokering) to reduce complexity of the tools required
DHCP compatible on-network presence for a drop-in, rapid security network
Ease of configuring or adapting a new sensor into the system
Robust enough that it could actually run and not encounter a memory leak
Add into the mix that I don't use the Mac any longer, and suddenly, I'm starting from scratch. I started out following Espressif's guide, but ran into a snag straight out of the gate, and that was I couldn't get the stupid thing to compile in the Espressif environment. Bugger. I have to go back to the Arduino IDE.
So, back into the Arduino IDE, slap some code together (usual copy-and-paste from things like Stack Overflow, the forums, etc).
ESP32 Code
I did have an issue where the ESP32 dropped off the network after a few hours. That almost killed my DHCP requirement out of the gate, and then one night, I had a dream about lines of code. A quick modification in the code to actually check if the ethernet had disconnected, and I had something that stayed with me a bit longer. The code turned out to be :
/**
* A simple firmware for sending MQTT queue messages based on GPIO changes
* Written (well, hack and slashed from examples) by Joe Lewis, December 18, 2024
*/
/*
* Some defaults
*/
//#define DEBUG_WIFI
#define USE_EXTERNAL_RES_PINS_34_39 1
#define DEFAULT_MQTT_CLIENT_PREFIX "mqttgpio-"
#define DEFAULT_MQTT_BROKER "mqttserverhostname"
#define DEFAULT_MQTT_TOPIC "GPIO"
#define DEFAULT_INIT_MESSAGE "init-"
#define DEFAULT_MQTT_MSG_PREFIX "pin-"
#define DEFAULT_MQTT_PIN_SEPARATOR "-"
#define DEFAULT_MQTT_PORT 1883
#define DEFAULT_WIRELESS_NETWORK_NAME ""
#define DEFAULT_WIRELESS_NETWORK_KEY ""
#define HALF_SECONDS_BEFORE_ETHDOWN 60
typedef struct {
int p;
int v;
int lastSteadyState;
int lastFlickerableState;
unsigned long lastDebounceTime;
} pin_t;
#include "driver/gpio.h"
#include <soc/gpio_struct.h>
// the trailing 0 is for the end of the list
#ifdef USE_EXTERNAL_RES_PINS_34_39
#define NUMBER_OF_PINS 16
pin_t pins[NUMBER_OF_PINS] = {
{ GPIO_NUM_0, 0, LOW, LOW, 0 }, // possible boot time issues
{ GPIO_NUM_1, 0, LOW, LOW, 0 }, // possible boot time issues
{ GPIO_NUM_2, 0, LOW, LOW, 0 }, // possible boot time issues
{ GPIO_NUM_3, 0, LOW, LOW, 0 }, // possible boot time issues
{ GPIO_NUM_4, 0, LOW, LOW, 0 }, // possible boot time issues
{ GPIO_NUM_5, 0, LOW, LOW, 0 }, // possible boot time issues
{ GPIO_NUM_13, 0, LOW, LOW, 0 },
{ GPIO_NUM_14, 0, LOW, LOW, 0 },
{ GPIO_NUM_15, 0, LOW, LOW, 0 },
{ GPIO_NUM_16, 0, LOW, LOW, 0 },
{ GPIO_NUM_32, 0, LOW, LOW, 0 },
{ GPIO_NUM_33, 0, LOW, LOW, 0 },
{ GPIO_NUM_34, 0, LOW, LOW, 0 }, // has a pre-built pull up resistor
{ GPIO_NUM_35, 0, LOW, LOW, 0 }, // has to have external pull up resistor soldered in
{ GPIO_NUM_36, 0, LOW, LOW, 0 }, // has to have external pull up resistor soldered in
{ GPIO_NUM_39, 0, LOW, LOW, 0 } // has to have external pull up resistor soldered in //USED
};
#else
#define NUMBER_OF_PINS 13
pin_t pins[NUMBER_OF_PINS] = {
{ GPIO_NUM_0, 0, LOW, LOW, 0 }, // possible boot time issues
{ GPIO_NUM_1, 0, LOW, LOW, 0 }, // possible boot time issues
{ GPIO_NUM_2, 0, LOW, LOW, 0 }, // possible boot time issues
{ GPIO_NUM_3, 0, LOW, LOW, 0 }, // possible boot time issues
{ GPIO_NUM_4, 0, LOW, LOW, 0 }, // possible boot time issues
{ GPIO_NUM_5, 0, LOW, LOW, 0 }, // possible boot time issues
{ GPIO_NUM_13, 0, LOW, LOW, 0 },
{ GPIO_NUM_14, 0, LOW, LOW, 0 },
{ GPIO_NUM_15, 0, LOW, LOW, 0 },
{ GPIO_NUM_16, 0, LOW, LOW, 0 },
{ GPIO_NUM_32, 0, LOW, LOW, 0 },
{ GPIO_NUM_33, 0, LOW, LOW, 0 },
{ GPIO_NUM_34, 0, LOW, LOW, 0 } // has a pre-built pull up resistor
};
#endif
// Important to be defined BEFORE including ETH.h for ETH.begin() to work.
// Example RMII LAN8720 (Olimex, etc.)
#ifndef ETH_PHY_TYPE
#define ETH_PHY_TYPE ETH_PHY_LAN8720
#define ETH_PHY_ADDR 0
#define ETH_PHY_MDC 23
#define ETH_PHY_MDIO 18
#define ETH_PHY_POWER -1
#define ETH_CLK_MODE ETH_CLOCK_GPIO0_IN
#endif
#define ESP_INTR_FLAG_DEFAULT 0
#define DEBOUNCE_TIME 50
#include <ETH.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include "esp_mac.h"
char mqtt_topic[255];
char mqtt_broker[255];
char mqtt_msg_prefix[32];
char mqtt_client_id[32];
int mqtt_port;
int delayCountSinceLastSend;
int sendCount;
char wifi_ssid[128];
char wifi_password[128];
bool use_wifi;
bool eth_connected;
WiFiClient espClient;
PubSubClient mqttClient(espClient);
typedef struct GPIOEvent_t {
pin_t *pin;
GPIOEvent_t *next;
} GPIOEvent_t;
GPIOEvent_t *events;
/*
* GPIO Functions
*/
void sendMessage(char *msg) {
if (!mqttClient.connected()) {
reconnect();
}
mqttClient.publish(mqtt_topic,msg);
}
void processEvent(GPIOEvent_t *e) {
char msg[255];
/*
int new_value;
new_value = gpio_get_level((gpio_num_t)e->pin->p);
if (new_value != e->pin->v) {
e->pin->v = new_value;
*/
e->pin->v = gpio_get_level((gpio_num_t)e->pin->p);;
sprintf(msg,"%s%s%d=%d",mqtt_msg_prefix,DEFAULT_MQTT_PIN_SEPARATOR,e->pin->p,e->pin->v);
sendMessage(msg);
// }
}
static void IRAM_ATTR gpio_isr_handler(void* arg) {
pin_t *p = (pin_t*)arg;
GPIOEvent_t *e = (GPIOEvent_t*)malloc(sizeof(GPIOEvent_t));
e->next = events;
e->pin = p;
e->pin->v = gpio_get_level((gpio_num_t)e->pin->p);
events = e;
}
void reconnect() {
// Loop until we're reconnected
int retries;
retries = 3;
while ((retries > 0) && (!mqttClient.connected())) {
// while (!mqttClient.connected()) {
retries--;
Serial.print("Attempting MQTT connection...");
// Attempt to connect
if (mqttClient.connect(mqtt_client_id)) {
Serial.println("connected");
} else {
Serial.print("failed, rc=");
Serial.print(mqttClient.state());
Serial.println(" try again in 5 seconds");
// Wait 5 seconds before retrying
delay(5000);
}
}
}
/*
* Ethernet-specific stuff
*/
// WARNING: onEvent is called from a separate FreeRTOS task (thread)!
void ethernetOnEvent(arduino_event_id_t event) {
switch (event) {
case ARDUINO_EVENT_ETH_START:
Serial.println("ETH Started");
// The hostname must be set after the interface is started, but needs
// to be set before DHCP, so set it from the event handler thread.
ETH.setHostname(mqtt_client_id);
break;
case ARDUINO_EVENT_ETH_CONNECTED: Serial.println("ETH Connected"); break;
case ARDUINO_EVENT_ETH_GOT_IP:
Serial.println("ETH Got IP");
Serial.println(ETH);
eth_connected = true;
break;
case ARDUINO_EVENT_ETH_LOST_IP:
Serial.println("ETH Lost IP");
eth_connected = false;
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
Serial.println("ETH Disconnected");
eth_connected = false;
break;
case ARDUINO_EVENT_ETH_STOP:
Serial.println("ETH Stopped");
eth_connected = false;
break;
default: break;
}
}
void readConfiguration() {
Serial.println("readConfiguration() not implemented, skipping (we use defaults)");
}
void setup() {
pin_t *pin;
uint8_t mac_addr[6];
char *mqtt_pin_message,*tmp_message;
Serial.begin(115200);
events = NULL;
strcpy(mqtt_topic,DEFAULT_MQTT_TOPIC);
strcpy(mqtt_broker,DEFAULT_MQTT_BROKER);
strcpy(mqtt_msg_prefix,DEFAULT_MQTT_MSG_PREFIX);
delayCountSinceLastSend = 0;
sendCount = 0;
mqtt_port = DEFAULT_MQTT_PORT;
use_wifi = false;
// read the configuration file if we can
readConfiguration();
if (strlen(wifi_ssid) > 0) {
// wifi network is defined, use it
use_wifi = true;
strcpy(wifi_ssid,DEFAULT_WIRELESS_NETWORK_NAME);
strcpy(wifi_password,DEFAULT_WIRELESS_NETWORK_KEY);
Serial.print("Starting connecting WiFi, connecting to ");
Serial.print(wifi_ssid);
#ifdef DEBUG_WIFI
Serial.print(" / ");
Serial.print(wifi_password);
#endif
Serial.println("...");
delay(10);
WiFi.begin(wifi_ssid, wifi_password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
esp_read_mac(mac_addr,ESP_MAC_WIFI_STA);
sprintf(mqtt_client_id,"%02X%02X%02X%02X%02X%02X",
mac_addr[0],mac_addr[1],mac_addr[2],
mac_addr[3],mac_addr[4],mac_addr[5]);
strcpy(mqtt_msg_prefix,mqtt_client_id);
} else {
use_wifi = false;
Network.onEvent(ethernetOnEvent);
ETH.begin();
while (eth_connected == false) {
delay(50);
}
Serial.print("Using Ethernet, IP address : ");
Serial.println(ETH);
esp_read_mac(mac_addr,ESP_MAC_ETH);
sprintf(mqtt_client_id,"%02X%02X%02X%02X%02X%02X",
mac_addr[0],mac_addr[1],mac_addr[2],
mac_addr[3],mac_addr[4],mac_addr[5]);
strcpy(mqtt_msg_prefix,mqtt_client_id);
}
mqttClient.setServer(mqtt_broker,mqtt_port);
gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
for (int x = 0;x < NUMBER_OF_PINS;x++) {
pin = &(pins[x]);
// set up the pin
gpio_reset_pin((gpio_num_t)pin->p);
//set the direction
gpio_set_direction((gpio_num_t)pin->p,GPIO_MODE_INPUT);
if ((pin->p != GPIO_NUM_34) &&
(pin->p != GPIO_NUM_35) &&
(pin->p != GPIO_NUM_36) &&
(pin->p != GPIO_NUM_39)) {
gpio_set_pull_mode((gpio_num_t)pin->p,GPIO_PULLUP_ONLY);
}
// run on any edge
gpio_set_intr_type((gpio_num_t)pin->p,GPIO_INTR_ANYEDGE);
// add the handler
gpio_isr_handler_add((gpio_num_t)pin->p,gpio_isr_handler,pin);
}
}
void loop() {
Serial.println("start sending events.");
if (events == NULL) {
// if (sendCount > 0) {
// delayCountSinceLastSend = 0;
// sendCount = 0;
// }
// if (delayCountSinceLastSend > HALF_SECONDS_BEFORE_ETHDOWN) {
// if ((use_wifi == false) && (eth_connected == true)) {
// ETH.end();
// }
// } else {
// delayCountSinceLastSend++;
// }
delay(500); // no events, let's just pause
} else {
if ((use_wifi == false) && (eth_connected == false)) {
ETH.begin();
while (eth_connected == false) {
delay(50);
}
}
while (events != NULL) {
GPIOEvent_t *event = events;
events = events->next;
processEvent(event);
free(event);
// sendCount++;
}
}
mqttClient.loop();
// at some point, perhaps an esp_restart() might be needed.
}
I have NOT modified the code from the network. I did not add WIRELESS information into those, but if I wanted to use wireless, I could. I'm running this on a POE network, so I might as well use that.
Essentially, the basic code starts up the ethernet (it would use wireless if I defined those fields), and then uses the MAC address (I'm refusing to override it) as it's client name.
Additionally, there is no configuration for a GPIO pin other than "listening on this pin". With these Olimex POE devices on the ESP32-WROOM-POE, pins 34, 36, and 39 are all needing to use an external pin grounding (e.g. add a resistor to the 3v pin) if you are going to use them. Aside from that, it's plug-and-play. Change the hostname, compile the code, and upload it to your devices.
Then, deploy a Raspberry Pi with mosquitto (the Linux MQTT server, and install the development libraries, too) with the same hostname you threw into your ESP32 code. That likely needs a static IP address (either configure the device with a static, or configure your DHCP server to always assign the same IP address).
With that, you simply need the two following services.
GPIO translator - this is a tool that subscribes to a channel of your choosing, and if something matches, it converts that into a message for another channel. For example, A3C1FFD901-PIN33=1 would get translated to "gate=OPEN". There was a very deep fear that I had to keep track of states for more complex things like a garage door where you had an OPENING, OPEN, CLOSING, and CLOSED states with two sensors, before I realized I didn't care, mapping the states would already take care of that. I DID need to swallow duplicate events because the ESP32 would send the same message a few times (you COULD bypass that using a debounce function, but why when I could potentially swallow those duplicates here?). Turns out to function pretty great. Code is below.
Nagios-to-CMD file - this was fairly simple in comparison to the last one. All it did was listen on an MQTT channel, and parse a message, then dump it into the Nagios CMD file used for passive or external checks.
So, you have to make sure Nagios is set up to process external checks, define checks with no actual check (set passive_checks_enabled to 1 in the service definition for your sensor), set command_file (it might already be set) and check_external_commands to a "1" in your global Nagios configuration file, and restart Nagios. Take note of your host name inside of your Nagios config for these services - you'll need it in one of the configs later. Also, make sure the directory and file specified in command_file is writeable by the user you will be running these things as.
GPIO message translator
First, let's look at our MQTT GPIO message translator. This is yet another hack-and-slash, I mean, copy-and-paste, chunk of code. The code :
/**
* @file sentinel_translator.c
*
* @brief Mqtt client used to translate ESP32 MQTT messages to other messages
*
* @date 19-Dec-2024
* @copyright GNU General Public License v3
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdarg.h>
#include <string.h>
#include <semaphore.h>
#include <stdint.h>
#include <time.h>
uses a configuration file called /etc/sentinel/translator.conf, and you will need to make sure there are "rules" defined inside. My configuration :
input_topic GPIO
output_topic SENSORS
hostname localhost
port 1883
daemonize true
log_level INFO
Rule A3C1FFD901-34=1 'Attic Door=OPEN'
Rule A3C1FFD901-34=0 "Attic Door=CLOSED"
Rule A3C1FFD901-33=1 'Bedroom Door=OPEN'
Rule "A3C1FFD901-33=0" "Bedroom Door=CLOSED"
# I always thought I needed something special, but the
# more I thought about it, the less we actually needed
# anything special for a four-state-sensor like garage
# doors
Rule A3C1FFD901-13=1 'Garage Door=OPENING'
Rule A3C1FFD901-13=0 'Garage Door=CLOSED'
Rule A3C1FFD901-14=1 'Garage Door=OPEN'
Rule A3C1FFD901-14=0 'Garage Door=CLOSING'
The rules will be unknown when you start. Simply run "mosquitto_sub -t \# -v" and then physically work the sensors tied to your GPIO pins (e.g. open the door). It will print the topic, and the "A3C1FFD901-13=1" message. Since you know it was slapped in there when your garage door started opening, you can map the rule appropriately. Run through each part until you know how each one behaves, and then you can launch the program (no parameters required).
This will see the pin message from the MQTT on the GPIO channel, and write it back as the second parameter, e.g. "Garage Door=OPENING". Note that the left side of the equal sign ("Garage Door") MUST match the service check name in Nagios for each one you configure.
Now, all we need to do is get that second parameter into Nagios.
Nagios Interface
Since Nagios can be configured to notify via e-mails, etc, I'll not worry about that here. You can play with that on your own. But, we DO need an MQTT subscription tool that writes these sensors into the external command file for Nagios. The code (yes, another hack-and-slash) :
/**
* @file sentinel_nagios_fifo.c
*
* @brief Mqtt client used to take MQTT messages and write them to Nagios
*
* @date 19-Dec-2024
* @copyright GNU General Public License v3
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdarg.h>
#include <string.h>
#include <semaphore.h>
#include <stdint.h>
#include <time.h>
Now, remember that command_file and other stuff from Nagios? We're going to need that. Edit your configuration file (/etc/sentinel/nagiosfifo.conf) and make the appropriate edits.
Configuration file :
topic security
daemonize true
nagios_fifo_file /var/lib/nagios3/rw/nagios.cmd
log_level WARNING
# nagios_service_host localhost
nagios_service_host house
state OPEN CRITICAL
state OPENING WARNING
state CLOSING WARNING
state CLOSED OK
Then, start the processes up and you have your home door sensors tied into Nagios. Congrats!
No comments:
Post a Comment