DoorBellGateway/src/main.cpp

416 lines
13 KiB
C++

/******************************************************
* (C) 2020 Dirk Jahnke (dirk.jahnke@mailbox.org)
* Uses Arduino with Ethernet Shield.
* Thus the GPIO usage is limited, because the shield
* needs the SPI pns: 10 (SS), 11 (MOSI), 12 (MISO), 13 (SCK).
*
* If the SD card is to be used, then pin 4 is used as SS
* for the SD card.
*
* We use 2 outputs to control Relays for
* - switching LED at the mailbox (light on/off) and
* - trigger the internal bell (gong).
* Following pins can be used as I/O:
* - 1
* - 2
* - 3
* - 5
* - 6 POSTBOX_DOOR_PIN
* - 7 POSTBOX_FLAP_PIN
* - 8 LIGHT_REQUEST_BUTTON_PIN
* - 9 DOOR_BELL_BUTTON_PIN
* - 13 out: LED_PIN
* - 14 (A0) out: DOOR_BELL_BUZZER_PIN
* - 15 (A1) out: POSTBOX_LIGHT_PIN
* - 16 (A2)
* - 17 (A3)
* - 18 (A4)
* - 19 (A5)
******************************************************/
#include <Arduino.h>
#include <SPI.h>
#include <Ethernet.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <Bounce2.h>
// Auto reset Arduino to start network again and avoid counter overflow
#define REBOOT_EVERY_ms (28 * (3600000 /* hours */))
void (*resetArduinoNow) (void) = 0; // declare reset fuction at address 0
// Update these with values suitable for your network.
byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xEA };
//IPAddress ip(192, 168, 89, 205);
IPAddress mqttServerIP(192, 168, 89, 222);
const char *mqttUser = "doorbell";
const char *mqttPassword = "tkSEM35EnLvVi7etjd";
const char *lwtTopic = "doorBellGW/LWT";
const char *lwtMessage = "Offline";
const char *onlineMessage = "Online";
const bool lwtRetain = true;
const int lwtQos = 2;
const char *doorBellTopic = "doorBellGW/doorbell";
const char *postboxFlapTopic = "doorBellGW/mailboxFlap";
const char *postboxDoorTopic = "doorBellGW/mailboxDoor";
const char *doorBellTriggerSoundTopic = "doorBellGW/doorbellTrigger";
const char *doorLightRequestTopic = "doorBellGW/lightRequest";
const char *postboxLightTopic = "doorBellGW/light";
const char *postboxLightStatusTopic = "doorBellGW/light/status";
EthernetClient ethClient;
EthernetServer server(80);
PubSubClient mqttClient(ethClient);
#define DOOR_BELL_BUTTON_PIN 9
#define LIGHT_REQUEST_BUTTON_PIN 8
#define POSTBOX_FLAP_PIN 7
#define POSTBOX_DOOR_PIN 6
// Debounce intervals (ms)
#define DOOR_BELL_DEBOUNCE_ms 200
#define LIGHT_REQUEST_DEBOUNCE_ms 200
#define POSTBOX_FLAP_DEBOUNCE_ms 200
#define POSTBOX_DOOR_DEBOUNCE_ms 500
// Output pins
#define LED_PIN 13
#define DOOR_BELL_BUZZER_PIN 14
#define POSTBOX_LIGHT_PIN 15
#define OUTPUT_NONE 0xff
struct {
const uint8_t pin;
const unsigned long quietAfterTriggerFor_ms;
unsigned long lastTrigger_ts;
bool isTriggered;
const char *topic; // sends changes: trigger / re-trigger, / release
const uint8_t triggerOutputPin;
Bounce debouncer;
uint16_t debounceInterval_ms;
} myInputs[] = {
{ DOOR_BELL_BUTTON_PIN, 500, 0 , false, doorBellTopic, DOOR_BELL_BUZZER_PIN, Bounce(), DOOR_BELL_DEBOUNCE_ms },
{ LIGHT_REQUEST_BUTTON_PIN, 500, 0 , false, doorLightRequestTopic, POSTBOX_LIGHT_PIN, Bounce(), LIGHT_REQUEST_DEBOUNCE_ms },
{ POSTBOX_FLAP_PIN, 1000, 0, false, postboxFlapTopic, OUTPUT_NONE, Bounce(), POSTBOX_FLAP_DEBOUNCE_ms },
{ POSTBOX_DOOR_PIN, 1000, 0, false, postboxDoorTopic, OUTPUT_NONE, Bounce(), POSTBOX_DOOR_DEBOUNCE_ms }
};
#define NUM_INPUTS (sizeof(myInputs) / sizeof(myInputs[0]))
int ledState = LOW;
struct {
const uint8_t pin;
const unsigned long triggerTime_ms;
bool isTriggered;
unsigned long lastTrigger_ts;
const char *listenTopic; // can be triggered by mqtt message as well
const char *statusTopic; // sends on/off status
} myOutputs[] = {
{ DOOR_BELL_BUZZER_PIN, 500, false, 0, doorBellTriggerSoundTopic, NULL },
{ POSTBOX_LIGHT_PIN, 30000, false, 0, postboxLightTopic, postboxLightStatusTopic }
};
#define NUM_OUTPUTS (sizeof(myOutputs) / sizeof(myOutputs[0]))
// Forward function declarations:
void reconnect();
void sendMqttMessage(const char *topic, const char *message);
uint8_t getOutputIndexByPin(uint8_t pin) {
for (unsigned int i=0; i<NUM_OUTPUTS; ++i) {
if (myOutputs[i].pin == pin) {
return i;
}
}
Serial.print(F("ERROR: Could not find output by pin")); Serial.println(pin);
return OUTPUT_NONE;
}
void switchOutputOn(unsigned int outputNumber) {
Serial.print(F("Switch output on: "));
Serial.print(outputNumber);
Serial.print(F(" (pin "));
Serial.print(myOutputs[outputNumber].pin);
Serial.print(F(") topic "));
Serial.println(myOutputs[outputNumber].listenTopic);
digitalWrite(myOutputs[outputNumber].pin, LOW);
myOutputs[outputNumber].isTriggered = true;
myOutputs[outputNumber].lastTrigger_ts = millis();
if (myOutputs[outputNumber].statusTopic != NULL) {
sendMqttMessage(myOutputs[outputNumber].statusTopic, "on");
}
}
void switchOutputOffHWOnly(unsigned int outputNumber) {
digitalWrite(myOutputs[outputNumber].pin, HIGH);
}
void switchOutputOff(unsigned int outputNumber) {
Serial.print(F("Switch output off: "));
Serial.print(outputNumber);
Serial.print(F(" (pin "));
Serial.print(myOutputs[outputNumber].pin);
Serial.print(F(") topic "));
Serial.println(myOutputs[outputNumber].listenTopic);
switchOutputOffHWOnly(outputNumber);
myOutputs[outputNumber].isTriggered = false;
if (myOutputs[outputNumber].statusTopic != NULL) {
sendMqttMessage(myOutputs[outputNumber].statusTopic, "off");
}
}
void checkOutputAutoOff() {
static bool headerDone = false;
for (unsigned int outputNumber=0; outputNumber<NUM_OUTPUTS; ++outputNumber) {
if (myOutputs[outputNumber].isTriggered) {
if (!headerDone) {
Serial.print(F("checkOutputAutoOff: ts=")); Serial.println(millis());
headerDone = true;
}
//Serial.print(outputNumber); Serial.print(": ");
//Serial.println(myOutputs[outputNumber].lastTrigger_ts + myOutputs[outputNumber].triggerTime_ms);
if ((myOutputs[outputNumber].lastTrigger_ts + myOutputs[outputNumber].triggerTime_ms) < millis()) {
Serial.println(F("Auto off output:"));
switchOutputOff(outputNumber);
}
}
}
}
void mqttReceiveMessage(char* topic, uint8_t* payload, unsigned int length) {
Serial.print(F("Message arrived ["));
Serial.print(topic);
Serial.print(F("] "));
Serial.print(length);
Serial.print(F(" bytes: "));
Serial.println((char *) payload);
for (unsigned int i=0;i<length;i++) {
Serial.print((char)payload[i]);
}
Serial.println();
bool handlerFound = false;
for (unsigned int i=0; i<NUM_OUTPUTS; ++i) {
if (strcmp(myOutputs[i].listenTopic, topic) == 0) {
handlerFound = true;
if (strncmp((char *) payload, "on", length < 2 ? length : 2) == 0) {
switchOutputOn(i);
} else if (strncmp((char *) payload, "off", length < 3 ? length : 3) == 0) {
switchOutputOff(i);
} else {
Serial.print(F("Unknown MQTT message payload received, topic: "));
Serial.print(topic);
Serial.print(F(", payload: "));
Serial.println((char *) payload);
}
}
}
if (!handlerFound) {
Serial.print(F("Unknown MQTT message received, topic: "));
Serial.print(topic);
Serial.print(F(", payload: "));
Serial.println((char *) payload);
}
}
void reconnect() {
// Loop until we're reconnected
while (!mqttClient.connected()) {
Serial.print(F("Attempting MQTT connection..."));
// Attempt to connect
if (mqttClient.connect("doorBellGW", mqttUser, mqttPassword, lwtTopic, lwtQos, lwtRetain, lwtMessage)) {
Serial.println(F("connected"));
// Once connected, publish an announcement...
mqttClient.publish(lwtTopic, onlineMessage, lwtRetain);
// mqttClient.publish("hello","hello world");
// ... and resubscribe
Serial.println(F("MQTT subscribe doorbell"));
if (!mqttClient.subscribe("doorBellGW", 1)) {
Serial.println(F("ERROR: Failed to subscribe"));
}
for (unsigned int i=0; i<NUM_OUTPUTS; ++i) {
Serial.print(F("MQTT subscribe ")); Serial.println(myOutputs[i].listenTopic);
if (!mqttClient.subscribe(myOutputs[i].listenTopic, 1)) {
Serial.println(F("ERROR: Failed to subscribe"));
}
}
} else {
Serial.print(F("failed, rc="));
Serial.print(mqttClient.state());
Serial.println(F(" try again in 5 seconds"));
// Wait 5 seconds before retrying
delay(5000);
}
}
}
void sendMqttMessage(const char *topic, const char *message) {
reconnect();
mqttClient.publish(topic, message, true);
}
void setup()
{
// Initialize serial port
Serial.begin(9600);
while (!Serial) continue;
Serial.println(F("DoorbellGW Copyright (C) 2020 Dirk Jahnke"));
Serial.println(F("Door Bell MQTT Gateway started."));
// setup the input and output pins
for (unsigned int i = 0; i < NUM_INPUTS; i++) {
pinMode(myInputs[i].pin, INPUT_PULLUP);
myInputs[i].debouncer.attach(myInputs[i].pin);
myInputs[i].debouncer.interval(myInputs[i].debounceInterval_ms);
}
// Setup the output pins :
for (unsigned int i=0; i < NUM_OUTPUTS; ++i) {
pinMode(myOutputs[i].pin, OUTPUT);
switchOutputOffHWOnly(i);
}
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, ledState);
// setup MQTT
mqttClient.setServer(mqttServerIP, 1883);
mqttClient.setCallback(mqttReceiveMessage);
// Initialize Ethernet libary
if (!Ethernet.begin(mac)) {
Serial.println(F("Failed to initialize Ethernet library"));
return;
}
// Start to listen
server.begin();
Serial.println(F("Server is ready."));
Serial.print(F("Please connect to http://"));
Serial.println(Ethernet.localIP());
// Setup the output pins logically:
for (unsigned int i=0; i < NUM_OUTPUTS; ++i) {
switchOutputOff(i);
}
}
void checkInputSignals() {
bool needToToggleLed = false;
for (unsigned int i = 0; i < NUM_INPUTS; i++) {
myInputs[i].debouncer.update();
if (millis()-myInputs[i].lastTrigger_ts > myInputs[i].quietAfterTriggerFor_ms) {
int readValue = myInputs[i].debouncer.read();
if (!readValue) {
// input is triggered, compare with existing state
if (myInputs[i].isTriggered) {
// was triggered as well, could send a retrigger now
Serial.print(F("Re-Trigger pin ")); Serial.println(myInputs[i].pin);
sendMqttMessage(myInputs[i].topic, "re-trigger");
needToToggleLed = true;
} else {
// handle trigger now
myInputs[i].isTriggered = true;
// send Trigger
Serial.print(F("Trigger pin ")); Serial.println(myInputs[i].pin);
sendMqttMessage(myInputs[i].topic, "trigger");
needToToggleLed = true;
if (myInputs[i].triggerOutputPin != OUTPUT_NONE) {
switchOutputOn(getOutputIndexByPin(myInputs[i].triggerOutputPin));
}
}
myInputs[i].lastTrigger_ts = millis();
} else {
// no trigger seen, if it was before, send a "released" message
if (myInputs[i].isTriggered) {
myInputs[i].isTriggered = false;
myInputs[i].lastTrigger_ts = millis();
Serial.print(F("Released pin ")); Serial.println(myInputs[i].pin);
sendMqttMessage(myInputs[i].topic, "release");
needToToggleLed = true;
}
}
}
}
// if a LED toggle has been flagged :
if ( needToToggleLed ) {
// Toggle LED state :
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
}
void checkWebRequest() {
// Wait for an incomming connection
EthernetClient client = server.available();
// Do we have a client?
if (!client) return;
Serial.println(F("New client on web ingress"));
// Read the request (we ignore the content in this example)
while (client.available()) client.read();
// Allocate a temporary JsonDocument
// Use arduinojson.org/v6/assistant to compute the capacity.
StaticJsonDocument<500> doc;
for (unsigned int pin = 0; pin < NUM_INPUTS; pin++) {
doc[myInputs[pin].topic] = myInputs[pin].isTriggered ? "triggered" : "released";
}
// Create the "digital" array
JsonArray digitalValues = doc.createNestedArray("digitalPin");
for (unsigned int pin = 0; pin < 14; pin++) {
// Read the digital input
int value = digitalRead(pin);
// Add the value at the end of the array
digitalValues.add(value);
}
Serial.print(F("Sending: "));
serializeJson(doc, Serial);
Serial.println();
// Write response headers
client.println(F("HTTP/1.0 200 OK"));
client.println(F("Content-Type: application/json"));
client.println(F("Connection: close"));
client.print(F("Content-Length: "));
// client.println(measureJsonPretty(doc));
client.println(measureJson(doc));
client.println();
// Write JSON document
//serializeJsonPretty(doc, client);
serializeJson(doc, client);
// Disconnect
client.stop();
}
void loop() {
// Client
if (!mqttClient.connected()) {
reconnect();
}
mqttClient.loop();
checkInputSignals();
checkWebRequest();
checkOutputAutoOff();
if (millis() > REBOOT_EVERY_ms) {
resetArduinoNow();
}
}