---
title: "Lookout: battery cameras that tell me what they see"
slug: "lookout-battery-cameras-that-tell-me-what-they-see"
url: "https://haakony.no/posts/lookout-battery-cameras-that-tell-me-what-they-see"
date: "2026-10-27"
updated: "2026-10-27"
categories: ["projects", "development", "ai"]
tags: ["esp32", "firmware", "camera", "yolox", "mattermost", "3d-printing", "claude"]
reading_time_minutes: 8
ongoing: true
description: "A self-hosted camera system built on cheap ESP32 boards: battery cameras that wake on motion, a model that names what it sees, alerts in Mattermost and a little photo frame on my desk. It is still convinced the cat is a dog."
---

# Lookout: battery cameras that tell me what they see

I wanted to know who walks up to my front door without paying a monthly fee to a camera company and sending every frame to someone else's cloud. So in July I started Lookout: small ESP32 cameras, my own server, and a notification in our chat when something interesting shows up.

Three months and about 70 commits later, it actually works. Like my [game](<https://haakony.no/posts/making-small-games-with-ai>), almost all of the code was written by Claude Code while I held the soldering iron, mounted things on the wall and said what I wanted.

## It started on a breadboard

The first camera was a cheap AI-Thinker ESP32-CAM stuck in a breadboard with jumper wires going everywhere. It joined the WiFi, took a JPEG and posted it to a small Go backend. That was the whole thing.

![The first ESP32-CAM in a breadboard with a USB serial adapter and jumper wires](https://haakony.no/media/105){width=50%} ![The ESP32-CAM on the breadboard with a SanDisk SD adapter next to it](https://haakony.no/media/106){width=50%}

It was USB powered, which is fine for a desk but useless by a door with no socket. So the next step was batteries.

## Battery cameras

I moved to the LilyGO T-Camera S3. It has a small OLED screen, a PIR motion sensor and a proper power chip, and the PIR can wake the board from deep sleep. That is the trick: the camera sleeps at microamps, and only wakes, takes a picture and uploads it when something warm moves in front of it. Then it goes back to sleep.

The first battery version was an 18650 cell taped into a clear plastic box from the parts drawer. Not pretty, but it worked.

![The T-Camera S3 and an 18650 cell in a clear plastic box on the workbench](https://haakony.no/media/107){width=50%} ![Holding the plastic box with the camera and PIR dome visible through the lid](https://haakony.no/media/108){width=50%}

It went straight up on the wall by the entrance.

![The plastic box camera mounted on the white wooden wall outside](https://haakony.no/media/111){width=50%}

Getting a sleeping camera to behave took most of the work. Some of the fixes from those weeks:

- Pick the strongest WiFi access point, not the first one that answers.
- Remember the access point so the camera skips the scan next time it wakes. Every second awake costs battery.
- If WiFi is gone, sleep and retry later with a back-off instead of draining the battery trying.
- Cut power to the PIR when the camera is not armed.
- Measure the battery instead of guessing, and show an estimate in the app.
- Power-cycle the camera a few times to bring up the setup portal, so it can be moved to another network without opening the box.

New firmware is pushed over the air, so I don't have to take it down from the wall to update it.

## A proper case

The tape box got replaced by a printed case for two 18650 cells with a snap-fit lid. It is parametric, built from LilyGO's own drawing of the board, so the camera, screen, PIR, buttons and USB-C all line up with their holes. It took a few rounds of "the lid doesn't fit" and "the rails broke off" before it printed right.

![The black 3D-printed case with the PIR dome, camera lens and OLED screen showing through](https://haakony.no/media/112){width=50%}

## When WiFi doesn't reach

Some places I want a camera have bad WiFi. So the camera can also send its picture over ESP-NOW, a direct radio link between ESP32 boards, to a small base with a screen that sits somewhere WiFi is good. The base puts the picture back together, checks it and uploads it to Lookout like any other camera. The camera and base pair themselves on first boot and encrypt the link. By default the camera tries WiFi first and only uses the base when the normal upload fails.

![The Lookout base screen showing Camera linked, an 800x600 image of 20 KB and Image sent to Lookout, next to the T-Camera](https://haakony.no/media/110){width=50%}

## How it sees things

Every picture that arrives goes to a detector running next to the backend. It uses YOLOX-s, an open object detection model trained on the 80 everyday COCO classes: person, car, cat, dog, bird, and so on. It runs on the CPU and takes about 35 ms per picture, so there is no need for a GPU.

The flow looks like this:

1. The camera wakes on motion and uploads a JPEG.
2. The backend stores it and puts it in a queue for detection. The upload never waits for the model, so a slow detector can't make the camera stay awake longer.
3. A worker sends the picture to the detector and saves what it found, with a confidence for each one and a box around it.
4. If anything was found, the camera's notification rules are checked.

Cameras are often mounted sideways, and the model is trained on upright pictures. A person lying across the frame is mostly invisible to it. So pictures are turned upright before detection, and now before they are stored.

At first I used the smaller YOLOX-tiny model, but it mixed up cats and dogs. YOLOX-s gets them right. Mostly.

## The cat is a dog

This is our cat. Lookout is 73% sure it's a dog, and that the plant pot is a potted plant, which is at least correct.

![The camera picture in the app: the cat is boxed and labelled dog 73%, the plant pot is labelled potted plant 53%](https://haakony.no/media/109){width=75%}

It has also been quite sure the cat is a horse. I have come to like it. The labels are a guess, not the truth, and the funny ones are half the fun of checking the feed.

## Alerts in Mattermost

I already use Mattermost, so that is where the alerts go. In the app you make a rule per camera:

- which things should trigger it, for example `person` and `cat`
- how sure the model must be, 50% by default
- a cooldown, five minutes by default, so a cat sitting on the doorstep doesn't post 40 messages
- the Mattermost incoming webhook to post to

When a picture matches a rule, Lookout posts something like **"🔔 inngang detected: cat (88%)"** with the picture shown right in the channel and a link to the camera in the app.

Some details that took a couple of tries:

- **The picture shows in the chat.** The first version pasted a long object storage link with 400 characters of signature that you had to click. Now the backend serves the picture itself through a signed link, Mattermost can fetch it without logging in, and nothing about the storage is exposed.
- **No double alerts.** The cooldown is claimed in the database in one step, so two pictures arriving at the same time can't both send.
- **No lost alerts.** If Mattermost is down and the post fails, the cooldown is given back, so the next picture can try again instead of staying silent for five minutes.
- **Slow chat doesn't slow detection.** Posting happens on the side, so a slow webhook never holds up detection of new pictures.
- **Some safety.** The server refuses webhook addresses that point at itself, like localhost, so a rule can't be used to poke at the database or the storage behind it.

Mattermost is the only target for now, but it is built so others can be added later.

## A photo frame on the desk

The newest addition: a "Cheap Yellow Display", an ESP32 with a 2.8" touch screen. It shows the newest picture from any of my cameras, with the camera name, how long ago it was taken, and what Lookout thinks it saw in yellow. A new picture shows up about 20 seconds after it reaches Lookout.

![The photo frame showing the entrance camera with car 86% and 48 min ago](https://haakony.no/media/113){width=50%} ![The photo frame with its touch menu open: Older, Newest, Newer, Camera, Detections and Settings](https://haakony.no/media/114){width=50%}

Tap the screen and a menu comes up. You can step back through older pictures, pick a camera, and choose whether to show the detections as text, as boxes or not at all.

![The photo frame showing the cat by the door, labelled cat 88%, car 80%, bird 41%](https://haakony.no/media/115){width=75%}

Here it got the cat right, and the car parked across the road. The bird at 41% is anyone's guess.

## What's in it

- **Firmware** for the ESP32-CAM and the T-Camera S3, in C with PlatformIO: WiFi setup portal, motion wake, battery measurement and over-the-air updates
- **An ESP-NOW base** for cameras out of WiFi range
- **The photo frame**
- **A backend** in Go with MariaDB and S3 storage for pictures and firmware
- **The detector**, a small Python service with YOLOX-s on ONNX Runtime
- **A web and phone app** in SvelteKit, for the gallery, camera settings, rules and firmware updates
- **The printed case**, parametric so it can be changed for other battery holders

## What I learned

- Battery is the hard part. Taking a picture is easy. Taking one and being asleep again a few seconds later, every time, is where all the work went.
- Test on the real hardware. A camera on the desk plugged into USB behaves nothing like one on the wall on battery.
- Keep the camera dumb. It wakes, uploads and sleeps. Everything clever happens on the server, where it can be changed without climbing a ladder.
- Don't trust the labels too much. They are good enough to decide what is worth a ping, and that's all I need.

Next up is seeing how long the batteries actually last through a Norwegian winter.
