Building a Context-Aware AI Doorbell: Surviving Hardware Gremlins and API Rate Limits
June 14, 2026
The premise was simple: build a smart doorbell that detects a visitor, reads the local temperature and humidity, and generates a dynamic, context-aware audio greeting using an LLM.
The reality was a crash course in hardware voltage mismatches, serial communication collisions, and the strict bottlenecks of cloud AI providers. Here’s a breakdown of the architecture, the failures, and how I eventually bypassed the cloud entirely by moving the AI inference to the edge.
Phase 1: The Hardware Gremlins (3.3V vs 5V Logic)
The initial setup attempted to run everything—an HC-SR04 ultrasonic sensor, a servo motor, a buzzer, LEDs, and a DHT11 temperature/humidity sensor—off a single Arduino Uno.
The DHT11 immediately threw read errors. However, isolating the sensor and testing it on an ESP32 worked perfectly. The root cause boiled down to logic levels and power delivery.
Why This Happens
The DHT11 uses a single-wire communication protocol that requires a specific pull-up resistor on the data line. The ESP32 runs at 3.3V logic with a 240MHz processor, which handled the pull-up resistor constraints smoothly. The Arduino Uno runs at 5V and 16MHz, and when powered via USB, the 5V rail can experience voltage sag under load—causing signal degradation on the breadboard that made DHT11 reads unreliable.
The ESP32 Pin Configuration
#define DHT_PIN4
#define DHTTYPEDHT11
DHT dht(DHT_PIN, DHTTYPE);
The DHT11 data line connects to GPIO 4 on the ESP32. This pin configuration matters because not all ESP32 pins can handle the DHT sensor’s timing requirements.
The Fix
Rather than fighting the Uno’s electrical limits, I pivoted to a dual-board architecture. The ESP32 became the dedicated environment node—its only job was to read the DHT11 and transmit the data to the Arduino.
Phase 2: Dual-Board Serial Architecture
The new architecture split responsibilities:
ESP32: Reads DHT11 every 2 seconds → packages as JSON → transmits via Serial2
Arduino Uno: Listens on Pin 5 → parses JSON → polls ultrasonic sensor → controls LEDs, buzzer, and servo
This introduced a new problem: USB collision.
The Hardware Serial Constraint
The Arduino Uno only has one hardware serial port (Pin 0/RX and Pin 1/TX), which was already needed to send the final JSON payload up the USB cable to my Mac. I couldn’t use hardware serial for ESP32 communication without losing the ability to talk to Python.
The Solution: SoftwareSerial
I wired the ESP32’s TX2 pin (GPIO 17) to Arduino Pin 5, establishing a virtual serial port using the SoftwareSerial library:
ESP32 Pin Arduino Pin Purpose GPIO 17 (TX2) Pin 5 Environmental data in GND GND Shared ground
SoftwareSerial espSerial(5, 6); // RX on Pin 5, TX on Pin 6
espSerial.begin(9600);
The Data Flow
The ESP32 transmits:
{"temp":27.6,"hum":63.4}
The Arduino receives this, combines it with the ultrasonic distance reading, and forwards:
{"dist":32,"temp":27.6,"hum":63.4}
This is the exact JSON payload that Python parses on the host machine. The string positions are manually parsed in the Arduino firmware—no JSON library needed, which kept the sketch lightweight.
Phase 3: The Cloud AI Bottleneck
With the hardware rock-solid, a Python script on the host machine listened to the serial port, parsed the JSON, and forwarded the data to an LLM to generate the greeting text before pushing it through pyttsx3 (text-to-speech).
This is where relying on free cloud AI tiers completely broke down for hardware triggers.
Failure 1: OpenRouter (401 → 429)
I first tried OpenRouter’s free tier. The initial error was straightforward: a missing environment variable returned a 401 Unauthorized. After fixing that, the system immediately hit a 429 Too Many Requests error.
Here’s why: free API tiers have aggressive rate limits. Waving a hand in front of an ultrasonic sensor to test a hardware loop will instantly flag you for “spamming” the endpoint. There’s no way to distinguish between a legitimate rapid-fire request and a sensor-triggered hardware loop. The cloud provider sees only API calls, not the physical cause.
Failure 2: Hugging Face (400)
I migrated to Hugging Face’s Serverless Inference API to access open-source models, pointing to meta-llama/Llama-3.2-3B-Instruct. The API returned a 400 Bad Request.
The root cause: Meta’s models are license-gated, meaning API calls fail unless you’ve manually authenticated via the web UI first. It’s an open-source model you can’t actually access programmatically on a free tier. The irony was not lost on me.
The Pattern
Cloud latency and rate limits made real-time hardware testing impossible. Every failed API call meant a silent doorbell—visitors got no greeting while I debugged HTTP error codes. The cloud was fundamentally incompatible with the instant-response requirements of a physical hardware trigger.

