Firmware Architecture
An exhaustive, line-by-line breakdown of the EdgeWake C++ codebase running on the ESP32-CAM.
1. Configuration [config.h]
Acts as the central dictionary for all hardware pins and credentials.
#define WIFI_SSID "YOUR_WIFI_SSID"
#define TELEGRAM_BOT_TOKEN "YOUR_BOT_TOKEN"
#define TELEGRAM_CHAT_ID "YOUR_CHAT_ID"
Defines macros for credentials. The preprocessor replaces these throughout the code, keeping sensitive data in one file.
#define FLAME_SENSOR_PIN GPIO_NUM_13
#define VIBRATION_SENSOR_PIN GPIO_NUM_33
Assigns sensors to specific pins. GPIO 13 and 33 are strictly chosen because they are RTC (Real-Time Clock) GPIOs, which are the only pins capable of waking the ESP32 from Deep Sleep.
2. Main Logic [EdgeWake_Main.ino]
The primary state machine executing the 5-Tier cascade.
RTC_DATA_ATTR int bootCount = 0;
RTC_DATA_ATTR places the variable in RTC slow memory. Unlike standard RAM, RTC memory remains powered during Deep Sleep, allowing us to track total wakes.void setup() {
esp_sleep_wakeup_cause_t wakeReason = esp_sleep_get_wakeup_cause();
if (wakeReason != ESP_SLEEP_WAKEUP_EXT1) {
goToDeepSleep();
return;
}
setup() runs on every wake. We query esp_sleep_get_wakeup_cause(). If the cause is not EXT1 (the hardware interrupt mask for our sensors), it was a manual reset, so we immediately return to sleep to preserve battery. uint64_t wakeupPins = esp_sleep_get_ext1_wakeup_status();
String alertType = "Unknown";
if (wakeupPins & (1ULL << FLAME_SENSOR_PIN)) {
alertType = "Fire";
} else if (wakeupPins & (1ULL << VIBRATION_SENSOR_PIN)) {
alertType = "Vibration";
}
We identify the exact hardware trigger.
esp_sleep_get_ext1_wakeup_status() returns a 64-bit bitmask. We use a bitwise AND (&) against a left-shifted (<<) 1 to see which specific pin went HIGH. if (initMicrophone()) {
isRealThreat = verifyThreatAudio();
deinitMicrophone();
}
if (!isRealThreat) {
goToDeepSleep();
return;
}
Tier 3: We initialize I2S, run the AI audio check, and immediately de-initialize the microphone to free DMA channels. If the AI flags a false alarm, we sleep.
camera_fb_t *photo = NULL;
if (initCamera()) {
photo = capturePhoto();
}
Tier 4: The OV2640 camera is initialized.
camera_fb_t is a pointer to the Frame Buffer structure in PSRAM containing the JPEG payload. if (photo) {
String message = "🚨 EDGEWAKE ALERT 🚨\nDanger Detected: " + alertType;
sendAlertToTelegram(photo, message);
esp_camera_fb_return(photo);
}
Tier 5: We execute the Telegram POST request. Crucially,
esp_camera_fb_return(photo) must be called immediately after transmission to free the PSRAM buffer and prevent catastrophic memory leaks on the next cycle.void goToDeepSleep() {
uint64_t wakeMask = (1ULL << FLAME_SENSOR_PIN) | (1ULL << VIBRATION_SENSOR_PIN);
esp_sleep_enable_ext1_wakeup(wakeMask, ESP_EXT1_WAKEUP_ANY_HIGH);
Configures the sleep state.
ESP_EXT1_WAKEUP_ANY_HIGH instructs the power management unit to wake the main CPU if any of the masked pins receive a high voltage signal. rtc_gpio_isolate((gpio_num_t)I2S_SD_PIN);
delay(5000);
esp_deep_sleep_start();
}
rtc_gpio_isolate prevents electrical leakage through the microphone pin. The delay(5000) is a mechanical debounce—it forces the CPU to wait 5 seconds, allowing analog sensor capacitors to drain before sleeping, preventing instant false re-triggers.3. Artificial Intelligence [audio_utils.h]
Handles I2S DMA memory and Edge Impulse CNN execution.
const i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = 16000,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
Configures the I2S peripheral to run in Master/Receive mode at exactly 16 kHz (the required sample rate for our trained Edge Impulse model).
int total_samples = 32000;
int16_t *audioBuffer = (int16_t *)ps_malloc(total_samples * sizeof(int16_t));
Calculates 2 seconds of audio (16,000 * 2). Allocates memory using
ps_malloc to force the allocation into external PSRAM, as the internal DRAM is too small for a 64KB array. for (int i = 0; i < total_samples; i += 512) {
i2s_read(I2S_PORT, &audioBuffer[i], 512 * sizeof(int16_t), &bytesRead, portMAX_DELAY);
}
Pulls raw audio from the I2S DMA buffer in 512-sample chunks using a blocking
portMAX_DELAY read until 2 seconds are recorded. signal_t signal;
numpy::signal_from_buffer(audioBuffer, total_samples, &signal);
ei_impulse_result_t result = { 0 };
run_classifier(&signal, &result, false);
Wraps the raw C-array into an Edge Impulse
signal_t structure, then passes it to run_classifier, which executes the 1D CNN inference engine. if (label == "fire") fireScore = score;
if (label == "chainsaw") chainsawScore = score;
Iterates through the model classification output. If the confidence score for "fire" or "chainsaw" exceeds 0.60 (60%), the code returns
true.4. Camera Utilities [camera_utils.h]
Manages OV2640 initialization and exposure logic.
config.pixel_format = PIXFORMAT_JPEG;
if (psramFound()) {
config.frame_size = FRAMESIZE_SVGA;
config.jpeg_quality = 10;
Sets the camera to encode JPEG in hardware. If PSRAM is detected, it utilizes SVGA resolution (800x600) with a high quality factor of 10 (lower is better).
digitalWrite(FLASH_LED_PIN, HIGH);
delay(150);
camera_fb_t *discard = esp_camera_fb_get();
if (discard) esp_camera_fb_return(discard);
camera_fb_t *fb = esp_camera_fb_get();
Critical Exposure Logic: The flash LED is turned on. We capture a frame and instantly discard it. The camera sensor auto-exposure mechanism requires one frame to adjust to the sudden flash brightness. The second frame (
fb) is perfectly exposed.5. Telegram Integration [telegram.h]
Handles RAM-safe multipart HTTP streaming.
WiFiClientSecure client;
client.setInsecure();
client.connect("api.telegram.org", 443);
Initializes a TLS/SSL connection to Telegram.
setInsecure() bypasses root certificate validation, drastically simplifying code maintenance while retaining encryption. String boundary = "----EdgeWakeBoundary123456";
Defines the boundary string required by the
multipart/form-data HTTP standard to separate the text payload from the binary image payload. client.print(head);
client.write(fb->buf, fb->len);
client.print(tail);
Memory Safety: Instead of constructing the entire HTTP request in a massive string (which would crash the ESP32 via heap fragmentation), we stream it directly to the socket. We print the headers, write the raw binary JPEG array, and print the tail.