Dec 02, 2025
7 min read

The 30-Second Boot: Debugging Serial Logs When Your Camera Only Wakes Briefly

When your device sleeps after 30 seconds, you need to capture logs non-interactively or you'll miss the critical errors.

The ESP32 camera boots, does its thing, and goes back to deep sleep. Total active time? About 30 seconds. If you’re lucky, maybe 40.

The problem is I can’t watch serial output in real time. By the time I open the monitor and connect to the serial port, the device is already asleep. I miss the boot logs, the error messages, everything that matters.

I needed a way to capture those 30 seconds of logs automatically, without human intervention.

The Interactive Monitor Problem

The normal workflow for ESP-IDF is:

idf.py monitor

This opens an interactive serial monitor. You see logs in real time, you can type commands, you can reset the device with Ctrl+T, Ctrl+R.

But it takes 5-10 seconds to start the monitor and establish the serial connection. The camera boots in 3 seconds, runs for 30 seconds, sleeps. By the time I’m connected, it’s too late.

Even if I flash the firmware with idf.py flash monitor (which launches the monitor immediately after flashing), I still miss critical logs. The flash operation resets the device, but there’s a delay before the monitor connects.

Non-Interactive Log Capture

The solution is to capture serial output non-interactively using a simple script:

#!/bin/bash
# monitor-log.sh - Capture 10 seconds of serial output

timeout 10s docker run --rm \
  --device=/dev/ttyUSB0 \
  -v "$PWD":/project \
  -w /project \
  espressif/idf \
  idf.py -p /dev/ttyUSB0 monitor > serial_output.log 2>&1

echo "Captured logs to serial_output.log"

This runs the monitor for exactly 10 seconds, captures all output to a file, and exits. No interaction needed. No delays. Just raw log capture.

I can run this immediately after flashing:

./scripts/flash.sh && ./scripts/monitor-log.sh

The && ensures the monitor starts as soon as flashing completes. The device resets automatically after flash, so the monitor is already listening when the bootloader starts running.

Parsing the Logs

The raw logs include ESP-IDF’s monitor wrapper output:

Executing action: monitor
Running idf_monitor in directory /project
--- idf_monitor on /dev/ttyUSB0 115200 ---
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
ets Jun  8 2016 00:22:57

rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)

To get just the device logs, grep out the monitor metadata:

grep -v "Executing action\|Running idf_monitor\|Quit: Ctrl" serial_output.log

Or parse it programmatically in Python:

with open('serial_output.log', 'r') as f:
    lines = f.readlines()

device_logs = [line for line in lines if not line.startswith('---')]

The actual device output starts with the ESP32 bootloader messages (starts with ets).

Timing Issues and Race Conditions

Even with automated capture, timing matters. If you start the monitor too late, you miss the bootloader. If you stop it too early, you miss the error that happens 35 seconds in.

I added a configurable duration to the monitor script:

#!/bin/bash
DURATION=${1:-10}  # Default 10 seconds, override with argument

timeout ${DURATION}s docker run --rm \
  --device=/dev/ttyUSB0 \
  -v "$PWD":/project \
  -w /project \
  espressif/idf \
  idf.py -p /dev/ttyUSB0 monitor > serial_output.log 2>&1

Now I can capture longer sessions when needed:

./scripts/monitor-log.sh 60  # Capture 60 seconds

For the 30-second boot case, 40 seconds is a safe margin. The camera finishes its work and sleeps, but the monitor keeps running a bit longer to catch any late errors.

Filtering for Errors

Most of the boot log is noise. I care about errors and warnings:

# Extract only errors and warnings
grep -E "E \(|W \(" serial_output.log > errors.log

ESP-IDF log format is:

I (1234) tag: Info message
W (2345) tag: Warning message
E (3456) tag: Error message

The pattern E (|W ( matches both errors and warnings. This gives you a focused view of what went wrong.

What I Found

Using the automated log capture, I discovered several issues that I would have missed with interactive monitoring:

Missing WiFi credentials:

E (2456) wifi: WiFi connection failed after retries

I had set the credentials in menuconfig but forgot to rebuild. The old firmware was still trying to connect to the default SSID.

I2C timeout during init:

E (1234) i2c.master: I2C software timeout
E (1235) mpu9250: Failed to read WHO_AM_I

This led me to discover the GPIO conflict issue that eventually became the “Documentation Lied” blog post.

Camera PSRAM allocation failure:

E (3456) camera: Failed to allocate frame buffer

PSRAM wasn’t enabled in sdkconfig. Without the error log, I would have just seen “no photos uploaded” and not known why.

Makefile Integration

I added a make target for easy log capture:

# Makefile
monitor-log:
    @timeout 40s docker run --rm \
        --device=/dev/ttyUSB0 \
        -v "$(PWD)":/project \
        -w /project \
        espressif/idf \
        idf.py -p /dev/ttyUSB0 monitor 2>&1 | tee serial_$(shell date +%Y%m%d_%H%M%S).log

Now it’s just:

make monitor-log

This captures 40 seconds of output to a timestamped log file. Perfect for debugging intermittent issues.

The Deep Sleep Challenge

The real challenge with deep sleep devices is you can’t poke them while they’re asleep. You can’t send a command to wake up. You can’t trigger verbose logging on demand.

You have to capture everything during the brief active window. The logs have to be comprehensive enough to diagnose issues, but not so verbose they hide the important messages.

I tuned the ESP-IDF log levels to INFO for most components, DEBUG for components I was actively debugging:

// In menuconfig:
// Component config -> Log output -> Default log verbosity: Info

// For specific components:
esp_log_level_set("wifi", ESP_LOG_DEBUG);
esp_log_level_set("mpu9250", ESP_LOG_DEBUG);

This keeps boot logs readable while still capturing enough detail to debug failures.

Continuous Logging

For long-running debugging sessions, I sometimes need continuous logging across multiple wake cycles:

# Capture 5 minutes of logs (catches multiple wake/sleep cycles)
./scripts/monitor-log.sh 300 &

# Do whatever triggers the issue
# ...

# Logs are automatically saved when timeout expires

This is useful for catching rare bugs that only happen once every 10-20 wake cycles.

Lessons Learned

Non-interactive log capture is essential for battery-powered devices that sleep. You can’t rely on being there when it wakes.

Timestamp your log files. When debugging intermittent issues, you’ll have dozens of captures. Timestamps prevent confusion.

Filter for errors first. Most of the log is successful operations. Focus on failures.

Capture more time than you think you need. A 30-second active window needs at least 40 seconds of capture to be safe.

Keep a history of logs. Sometimes the bug shows up in logs from yesterday but you didn’t notice until today’s test revealed the pattern.

Use tee instead of > redirect if you want to see logs in real time while also saving to file.

References