Phase 4: Going Local with Ollama
The final and most robust solution was to sever the cloud dependency entirely and run the LLM locally on the host machine using Ollama.
First Attempt: OpenAI-Compatible Endpoint
I initially tested qwen3:0.6b via Ollama’s OpenAI-compatible endpoint (/v1/chat/completions). The Python script connected, but the model choked on the system prompt structure. It returned HTTP 200 success codes with entirely blank string payloads.
The model was technically “working” but producing nothing useful—a silent failure that’s harder to debug than an error code.
The Final Solution: Native API + the Right Model
I rewrote the Python integration to use Ollama’s native /api/chat endpoint and swapped the model to Meta’s llama3.2:1b, which is exceptionally fast and handled the prompt structure correctly.
resp = session.post(
"<http://localhost:11434/api/chat>",
json={
"model": "llama3.2:1b",
"messages": [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": prompt}
],
"stream": False
},
timeout=30
)
Why This Works
Native endpoint (
/api/chatvs/v1/chat/completions): The response structure is simpler and more predictable for Ollama-specific modelsllama3.2:1b: A 1-billion parameter model is small enough for fast inference on consumer hardware while still being capable enough for short greeting generation30-second timeout: Local models suffer from “cold starts.” The host machine needs time to load the model into memory on the very first trigger. Once loaded, the model generates greetings in milliseconds for all subsequent triggers

The Final Architecture
Here’s the complete data pipeline:
DHT11 (GPIO 4) → ESP32 → Serial2 (TX2/GPIO 17)
↓
Arduino Pin 5 (SoftwareSerial)
↓
Ultrasonic Sensor → Arduino (pins 2, 3)
↓
Physical: LEDs, Buzzer, Servo → USB Serial → Python
↓
Ollama (localhost:11434/api/chat)
↓
pyttsx3 → Computer Speakers
What Works Now
Detection threshold: 50cm
Cooldown: 8 seconds between triggers (prevents retriggering while someone’s still at the door)
Greeting generation: ~milliseconds after the first cold start
All processing local: No cloud API keys, no rate limits, no external dependencies
Conclusion: What I Learned
Hardware Lessons
Separate sensor logic based on voltage requirements. The DHT11 isn’t inherently incompatible with 5V Arduino, but the USB-powered setup created marginal signal conditions that failed reliably. Switching to a 3.3V ESP32 with stable power removed the variable.
SoftwareSerial saves you. When you need more serial ports, a software-defined virtual port on any digital pin is the practical solution—no hardware redesign required.
Dual-board architecture isn’t overkill—it’s practical. Splitting environmental sensing from doorbell control allowed each board to focus on what it does well.
AI Integration Lessons
Cloud APIs for instant physical triggers are a liability. Rate limits break real-time hardware loops in ways that no amount of retry logic can solve.
Local inference is the only reliable path for responsive IoT. Ollama running on a laptop or desktop is fast enough for sub-second response once the model is loaded.
The edge AI era is here. Running a capable LLM locally no longer requires a GPU. The
llama3.2:1bmodel runs on integrated graphics or basic CPU setups.
The project is complete and fully functional. The biggest takeaway? When building edge IoT devices, separating sensor logic based on voltage requirements saves hours of debugging, and relying on cloud APIs for instant physical triggers is a massive liability. Local inference is the only reliable way forward for responsive hardware.
The gremlins are gone. The doorbell works.
Github link: https://github.com/geek-commits/Doorbell-Ai
Explainer videos: https://drive.google.com/file/d/1sW3LZ4CEJXW4jqONuufKxoZmb2wp7g9E/view?usp=sharing
https://drive.google.com/file/d/1t7QgkKrN52Y2iLekizW4-IxhtS1gMRat/view?usp=sharing
