Initial committ: Front door video controller
This commit is contained in:
41
README.md
Normal file
41
README.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Smart Video Doorbell Controller (Master)
|
||||||
|
|
||||||
|
**Hardware:** ESP32-S3-N16R8 (16MB Flash / 8MB PSRAM)
|
||||||
|
**Role:** Master Controller & Network Bridge.
|
||||||
|
|
||||||
|
## System Architecture
|
||||||
|
This device acts as the brain of the DIY Doorbell. It manages power states, handles peripherals, retrieves images from the Slave Camera via UART, and dispatches data to the Local Home Lab.
|
||||||
|
|
||||||
|
### Logic & State Machine
|
||||||
|
The device utilizes **Deep Sleep** to conserve battery, waking only on triggers:
|
||||||
|
|
||||||
|
| Trigger | Visual State | Action Sequence |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Motion (PIR)** | **RED** LED | Wake $\to$ Snap Photo $\to$ Upload to N8N $\to$ Send MQTT (Motion) $\to$ Sleep (60s) |
|
||||||
|
| **Button Press** | **BLUE** LED | Wake $\to$ Snap Photo $\to$ Upload to N8N $\to$ Send MQTT (Ring) $\to$ Sleep (120s) |
|
||||||
|
|
||||||
|
*Note: Button press has priority. If PIR wakes the device but the button is pressed, it switches immediately to Blue/Ring mode.*
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
### 1. Home Assistant (MQTT)
|
||||||
|
State updates are sent to \`front-door-cam/events\`.
|
||||||
|
* **PIR Event:** Sets Home Assistant lights to **RED** (Visual Warning).
|
||||||
|
* **Button Event:** Sets Home Assistant lights to **BLUE** (Doorbell Ring).
|
||||||
|
|
||||||
|
### 2. N8N Automation Pipeline
|
||||||
|
The ESP32 posts raw JPEG data to an N8N Webhook. The workflow executes:
|
||||||
|
1. **AI Vision:** Passes image to **Gemini Flash** to generate a text description of the visitor.
|
||||||
|
2. **Database:** Pushes the image and description to **NocoDB** for historical logging.
|
||||||
|
3. **TTS Announcement:** Sends the text description to Home Assistant.
|
||||||
|
4. **Audio Playback:** HA forwards text to a **Piper (Docker)** container for generation, which broadcasts via **Snapcast** speakers.
|
||||||
|
|
||||||
|
## Hardware Wiring
|
||||||
|
* **UART to Camera:** GPIO 17 (TX) / 18 (RX) $\to$ Slave Board.
|
||||||
|
* **PIR Sensor:** GPIO 13.
|
||||||
|
* **Button:** GPIO 14 (Active Low).
|
||||||
|
* **Status LED:** GPIO 12 (WS2812B).
|
||||||
|
|
||||||
|
## Todo / Roadmap
|
||||||
|
* [ ] Add I2S Microphone and Speaker for 2-way audio.
|
||||||
|
* [ ] Add Email notification with image attachment via N8N.
|
||||||
217
front_door_cam_control.ino
Normal file
217
front_door_cam_control.ino
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
#include <WiFi.h>
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include <Adafruit_NeoPixel.h>
|
||||||
|
#include <PubSubClient.h>
|
||||||
|
#include "driver/rtc_io.h"
|
||||||
|
|
||||||
|
// --- CONFIG ---
|
||||||
|
const char* WIFI_SSID = "Aussie Broadband 8729";
|
||||||
|
const char* WIFI_PASS = "Ffdfmunfca";
|
||||||
|
// N8N HTTP IP
|
||||||
|
const char* N8N_URL = "http://192.168.20.13:5678/webhook/front-door-cam";
|
||||||
|
|
||||||
|
// MQTT
|
||||||
|
const char* MQTT_BROKER = "192.168.20.30";
|
||||||
|
const int MQTT_PORT = 1883;
|
||||||
|
const char* MQTT_USER = "mqtt-user";
|
||||||
|
const char* MQTT_PASS = "sam4jo";
|
||||||
|
const char* MQTT_TOPIC = "front-door-cam/events";
|
||||||
|
|
||||||
|
// --- TIMING ---
|
||||||
|
const long TIME_SLEEP_BUTTON = 120000;
|
||||||
|
const long TIME_SLEEP_PIR = 60000;
|
||||||
|
const long TIME_RETRIGGER = 10000;
|
||||||
|
|
||||||
|
// --- PINS ---
|
||||||
|
#define PIN_BTN 14
|
||||||
|
#define PIN_PIR 13
|
||||||
|
#define PIN_LED 12
|
||||||
|
#define PIN_RX_CAM 18
|
||||||
|
#define PIN_TX_CAM 17
|
||||||
|
|
||||||
|
Adafruit_NeoPixel strip(12, PIN_LED, NEO_GRB + NEO_KHZ800);
|
||||||
|
HardwareSerial CamSerial(1);
|
||||||
|
WiFiClient espClient;
|
||||||
|
PubSubClient mqtt(espClient);
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
uint32_t C_RED = strip.Color(255, 0, 0);
|
||||||
|
uint32_t C_BLUE = strip.Color(0, 0, 255);
|
||||||
|
uint32_t C_YELLOW = strip.Color(255, 200, 0);
|
||||||
|
uint32_t C_GREEN = strip.Color(0, 255, 0);
|
||||||
|
uint32_t C_OFF = 0;
|
||||||
|
|
||||||
|
void setColor(uint32_t c) { strip.fill(c); strip.show(); }
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
|
||||||
|
CamSerial.begin(115200, SERIAL_8N1, PIN_RX_CAM, PIN_TX_CAM);
|
||||||
|
pinMode(PIN_BTN, INPUT_PULLUP);
|
||||||
|
pinMode(PIN_PIR, INPUT_PULLDOWN);
|
||||||
|
strip.begin(); strip.setBrightness(50);
|
||||||
|
|
||||||
|
Serial.println("Connecting WiFi...");
|
||||||
|
WiFi.setTxPower(WIFI_POWER_19_5dBm);
|
||||||
|
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||||
|
|
||||||
|
int t=0;
|
||||||
|
while(WiFi.status() != WL_CONNECTED && t < 16) { delay(500); t++; }
|
||||||
|
if(WiFi.status() == WL_CONNECTED) Serial.println("Connected.");
|
||||||
|
else Serial.println("WiFi Failed.");
|
||||||
|
|
||||||
|
// Check Wakeup
|
||||||
|
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
|
||||||
|
bool btnPressed = (digitalRead(PIN_BTN) == LOW);
|
||||||
|
bool pirTrigger = (digitalRead(PIN_PIR) == HIGH);
|
||||||
|
|
||||||
|
if (cause == ESP_SLEEP_WAKEUP_EXT0 || btnPressed) {
|
||||||
|
handleTrigger("BUTTON");
|
||||||
|
}
|
||||||
|
else if (cause == ESP_SLEEP_WAKEUP_EXT1 || pirTrigger) {
|
||||||
|
handleTrigger("MOTION");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Serial.println("Fresh Boot.");
|
||||||
|
setColor(C_GREEN); delay(2000);
|
||||||
|
goToSleep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// Main loop handles Retrigger logic
|
||||||
|
if (digitalRead(PIN_BTN) == LOW) {
|
||||||
|
// Only re-trigger if logic is running
|
||||||
|
handleTrigger("BUTTON");
|
||||||
|
}
|
||||||
|
// If we fall through here, go to sleep
|
||||||
|
goToSleep();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main Logic Sequence
|
||||||
|
void handleTrigger(const char* type) {
|
||||||
|
Serial.printf("TRIGGER: %s\n", type);
|
||||||
|
|
||||||
|
// 1. Set Visual State
|
||||||
|
uint32_t stateColor = (strcmp(type, "BUTTON") == 0) ? C_BLUE : C_RED;
|
||||||
|
int maxAwakeTime = (strcmp(type, "BUTTON") == 0) ? TIME_SLEEP_BUTTON : TIME_SLEEP_PIR;
|
||||||
|
|
||||||
|
setColor(stateColor);
|
||||||
|
|
||||||
|
// 2. Send MQTT IMMEDIATELY (Before Image)
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
sendMQTT(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Wait 2s Pre-Snap
|
||||||
|
delay(2000);
|
||||||
|
|
||||||
|
// 4. Snap & Upload N8N
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
snapAndUploadN8N(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore Color
|
||||||
|
setColor(stateColor);
|
||||||
|
|
||||||
|
// 5. Cooldown Loop (Stay Awake)
|
||||||
|
unsigned long startLoop = millis();
|
||||||
|
while (millis() - startLoop < maxAwakeTime) {
|
||||||
|
|
||||||
|
// Check for BUTTON Retrigger
|
||||||
|
if (digitalRead(PIN_BTN) == LOW) {
|
||||||
|
// Debounce Check
|
||||||
|
static unsigned long lastBtnTime = 0;
|
||||||
|
if (millis() - lastBtnTime > TIME_RETRIGGER) {
|
||||||
|
Serial.println("Retriggered by Button!");
|
||||||
|
lastBtnTime = millis();
|
||||||
|
// Reset timers and switch to Button Mode
|
||||||
|
type = "BUTTON";
|
||||||
|
stateColor = C_BLUE;
|
||||||
|
maxAwakeTime = TIME_SLEEP_BUTTON;
|
||||||
|
setColor(C_BLUE);
|
||||||
|
startLoop = millis(); // Reset Sleep Timer
|
||||||
|
|
||||||
|
// REPEAT ACTION
|
||||||
|
sendMQTT("BUTTON");
|
||||||
|
delay(2000);
|
||||||
|
snapAndUploadN8N("BUTTON");
|
||||||
|
setColor(C_BLUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
goToSleep();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendMQTT(const char* eventType) {
|
||||||
|
mqtt.setServer(MQTT_BROKER, MQTT_PORT);
|
||||||
|
if (mqtt.connect("ESP32Doorbell", MQTT_USER, MQTT_PASS)) {
|
||||||
|
char payload[64];
|
||||||
|
// Simple Alert Payload
|
||||||
|
snprintf(payload, sizeof(payload), "{\"event\": \"%s\", \"status\": \"active\"}", eventType);
|
||||||
|
mqtt.publish(MQTT_TOPIC, payload);
|
||||||
|
mqtt.disconnect();
|
||||||
|
Serial.println("MQTT Sent.");
|
||||||
|
} else {
|
||||||
|
Serial.println("MQTT Failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void snapAndUploadN8N(const char* eventName) {
|
||||||
|
setColor(C_YELLOW);
|
||||||
|
|
||||||
|
while(CamSerial.available()) CamSerial.read();
|
||||||
|
CamSerial.write('S');
|
||||||
|
|
||||||
|
uint32_t start = millis();
|
||||||
|
while (CamSerial.available() < 4) {
|
||||||
|
if (millis() - start > 3000) {
|
||||||
|
Serial.println("Camera Timeout");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t imgLen = 0;
|
||||||
|
CamSerial.readBytes((char*)&imgLen, 4);
|
||||||
|
Serial.printf("Size: %d\n", imgLen);
|
||||||
|
|
||||||
|
if (imgLen > 300000) return;
|
||||||
|
|
||||||
|
uint8_t *fb = (uint8_t *)malloc(imgLen);
|
||||||
|
if (!fb) return;
|
||||||
|
|
||||||
|
uint32_t received = 0;
|
||||||
|
start = millis();
|
||||||
|
while (received < imgLen && (millis() - start < 5000)) {
|
||||||
|
if (CamSerial.available()) fb[received++] = CamSerial.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (received == imgLen) {
|
||||||
|
HTTPClient http;
|
||||||
|
if (http.begin(N8N_URL)) {
|
||||||
|
http.addHeader("Content-Type", "image/jpeg");
|
||||||
|
http.addHeader("X-Event-Type", eventName);
|
||||||
|
|
||||||
|
int code = http.POST(fb, imgLen);
|
||||||
|
Serial.printf("N8N Upload: %d\n", code);
|
||||||
|
http.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(fb);
|
||||||
|
}
|
||||||
|
|
||||||
|
void goToSleep() {
|
||||||
|
Serial.println("Sleep.");
|
||||||
|
setColor(C_OFF);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
rtc_gpio_pullup_en((gpio_num_t)PIN_BTN);
|
||||||
|
rtc_gpio_pulldown_dis((gpio_num_t)PIN_BTN);
|
||||||
|
|
||||||
|
esp_sleep_enable_ext0_wakeup((gpio_num_t)PIN_BTN, 0);
|
||||||
|
esp_sleep_enable_ext1_wakeup(1ULL << PIN_PIR, ESP_EXT1_WAKEUP_ANY_HIGH);
|
||||||
|
|
||||||
|
esp_deep_sleep_start();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user