Initial committ: Front door video controller

This commit is contained in:
2025-12-02 17:12:01 +11:00
commit 283df791ab
2 changed files with 258 additions and 0 deletions

41
README.md Normal file
View 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
View 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();
}