Building Tracer2: How I Got 53 Days from a Coin Cell
The full story of designing a multi-sensor wearable platform - including the power problems that almost killed the project

Building Tracer2: How I Got 53 Days from a Coin Cell
The first Tracer was a disaster.
Not in the "it doesn't work" sense - it worked fine. The problem was battery life. My calculations said 30 days. Reality delivered 4 days. That's not a rounding error. Something was fundamentally wrong.
Three board revisions and countless hours with a power analyzer later, Tracer2 now hits 53 days on a CR2450 coin cell. This is the story of how I got there.
What I Was Trying to Build
I wanted an open-source wearable sensor platform. Something I could use for:
- Fitness tracking with custom algorithms
- Gesture recognition experiments
- Research data collection
- A platform to actually understand what my commercial fitness tracker was doing
Commercial fitness trackers are black boxes. You get the data they decide to give you, in the format they choose, with the algorithms they picked. I wanted the raw data and control over the processing.
The Hardware Stack
Tracer2 packs a lot into a small package:
| Component | Purpose | Why This One |
|---|---|---|
| ESP32-C6 | Brain | LP core + BLE 5.3 for power |
| LSM6DSLTR | 6-axis IMU | Embedded features, interrupts |
| VL53L0CX | Distance sensor | ToF up to 2m, gesture detection |
| AHT20 | Temp/humidity | I2C, ultra-low sleep |
| PDM Microphone | Audio | Voice/sound detection |
| CR2450 | Battery | 600mAh, thin form factor |
Total BOM cost: about $25 at single quantities.
Why ESP32-C6?
I chose the C6 specifically for two features:
1. The LP (Low-Power) Core
The ESP32-C6 has a secondary RISC-V core that runs at 20MHz while the main core sleeps. On Tracer2, this core monitors the IMU for motion events.
Without LP core (how C3 would work):
Wake every 100ms → Check IMU → Sleep
Average current: ~80µA (mostly from waking up)
With LP core:
LP core monitors IMU interrupt → Wakes main core only on motion
Average current: ~15µA when idle
For a motion-triggered wearable, this is huge.
2. BLE 5.3 Connection Subrating
When streaming sensor data over BLE, connection subrating adjusts the radio interval based on data activity. During idle periods, the radio wakes up less frequently. During active transfer, it speeds up.
This saved about 10% power during continuous streaming on Tracer2.
The Power Disaster (And How I Fixed It)
My first power budget looked reasonable:
ESP32-C6 sleep: 7µA (99% of time) = 6.93µA average
ESP32-C6 active: 80mA (1% of time) = 800µA average
IMU: 3µA (always on in low-power mode)
Total: ~810µA average
Battery life: 600mAh / 0.81mA = 740 hours = 31 days
Reality: 4 days. Where did 27 days go?
Problem 1: The Phantom Resistor
I was measuring 340µA in sleep mode. Should have been 7µA.
After hours with a thermal camera and current probe, I found it: a 10kΩ pull-up resistor on an I2C line that was being pulled low by a sleeping sensor. That's 330µA, gone.
Fix: Changed to 100kΩ pull-ups (33µA) and made sure no sensors pulled lines low during sleep.
Problem 2: The Chatty IMU
The LSM6DSLTR has great low-power modes, but the default configuration left the FIFO enabled. Even in "sleep" mode, it was buffering data and drawing 200µA.
Fix: Properly disable FIFO and all unused features before sleep:
void configureIMUForSleep() {
// Disable FIFO
writeRegister(LSM6DSLTR_FIFO_CTRL5, 0x00);
// Set ODR to 0 (power-down) for both accel and gyro
writeRegister(LSM6DSLTR_CTRL1_XL, 0x00);
writeRegister(LSM6DSLTR_CTRL2_G, 0x00);
// Keep interrupt routing active for wake-on-motion
writeRegister(LSM6DSLTR_INT1_CTRL, 0x10);
}Problem 3: The WiFi That Wouldn't Die
During testing, I had WiFi enabled for debugging. I thought I disabled it before shipping the firmware. I hadn't.
Every 30 seconds, the device was doing a WiFi scan looking for my development network. That's 80mA for 3 seconds, every 30 seconds. Catastrophic.
Fix: Added compile-time flags to completely disable WiFi in production firmware:
#ifdef PRODUCTION_BUILD
esp_wifi_deinit(); // Completely remove WiFi stack
#endifThe Final Power Budget
After fixing these issues:
Component | Current | Duty Cycle | Average
-------------------|---------|------------|--------
ESP32-C6 (sleep) | 7µA | 99% | 6.9µA
ESP32-C6 (active) | 80mA | 0.5% | 400µA
LSM6DSLTR (sleep) | 3µA | 99.5% | 3µA
LP core monitoring | 8µA | 99.5% | 8µA
BLE transmission | 20mA | 0.1% | 20µA
VL53L0CX (gated) | 0µA | 99.8% | 38µA when on
AHT20 (gated) | 0µA | 99.9% | 0.2µA when on
Total average: | 476µA
Battery life: 600mAh / 0.476mA = 1260 hours = 52.5 days ✅
The 53-day number comes from real-world testing. The calculation matches reality.
The Sensor Stack
LSM6DSLTR: More Than Just an IMU
I chose the LSM6DSLTR specifically for its embedded features:
Step counter: Built-in pedometer with configurable sensitivity. Doesn't need the main MCU.
Tilt detection: Hardware-based orientation changes, generates interrupt.
Wake-on-motion: Configurable acceleration threshold triggers interrupt. The LP core waits for this interrupt instead of polling.
FIFO buffering: Can collect samples while main MCU sleeps, then wake to process in batch. (But disable it when you're not using it!)
void configurePedometer() {
// Enable embedded functions
writeRegister(FUNC_CFG_ACCESS, 0x80);
// Enable step counter
writeRegister(EMB_FUNC_EN_A, 0x10);
// Route step counter interrupt to INT1
writeRegister(EMB_FUNC_INT1, 0x10);
// Disable embedded function access
writeRegister(FUNC_CFG_ACCESS, 0x00);
}VL53L0CX: Time-of-Flight Distance
The ToF sensor is power-gated (completely off when not in use) because it draws 19mA when active. I use it for:
Gesture detection: Wave your hand near the sensor, detect approach/retreat patterns.
Proximity wake: Wake up when something comes close (like lifting your wrist).
int readDistanceMM() {
// Power on ToF
digitalWrite(TOF_POWER_PIN, HIGH);
delay(50); // Boot time
// Initialize and read
tof.begin();
int distance = tof.readRangeSingleMillimeters();
// Power off immediately
digitalWrite(TOF_POWER_PIN, LOW);
return distance;
}AHT20: Environment Context
Temperature and humidity provide context for motion data. Is the user exercising (elevated temp) or resting? Indoor or outdoor?
The AHT20 draws almost nothing in sleep mode (<1µA), so I leave it powered but only read it occasionally.
PCB Design for Wearables
Designing for a wearable has unique constraints:
Size Matters (A Lot)
My target was 30mm × 25mm - roughly the size of a large coin. This meant:
- 0402 passives minimum, some 0201
- QFN packages only (no SOICs)
- Four-layer board for routing density
- Components on both sides
Antenna Considerations
The ESP32-C6's PCB antenna needs a ground plane keepout and clear area. On a small wearable, this is challenging.
What I learned:
- Keep the antenna at the board edge
- Don't put ground pour under the antenna
- Body proximity affects antenna performance (tune with actual wrist placement)
I lost about 30% BLE range compared to the devkit, but it's still usable.
The Battery Holder Trade-off
CR2450 holders are bulky. I tried three approaches:
- Through-hole holder: Reliable but adds 3mm height
- SMD holder: Lower profile but less secure
- Battery tabs (final choice): Shortest but requires spot welding or careful soldering
I went with battery tabs for the production version. It's more work during assembly but gives the thinnest profile.
Firmware Architecture
The firmware is structured around power efficiency:
┌──────────────────────────────────┐
│ Main App │
│ - Sensor fusion │
│ - BLE communication │
│ - User interface │
└──────────────┬───────────────────┘
│ (wake on interrupt)
┌──────────────┴───────────────────┐
│ LP Core Watchdog │
│ - IMU interrupt monitoring │
│ - Motion pattern detection │
│ - Wake main core on events │
└──────────────────────────────────┘
The LP core is the gatekeeper. It monitors for interesting events and only wakes the power-hungry main core when there's actual work to do.
Motion Detection State Machine
enum MotionState {
IDLE, // No significant motion
ACTIVE, // Continuous motion detected
GESTURE, // Specific gesture pattern
SLEEPING // Device in low-power mode
};
void lpCoreLoop() {
while (true) {
if (imuInterruptPending()) {
MotionState state = classifyMotion();
if (state == GESTURE || state == ACTIVE) {
wakeMainCore(state);
}
}
// LP core sleep between checks
delayMicroseconds(10000); // 10ms polling at ~10µA
}
}BLE Data Streaming
For continuous motion streaming (research mode), I use a custom GATT characteristic with notifications:
// 20-byte packets: timestamp (4) + accel XYZ (6) + gyro XYZ (6) + flags (4)
struct MotionPacket {
uint32_t timestamp;
int16_t accel[3];
int16_t gyro[3];
uint32_t flags;
};
void streamMotionData() {
MotionPacket packet;
packet.timestamp = millis();
imu.getMotion6(
&packet.accel[0], &packet.accel[1], &packet.accel[2],
&packet.gyro[0], &packet.gyro[1], &packet.gyro[2]
);
motionCharacteristic.notify((uint8_t*)&packet, sizeof(packet));
}At 50Hz sampling, this generates about 1KB/sec of data. BLE handles it fine.
Machine Learning on the Edge
Tracer2 has enough resources for simple ML inference:
Available:
- 512KB SRAM (after BLE stack)
- 160MHz RISC-V CPU
- ~200KB free for models and inference
What works:
- Activity classification (walking, running, cycling): ~10KB model
- Gesture recognition (5-10 gestures): ~20KB model
- Step counting: Built into IMU, no ML needed
What doesn't fit:
- Voice recognition (need external accelerator)
- Complex gesture sequences (need more RAM)
- Real-time pose estimation (need more compute)
I've deployed Edge Impulse models for activity classification. The workflow:
- Collect labeled data using Tracer2's streaming mode
- Train model in Edge Impulse
- Export as C++ library
- Include in firmware
Inference takes about 50ms and draws ~30mA - acceptable for occasional classification.
Lessons Learned
What Went Well
- ESP32-C6 LP core: Game-changer for battery life
- LSM6DSLTR embedded features: Reduced MCU wake time significantly
- Power gating: VL53L0CX and AHT20 are effectively free when off
- BLE 5.3: Connection subrating helped more than expected
What I'd Do Differently
- Bigger battery option: CR2477 (1000mAh) would fit with minor redesign
- Better antenna placement: Lost more range than I wanted to body effects
- Hardware revision without ToF: Many applications don't need it, could save cost
- Add fuel gauge IC: Software battery estimation is imprecise
Unexpected Challenges
- I2C bus conflicts: Multiple sensors with similar addresses required careful initialization order
- IMU calibration: Offset varies between units; need per-device calibration
- Deep sleep reliability: Some early units wouldn't wake reliably (fixed with stronger pull-ups on wake pins)
The Open Source Release
Everything is available on GitHub: github.com/tooyipjee/tracer2
What's included:
- KiCad schematic and PCB files
- BOM with supplier links
- ESP-IDF firmware with examples
- Edge Impulse integration guide
- Calibration procedures
Get Your Own
If you don't want to build from scratch, Tracer2 is available in my store for $39.99. Includes:
- Assembled and tested PCB
- CR2450 battery
- Getting started guide
- Access to firmware updates
What's Next
I'm working on a few improvements:
- E-ink display add-on for always-on status
- Qi wireless charging cradle
- Swimming/waterproof enclosure
- Integration with Home Assistant for presence detection
Building something with Tracer2? I'd love to see it. Share your project via the contact form or tag me on GitHub.