+++ title = “Acme Camera” description = “” date = 2026-10-13
[extra] reveal_theme = “black” reveal_transition = “slide” +++
Acme Camera
A Proposal
Agenda
- Architecture: hardware and software
- Performance metrics and targets
- Instrumentation and fleet management
- Risks, plan and questions
Note: Roughly 15 minutes each, questions welcome throughout Detail for every software component is in the appendix if we want to dig in
Product Overview
A wireless security system architecture supporting long operational life
Note: The pitch: months on a charge, a person-confirmed alert in seconds, and fleet data that proves both
–
Requirements
- Battery powered, months per charge
- Outdoor: weatherproof, −20 to 50 °C
- Person-confirmed alerts in seconds
- Night vision and a spotlight
- Setup from a phone, Wi-Fi to the home
- Optional solar
Note: Matches the example products: a standalone battery camera and a solar-topped variant Temperature range matters for the battery more than anything else, cold eats usable capacity
Assumptions
| Area | Assumption |
|---|---|
| Battery | ~6000 mAh Li-ion pack |
| External power | regulated 5 V, solar included |
| Activity | ~20 events/day, ~10 s clips |
| Network | 2.4 GHz Wi-Fi to a home AP |
| Recording | event clips, live view on demand |
| Backend | exists, we own the device side |
Note: These drive every number later in the deck, so happy to rerun the math if Acme’s are different Solar is just another 5 V source, the panel’s own controller handles MPPT No continuous recording, a battery camera can’t afford it
Hardware Design
Note: Two domains: an always-on MCU with the sensors, lights, PMIC and BLE / Thread radio, and a mostly-asleep SOC with the camera, storage and Wi-Fi The MCU decides when the SOC is worth waking
SOC + MCU
–
MCU
ultra-low power attached to motion sensors, lighting, power control, and the BLE / Thread radio
SOC
attached to camera/storage/Wi-Fi and spends most of its time in deep sleep
–
MCU Candidates
- nRF54L: BLE 6, Thread / Matter
- nRF52840: BLE 5, Thread / Matter
SOC Candidates
- Rockchip RV1106
- Ingenic T41
- Ambarella CV7
Note: Radio split: Wi-Fi lives on the SOC for video, BLE and Thread live on the MCU None of the SOCs have Wi-Fi on-die, so it’s an SDIO module next to the SOC BLE on the MCU handles setup from the phone app without waking a soft-AP Thread / Matter lets the camera show up as a motion sensor or light in a smart home while the SOC sleeps Neither MCU has Wi-Fi; the nRF54L can host an nRF7002, but putting video through the MCU is the wrong path
Motion
Configurable
PIR
plus
MMWave
Note: Power Efficient Setup, PIR is used to wake the SOC and then mmwave is used to determine if people are present
LED Driver
to support Infrared and Visible Spots for lighting areas for improved visibility
Environmental
- Temperature
- Luminosity
Power Subsystem
- Battery Charge Controller & Gauge
–
What about solar?
Let’s set the expectation that any input power source provides regulated 5V, solar included
–
Solar Charge Controller
- TI bq25504
- e-peas AEM10941 / AEM13920:
Note: nPM1300 does not provide the MPP support for the solar cells so we need an external IC to properly do it
Software Design
Note: mcuctl sets the motion and light mode on the embedded mcu powerctl controls the power domain for the battery charger and reads the fuel gauge portalctl provides the initial configuration UI and subsequent status web portals cameractl reads frames and dispatches them to whatever has been configured configctl is the source of truth that gets configuration requests broker: mqtt, likely mosquitto infctl: inference controller for on-chip npus to load models (bonus)
Build & OS
- Yocto on the SOC vendor’s BSP
- Read-only rootfs in A/B slots, writable
/data - Signed images, built reproducibly in CI
- Rust daemons as sandboxed systemd units
Note: Yocto for layers, an SDK for app developers, and license manifests / SBOM for free; Buildroot if the chosen vendor only supports that Kernel trimmed to what the board needs, suspend-to-RAM tuned so a wake is a resume, not a boot Read-only rootfs means an update is a whole-slot swap, nothing drifts between devices /data holds config, credentials and the clip buffer, and is the only thing a factory reset wipes
–
Process Model
| Unit | Can reach |
|---|---|
| broker | /run/broker.sock |
| mcuctl | UART to the MCU |
| powerctl | PMIC over I²C |
| cameractl | sensor, ISP, clip buffer |
| portalctl | BLE via mcuctl, HTTP |
| configctl | /data/config |
| telemetryctl | /data/telemetry, uplink |
Note: Each unit runs as its own user with ProtectSystem=strict and DeviceAllow for only its hardware The broker checks the connecting uid (SO_PEERCRED) against per-topic ACLs, so portalctl can request config but never write it A crashed daemon is restarted by systemd and counted in telemetry
–
Boundaries
| Boundary | Transport |
|---|---|
| process ↔ process | MQTT on a Unix socket |
| SOC ↔ MCU | UART + wake GPIO |
| device ↔ cloud | MQTT 5 over mutual TLS |
| clips → cloud | HTTPS to object storage |
Note: The MCU raises a GPIO to wake the SOC, then they talk COBS-framed messages over UART from one shared schema, so both sides agree on MotionDetectionMode and friends mcuctl is the only process that touches the UART, everything else goes through the broker Video never goes over MQTT, cameractl gets a pre-signed URL and uploads clips directly
Boot Plan
Note: Uses an A+B boot that only changes the boot target once we’ve booted successfully and connected to the backend
Security
Trust from boot to cloud
Note: Home security buyers ask about this first, and a compromised camera is a camera pointed into someone’s home
–
Layers
| Layer | Measure |
|---|---|
| Boot | secure boot, signed A/B images |
| Keys | secure element / OTP, never in rootfs |
| Storage | encrypted /data and clips |
| Processes | per-service users, sandboxed units |
| Broker | per-topic ACLs by peer uid |
| Setup | BLE pairing, soft-AP as fallback |
| Cloud | per-device certs, mutual TLS |
Note: The claim token from setup is exchanged for a per-device cert, nothing shared across the fleet Telemetry carries metrics only, no video and nothing personal beyond the display name Factory reset wipes /data and revokes the device cert server side
Device ↔ Cloud
One connection, held only while awake
Note: The device is asleep most of the time, so the cloud is designed around a device that is usually unreachable
–
Connection
| Concern | Approach |
|---|---|
| Identity | per-device cert from the claim |
| Transport | MQTT 5 over mutual TLS |
| Reconnects | TLS session resumption |
| Commands | queued until the next wake |
| Config | device shadow, desired vs reported |
| Offline | buffer locally, send on reconnect |
Note: Session resumption skips most of the TLS handshake on every wake, which is time and energy on the event path A persistent MQTT session holds commands for a sleeping device, it picks them up on its next wake or heartbeat Live view needs the device reachable while asleep, which means Wi-Fi power-save on the SOC module or the keep-alive radio from the trade-offs, both cost standby current
–
Cloud Topics
| Topic | Direction | Carries |
|---|---|---|
d/<id>/event |
up | motion, detections |
d/<id>/telemetry |
up | batched metrics |
d/<id>/shadow |
both | desired / reported config |
d/<id>/cmd |
down | live view, reboot, logs |
d/<id>/ota |
down | update offers |
Note: The device can only publish and subscribe under its own id, enforced by the cert The shadow is the bridge to configctl: the cloud writes desired, configctl applies it and reports back what it actually runs
Components
| Component | Role |
|---|---|
| broker | MQTT on a Unix socket |
| mcuctl | motion, lights, BLE via the MCU |
| powerctl | power modes, PMIC telemetry |
| portalctl | setup and status portals |
| cameractl | frames to storage and cloud |
| configctl | source of truth for settings |
| infctl | on-device inference (bonus) |
| telemetryctl | buffers and uploads metrics |
Note: Details and types for each are in the appendix, the links jump there
Anatomy of an Event
| Step | t |
|---|---|
| PIR trips, MCU wakes the SOC | 0 |
| SOC resumes from suspend | ~200 ms |
| Radar confirms a person | ~300 ms |
| First frame from the sensor | ~700 ms |
| Wi-Fi reconnects | ~1.5 s |
| Notification sent | ~2 s |
| Clip uploaded, back to sleep | ~15 s |
Note: The SOC resumes in parallel with the radar check, if the radar doesn’t confirm it goes straight back to sleep Capture starts before the network is up, frames go to the local clip buffer first, so the first frame isn’t waiting on Wi-Fi ~15 s at ~400 mA is the ~1.5 mAh per event the power budget uses Keeping Wi-Fi associated while asleep would cut 1.5 s but costs standby current, a config trade-off
Metrics
- Performance within the configured power envelope
leading to insights for better defaults
- Battery life across the fleet
Note: Every config choice (motion mode, light mode, schedules) trades responsiveness for battery, so the metrics are about how well a device does inside the envelope its owner picked, and fleet data tells us which defaults to ship
–
Targets
| Metric | Target |
|---|---|
| Battery life, default config | ≥ 3 months |
| Standby draw, SOC asleep | ≤ 100 µA |
| Motion → first frame | < 1 s p95 |
| Motion → notification | < 5 s p95 |
| False wakes (no person) | < 20% |
| Crash-free wakes | > 99.9% |
| OTA success, first try | > 99.5% |
Note: Assumptions: ~6000 mAh pack, 20 events/day, 10 s clips, ~400 mA while the SOC, Wi-Fi and IR are up Each event costs roughly 1.5 mAh, so events dominate and standby has to stay tiny Wake latency is the user-visible one: a late first frame means a clip of someone’s back False wakes are PIR triggers the radar doesn’t confirm, they cost battery for nothing A/B boot means a failed OTA rolls back rather than bricks, the target is about not needing to
–
Power Budget
| Item | mAh/day |
|---|---|
| Standby, 100 µA × 24 h | 2.4 |
| Events, 20 × 1.5 mAh | 30 |
| Heartbeat + telemetry | ~0.5 |
| Self-discharge, ~2%/month | ~4 |
| Total | ~37 |
| 6000 mAh × 80% usable | ~130 days |
Note: Events are ~80% of the budget, so the false wake target and clip length matter more than standby 40 events/day halves it to ~65 days, cold weather takes another chunk off usable capacity This is the table the fleet data should confirm or correct
–
Slicing the Fleet
- Firmware version
- Motion / light / power mode
- Battery vs solar vs external
- Temperature and Wi-Fi signal
Note: Averages hide problems: a battery life drop on one firmware, or in cold climates, or on weak Wi-Fi These are the dimensions every metric gets broken down by Firmware cohorts gate staged OTA rollouts: a regression in one cohort halts the rollout
Instrumentation
What we measure, how it leaves the device, and how often
Note: Combines environmental and fuel gauge data with what each component already knows
–
Signals
| Source | Signals |
|---|---|
| powerctl | voltage, current, charge |
| mcuctl | wake reason, radar confirm |
| cameractl | wake → frame latency |
| network | connect time, RSSI |
| system | fw, boot slot, crashes |
| environment | temperature, light |
Note: powerctl also reports time to empty, mcuctl how long the lights were on, cameractl clip length, network upload time, system the reboot reason Each component publishes its own metrics to the broker, so nothing new has to reach into another process Energy per wake comes from the fuel gauge before and after, which ties every event to its battery cost
–
Collection
- Components publish metrics to the broker
- telemetryctl aggregates and buffers to flash
- MCU logs hourly samples while the SOC sleeps
- Uploads ride along when Wi-Fi is already up
Note: Never wake the radio just to report, telemetry should be well under 1% of the energy budget The MCU keeps a small ring buffer of sleep samples (battery, temperature, wake counts) that the SOC drains on its next wake Buffered to flash so a dead battery or lost network doesn’t lose history Upload is MQTT over TLS to the backend, the same connection the device already holds for events
–
Reporting Cadence
| When | What |
|---|---|
| Every event | wake summary: reason, latency, energy |
| Daily heartbeat | counters, battery state, sleep samples |
| After a crash or rollback | coredump and reboot reason |
| On demand | debug logs, per device |
Note: The heartbeat goes out even with no events, so a silent device is a signal on its own Coredumps are symbolicated server side, the device only ships the raw dump On demand logs are for support cases, off by default to save power and bandwidth
–
Fleet Dashboards
- Projected battery life per device vs target
- Latency and false wake rate per cohort
- Alerts: projected life below target, missed heartbeat, crash spikes after OTA
Note: Projected life comes from the measured drain rate, so we can flag a device months before it dies Missed heartbeat for 48 h means dead, offline or stuck, worth an owner notification A crash spike in a new firmware cohort pauses the rollout automatically
Fleet Management
Identity, config and updates for every device
Note: Two audiences use the same tools: Acme operating the whole fleet, and owners managing their own cameras
–
Lifecycle
| Stage | What happens |
|---|---|
| Factory | keys in the secure element, cert registered |
| Claim | owner’s app links the device to an account |
| Operate | shadow config, telemetry, alerts |
| Update | staged OTA by cohort |
| Retire | reset wipes /data, cert revoked |
Note: The claim token is the QR code on the device, it’s useless without the device cert behind it A resold camera goes through Retire then Claim, so the new owner never sees the old owner’s data
–
Bulk Configuration
- Owners group cameras by site or area
- Profiles: motion, light, power, processing
- Apply a profile to a group, override per camera
- Each camera reports when it has applied
Note: A profile write sets the desired shadow on every camera in the group, each one picks it up on its next wake or heartbeat configctl validates it like any other change, so a profile can’t push a camera into a state it doesn’t support The app shows pending / applied / rejected per camera, because sleeping devices apply changes over minutes, not instantly Same mechanism Acme uses at fleet scale, e.g. changing a default for a firmware cohort
–
OTA Rollout
| Stage | Devices | Gate |
|---|---|---|
| Internal | ~50 dogfood | a clean week |
| Canary | 1% | crashes, drain vs control |
| Staged | 10% → 50% | same, per cohort |
| Full | 100% |
Note: Any cohort regressing on crash rate or battery drain pauses the rollout automatically Devices only download above a battery threshold or while charging, and delta updates keep the bytes down A/B slots mean a bad update rolls back on its own, the rollout gates are about not needing that
Risks
| Risk | Mitigation |
|---|---|
| SOC vendor BSP quality | evaluate two SOCs early |
| Battery life misses target | measure from the first prototype |
| Wake latency | resume, don’t boot |
| Weak Wi-Fi | local clip buffer, RSSI telemetry |
| RF certification | pre-certified Wi-Fi module |
| Fleet regressions | staged OTA, A/B rollback |
Note: BSP quality is the biggest schedule risk for camera SOCs, kernel age and ISP blobs vary a lot between vendors Battery life is the product, so it’s measured on every build, not at the end
–
Trade-offs
| Choice | Gains | Costs |
|---|---|---|
| PIR wakes radar | fewer false alerts | ~100 ms |
| Radar only | catches still people | standby draw |
| Lights on motion | color night video | battery |
| Longer clips | more context | battery, upload |
| Keep-alive Wi-Fi radio | instant wake | BOM, 2nd radio |
| On-device inference | smarter alerts | NPU power, BOM |
Note: Every row is a battery-life knob, which is why the metrics are per configuration The defaults are a product decision, fleet data tells us whether they were the right ones
–
Delivery Plan
| Phase | Outcome |
|---|---|
| EVT, ~2 mo | dev kits, BSP up, power measured |
| DVT, ~3 mo | custom board, all daemons, OTA |
| PVT, ~3 mo | certification, ~100 device pilot |
| Launch | staged rollout, dashboards live |
Note: Rough durations, mostly gated by the board spins and certification Telemetry ships in EVT firmware, the pilot fleet is where the targets get validated
Summary
- The MCU decides when the SOC wakes
- PIR + radar: wake for people only
- Yocto, signed A/B, sandboxed daemons
- ≥ 3 months, < 5 s to alert
- Telemetry from day one gates every OTA
Note: If Acme remembers one thing: battery life is designed in, measured on every build, and proven across the fleet
Questions?
Appendix
Component details and types
Component : Broker
- MQTT (mosquitto) on a Unix socket
Note: on unix socket for the ability to have granular security No TCP listener at all, so nothing on the network can reach it The socket’s file permissions decide who can connect, and the peer uid (SO_PEERCRED) maps each connection to a component without passwords or certs A small auth plugin applies per-topic ACLs to that identity
–
Topics
| Topic | Publisher | Subscribers |
|---|---|---|
motion/event |
mcuctl | cameractl, powerctl |
power/reading |
powerctl | portalctl, telemetryctl |
config/request |
portalctl | configctl |
config/<area> |
configctl | everyone |
camera/clip |
cameractl | telemetryctl |
metrics/<unit> |
everyone | telemetryctl |
Note: config/ is retained, so a restarted component gets the current config the moment it subscribes Only configctl can publish config, only portalctl can request changes, the ACLs enforce the arrows in this table
Component : MCUctl
Owns the MCU link: motion, lighting and the BLE radio
Note: Talks to MCU and sets/gets the motion detection and light settings
–
Motion
Which sensors watch for motion
enum MotionDetectionMode {
OnlyPIR,
PIRWakesRadar,
OnlyRadar,
Off
}
–
Lighting
When the visible and IR spots come on
enum LightControlMode {
DuskDawn,
Motion{cooldown: u32},
Off
}
Component: Powerctl
Power modes, schedules and PMIC telemetry
Note: Collects power usage data, rundown and fuel gauge from the PMIC
–
Power Modes
Awake on detection, or on a schedule
enum PowerCtlMode {
Detection{cooldown: u32},
Timer{configs: Vec<TimerConfig>},
}
struct TimerConfig {
start_time: u64,
end_time: u64,
schedule_days: HashSet<Weekday>
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Weekday { M, T, W, R, F, S, U }
–
Power Readings
One sample from the PMIC
enum PowerSource { Battery, External }
struct PowerReading {
source: PowerSource,
timestamp: u64,
voltage_mv: u32,
current_ma: i32, // + charge / - drain
state_of_charge: Option<u8>, // fuel gauge, %
time_to_empty_s: Option<u32>, // rundown estimate
}
Component: Portalctl
First-boot setup, then a status portal
Note: Interactive wireless authentication and cloud configuration
–
Modes
Which portal is up, and how setup arrives
enum SetupTransport {
Ble,
SoftAp { ssid: String, psk: String },
}
enum PortalMode {
Setup { via: SetupTransport },
Status,
Off,
}
Note: Setup runs on first boot / factory reset, over BLE through mcuctl from the phone app, soft-AP only as a fallback without the app Status once on the home network, Off once cloud-managed
–
Provisioning
How far setup has got
enum ProvisioningState {
AwaitingCredentials,
JoiningNetwork,
Claiming,
Provisioned,
Failed { reason: String },
}
Note: The phone app polls this over BLE, or the soft-AP page does, to show progress and failures
–
Credentials
What setup collects from the user
enum WifiSecurity { Open, Wpa2Psk, Wpa3Sae }
struct WifiCredentials {
ssid: String,
security: WifiSecurity,
passphrase: Option<String>,
}
struct CloudClaim {
claim_token: String, // QR / app
display_name: String,
}
Note: claim_token comes from a QR code or the app and is exchanged for device certs
–
Config Requests
Changes sent to configctl via the broker
enum ConfigRequest {
Network(WifiCredentials),
Cloud(CloudClaim),
DisplayName(String),
}
Note: portalctl publishes these over the broker; configctl validates and applies them
–
Status Portal
The status portal’s read-only view
enum BootSlot { A, B }
struct DeviceStatus {
display_name: String,
firmware_slot: BootSlot, // A+B boot
state_of_charge: Option<u8>, // powerctl
motion_mode: MotionDetectionMode, // mcuctl
cloud_connected: bool,
}
Note: Read-only view assembled from the other components via the broker
Component: Cameractl
Frames to the clip buffer, the cloud and inference
Note: Reads frames from the camera and decides to do further processing depending on config
–
Capture
What starts a recording, and how much
enum CaptureTrigger { Motion, LiveView, Timer }
enum Resolution { P720, P1080, P1440 }
struct ClipPolicy {
resolution: Resolution,
max_len_s: u16,
extend_on_motion: bool,
}
Note: extend_on_motion keeps recording while the radar still sees someone, up to max_len_s
–
Clips
Where frames go
enum Sink { ClipBuffer, Cloud, Inference }
struct Clip {
id: u64,
trigger: CaptureTrigger,
started_at: u64,
len_ms: u32,
uploaded: bool,
}
Note: Every clip lands in the local buffer first, then uploads, so a weak or missing network never loses one
Component: Configctl
Source of truth for settings
Note: Source of truth for various configurations set from the portal
- Network configuration
- Display Name
- Cloud Configuration
- On-Device Processing Configuration
–
Device Config
One versioned document
struct DeviceConfig {
version: u32,
display_name: String,
network: WifiCredentials,
cloud: CloudConfig,
motion: MotionDetectionMode,
light: LightControlMode,
power: PowerCtlMode,
processing: ProcessingConfig,
}
Note: Written atomically to /data/config, then published retained on config/ The cloud can push changes too, they go through the same validation as the portal’s
–
Cloud & Processing
struct CloudConfig {
endpoint: String,
device_id: String,
}
enum ProcessingConfig {
Off,
Detect {
targets: Vec<InferenceTarget>,
threshold: f32,
},
}
Note: ProcessingConfig is what infctl reads to decide which models to run
–
Applying Changes
Every request gets an answer
enum ConfigResult {
Applied { version: u32 },
Rejected { reason: String },
}
Note: Every ConfigRequest gets a ConfigResult back, so the portal can show why a change was refused
–
Component: Infctl
On-device inference on the SOC’s NPU
Note: This one is – because it’s a bonus one that we may not get into Configures on-device inference using NPU hardware available in modern camera SOCs
–
Models
struct Model {
name: String,
version: String,
input: (u16, u16),
}
enum InferenceTarget {
Person, Vehicle, Package, Animal,
}
Note: Models ship with the OS image so they’re signed and A/B like everything else
–
Detections
struct Detection {
target: InferenceTarget,
confidence: f32,
bbox: (u16, u16, u16, u16),
}
Note: A detection above the threshold upgrades a motion event into a person / vehicle / package alert
How I Used AI
| Where | How |
|---|---|
| Slides | drafted text, tables and notes |
| Types | sketched the Rust types per component |
| Review | checked the deck against the brief |
| Tooling | built and deployed the deck site |
Note: Draft, edit to match what you actually did Tooling included the Diogramos embed frame mode and theme sync, the Forgejo CI and the Argo deploy
–
Practices
- AI drafts, I decide the architecture
- Numbers checked by hand: power math, part specs
- Small steps, reviewed and committed as I go
- Every slide rendered and checked
Note: Draft, edit to match how you actually work