A webcam + AprilTag + e-ink feedback loop for better posture at your desk.
Quick Start Β· How It Works Β· Features Β· Gallery Β· Calibration Β· CLI Toolbox
Posture Watcher is a small end-to-end posture feedback loop:
- A side-mounted webcam watches AprilTags on the ear, C7, shoulder, and optional hip.
- A Rust analyzer turns those tags into marker geometry, placement diagnostics, and a simple spine/head curve.
- A Badger2040 e-ink display, used in portrait orientation, shows the feedback where it is easy to glance at while working.
- A native macOS wrapper owns Camera permission, shows the same display as the Badger, and gives debugging controls when the hardware is not nearby.
Philosophy: The goal is not to nag on every frame. The app samples slowly, averages over a rolling window, and refuses to show a posture curve when the markers are visible but anatomically implausible.
| Live debug overlay | macOS app window |
|---|---|
![]() |
![]() |
| Badger in action | Wearing the tags |
|---|---|
![]() |
The wearing-tags placeholder is intentional β drop in a real photo after the next physical test.
| What | Details |
|---|---|
| Badger2040 | Connected over USB-C (default port: /dev/cu.usbmodem83201) |
| Logitech C930e | Mounted sideways for maximum vertical image height |
| AprilTags | Printed from the tag36h11 family |
# Build the Rust CLI
cargo build
# Install the Badger receiver (backs up existing main.py first)
cargo run -- install-badger
# Build and install the macOS app
POSTURE_WATCHER_INSTALL_DIR=/Applications scripts/install-macos-app.sh --openThe installer builds target/macos/Posture Watcher.app, copies it to /Applications, removes quarantine metadata, and opens it. macOS will prompt for Camera permission.
π Optional: Launch at login
POSTURE_WATCHER_INSTALL_DIR=/Applications scripts/install-macos-app.sh --launch-at-login
scripts/disable-launch-at-login.sh # to undocargo run -- stickers --openThis prints a sheet with four flat tags plus a foldable C7 side-facing flag template.
| Tag | Landmark | Placement |
|---|---|---|
tag36h11-0 |
π Ear / tragus | Small cartilage bump in front of ear canal |
tag36h11-1 |
𦴠C7 | Base of neck β use the foldable side-facing flag |
tag36h11-2 |
πͺ Shoulder / acromion | Bony shelf at the outside/top of the shoulder |
tag36h11-3 |
𦡠Hip / belt | (Optional) Side hip / waistband, helps with sit/stand detection |
Open /Applications/Posture Watcher.app and look for:
| Status | Meaning |
|---|---|
β
Badger connected |
E-ink receiver is ACKing payloads |
β
Tags ready |
Required tags are visible |
β
Placement good |
Marker geometry is plausible |
β
Baseline ready |
Sitting and standing have enough good samples |
Move ear tag up |
Tags visible but probably misplaced β fix before trusting the curve |
Use Save Sample whenever you have a useful sitting, standing, good, or bad setup. Samples go to:
~/Library/Application Support/Posture Watcher/samples/<mode>/
Each sample includes the raw frame, debug images, and a *-tags.txt report with marker coordinates, detected mode, placement score, and posture measurements.
The macOS app captures frames via AVFoundation and writes a rolling burst to disk. The Rust analyzer inspects recent frames, detects AprilTags, fuses the strongest observations, and pushes posture feedback to both the app window and the Badger display.
Camera β Burst Frames β AprilTag Detection β Marker Fusion β Rolling Average β Badger + App
Each update is based on fused AprilTag observations from multiple recent frames, so one blurry or occluded frame doesn't dominate.
| Setting | Default | Env Var |
|---|---|---|
| Update interval | 15s | POSTURE_WATCHER_INTERVAL_SECS |
| Burst frames | 8 | POSTURE_WATCHER_BURST_FRAMES |
| Frame interval | 0.25s | POSTURE_WATCHER_BURST_FRAME_INTERVAL_SECS |
The app auto-detects sitting/standing from shoulder-to-hip geometry when those tags are visible. Use the Mode picker to override or leave on Auto.
Each mode keeps its own rolling average window, so switching desk modes doesn't blend postures together.
Posture feedback only appears when markers are trustworthy:
| Condition | Display |
|---|---|
| No tags for extended period | No person found |
| Tags visible, geometry implausible | Move ear tag up / Aim C7 flag / Check markers |
| Tags visible, geometry good | Live posture curve β |
Short marker dropouts are counted but don't immediately replace the curve β sustained problems trigger a fix suggestion.
| Area | Feature | What It Does |
|---|---|---|
| Capture | Native macOS app | Owns Camera permission, camera selection, live capture, and the daily workflow |
| Sideways webcam | Defaults to 90Β° CCW rotation for the C930e mounted vertically | |
| Markers | Printable AprilTags | Generates ear, C7, shoulder, and hip tags from tag36h11 |
| C7 flag workflow | Foldable C7 flag template + anchor correction for side-facing neck marker | |
| Detection | Burst sampling | Checks several recent frames so one bad frame doesn't dominate |
| Marker fusion | Combines strongest observations across the burst before calculating posture | |
| Posture | Rolling average curve | Long-window posture trend instead of twitchy frame-by-frame warnings |
| Sitting/standing modes | Auto, Sitting, and Standing with separate rolling windows and baselines | |
| Calibration | Personal baselines | Builds sitting and standing baselines from your own saved good samples |
| Feedback | Baseline comparison | Live curve against a dashed calibrated baseline |
| Quality strip | Recent valid/missed samples as a compact strip | |
| Placement warnings | Actionable messages like Aim C7 flag or Move ear tag up |
|
| Hardware | Badger2040 display | Same posture feedback on a USB-connected e-ink display |
| USB orientation | Supports USB-C at top or bottom | |
| Debug | Labeled overlays | Debug images with labeled points in the macOS preview |
| Badger renderer | Renders exact Badger framebuffer without aiming a camera at the screen | |
| Sample capture | Saves raw frames, overlays, and tag reports | |
| Setup | Installable .app |
Installs to /Applications with optional launch-at-login |
| Badger backup/restore | Backs up main.py before installing; can restore later |
Calibration has three layers β do them in order.
Keep the camera boring and repeatable:
- Same camera, desk position, and side-view angle each day
- C930e sideways with
--rotate ccw90 - Frame the body so all four tags are visible without being tiny
- Don't move the camera between sitting and standing
Get the app to say Tags ready β then Placement good.
π·οΈ Detailed Marker Placement Guide
Place every marker on the camera-facing side of your body. This is a side-view tracker.
Put this tag at the tragus region β the small cartilage bump immediately in front of the ear canal. Keep hair, glasses arms, headphones, and mask straps from covering the tag.
C7 is the prominent bump at the base of the neck:
- Gently bend your head forward and feel for the most prominent lower-neck bump.
- Gently extend your neck; the C7 bump tends to stay prominent while the segment above it moves more.
- Put the tag centered over that bump, on skin or a tight collar.
β οΈ Important: Don't stick this tag flat to the back of your neck β the side camera will see only the paper edge. Use the foldable flag template instead.
C7 flag assembly:
- Print and cut out the
C7 side-facing flag template - Tape the
ANCHORarea directly over C7 - Fold the tag panel on the dashed line so the tag face points toward the camera
- Stiffen with a second layer of tape or thin cardstock
The app applies a C7 anchor correction (default 0.75 tag widths, configurable via POSTURE_WATCHER_C7_ANCHOR_OFFSET_TAG_WIDTHS).
Use the acromion: the bony roof at the outside/top of the shoulder. Trace the collarbone outward until you reach the flat bony shelf. Avoid the upper arm muscle and loose sleeve fabric.
For sit/stand detection. Prefer a belt clip or tight waistband on the camera-facing side, roughly aligned with the greater trochanter area. Loose shirt hems drift independently.
Before trusting the curve:
Tags readyβ all required tags visiblePlacement goodβ geometry is plausibleDetected SittingorDetected Standingβ mode agrees with reality
If you see Aim C7 flag, Move ear tag up, or similar β fix the stickers first.
Don't calibrate against "perfect posture." Calibrate against your own repeatable, comfortable, clinician-approved working positions.
- Set Mode β
Standing, set up normally, wait forPlacement good, hit Save Sample 3Γ over a minute. - Repeat in Sitting mode.
- Click Calibrate in the app, or:
cargo run -- calibrate-baselineThis writes to ~/Library/Application Support/Posture Watcher/calibration/baseline.txt. Once ready, the Badger shows baseline-relative drift like sit -3deg or std +2deg with a dashed baseline curve.
ποΈ What to tune later
Once you have good samples in both modes:
- Mode detection: shoulder-to-hip geometry and absolute marker positions
- Head/neck trend: craniovertebral angle from tragus/ear to C7
- Shoulder/torso trend: shoulder, C7, and hip relationship
- Feedback threshold: how far and how long you drift before the Badger looks "off"
The important idea is trend feedback over time β noticing sustained drift, not forcing a rigid pose.
Snapshot & diagnostics
# Analyze current app frame
cargo run -- snapshot \
--input "$HOME/Library/Application Support/Posture Watcher/latest-frame.jpg" \
--rotate ccw90 --out-dir artifacts/snapshot
# Full diagnostic
cargo run -- doctorDoctor checks: C930e in camera list, one-frame capture works, Badger ACKs, tag detection works, baseline smoke test passes.
Live capture
# CLI live capture
cargo run -- live --camera "Logitech Webcam C930e" --port /dev/cu.usbmodem83201
# Live from app frame file
cargo run -- live-file \
--input "$HOME/Library/Application Support/Posture Watcher/latest-frame.jpg" \
--burst-dir "$HOME/Library/Application Support/Posture Watcher/burst" \
--burst-frames 8Useful flags:
cargo run -- live --capture-backend imagesnap
cargo run -- live --capture-backend ffmpeg --ffmpeg-input "0:none"
cargo run -- live --capture-timeout-secs 5
cargo run -- live --rotate none
cargo run -- live --baseline "$HOME/Library/Application Support/Posture Watcher/calibration/baseline.txt"
cargo run -- live --mode sitting
cargo run -- live-file --input "artifacts/tagged-samples/<sample>-tagged.png" --rotate none --once --no-badgerSample tools & Badger
# Generate fake tagged samples
cargo run -- annotate-samples
# Run sample sequence β Badger
cargo run -- run-samples --send-badger
# Render Badger framebuffer from latest payload
scripts/render-badger-display.py \
--out artifacts/badger-debug/latest-mounted.png \
--raw-out artifacts/badger-debug/latest-raw.png
# Restore original Badger launcher
cargo run -- restore-badgerWatch logs
scripts/watch-macos-app-log.shEnvironment overrides
POSTURE_WATCHER_CAMERA="Logitech Webcam C930e"
POSTURE_WATCHER_PORT="/dev/cu.usbmodem83201"
POSTURE_WATCHER_INTERVAL_SECS=15
POSTURE_WATCHER_NO_PERSON_AFTER_SECS=60
POSTURE_WATCHER_ROTATE=ccw90
POSTURE_WATCHER_BADGER_ORIENTATION=usb-bottom
POSTURE_WATCHER_NO_BADGER=1The Badger sits in portrait orientation just below the monitor, close enough to glance at without turning the setup into another dashboard. The macOS app stays open as the debugging view, while the e-ink display shows the low-friction posture trace.
The hardware loop is deliberately simple: the camera is mounted to the side for a clean profile view, a printed AprilTag sheet provides the daily markers, and the Badger plugs in over USB-C so the Rust analyzer can push the same curve to the desk display.
The software loop was built with Codex using a long-running /goal: "set up a working e2e Rust app for tracking my posture using a webcam and e-ink device." Codex drove the implementation across the Rust analyzer, Badger MicroPython receiver, macOS camera app, hardware checks, README screenshots, and iterative fixes while I tested the physical setup at the desk.
This project uses photogrammetry-style marker tracking, not medical diagnosis.
The calibration approach is based on a few practical constraints from the literature:
- CVA reliability: Craniovertebral angle can be measured reliably from photographs when marker placement and camera setup are controlled β systematic review, CVA sitting/standing discussion
- Ear/C7 placement: Follows common CVA photogrammetry practice (tragus + C7 spinous process, lateral photos from a fixed side view) β reliability study, radiography vs. photogrammetry
- Shoulder/hip landmarks: Support markers based on sagittal posture work using the acromion and greater trochanter β sagittal posture guidelines
- Placement consistency: Photogrammetry studies often use calibration/training to align marker methods β reliability study
- Ergonomics: Emphasis on changing positions and avoiding long static postures β Mayo Clinic guide
Built with π¦ Rust, π macOS, and 𦑠Badger2040 β by Jess Martin




