Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ You are a seasoned embedded Rust engineer with deep expertise in:
- Avoid over-engineering: solve the problem at hand, not hypothetical future problems
- Keep abstractions minimal until they prove necessary

**SOLID Principles:**
- **Single Responsibility**: Each module/struct should have one reason to change. Keep sensors, protocols, and business logic separate.
- **Open/Closed**: Design for extension without modification. Use traits to allow new sensor types or protocols without changing core code.
- **Liskov Substitution**: Implementations of traits must be substitutable. If `GpsSensor` trait exists, any implementation should work interchangeably.
- **Interface Segregation**: Prefer small, focused traits over large ones. Don't force implementations to depend on methods they don't use.
- **Dependency Inversion**: High-level modules should depend on abstractions (traits), not concrete implementations. Pass dependencies via constructors or function parameters.

**Testing Philosophy:**
- Write meaningful tests that catch real bugs, not boilerplate
- **Every test MUST have assertions** (`assert!`, `assert_eq!`, etc.) - a test without assertions proves nothing
Expand Down Expand Up @@ -645,6 +652,132 @@ Displayed as updates per minute (rolling average), not cumulative count. Typical
- ZUPT: 0-60/min (depends on stops)
- Free heap: ~60KB (stable during operation)

### settings.rs - NVS Settings Persistence

**Purpose:**
Persists mode detection thresholds to ESP32's Non-Volatile Storage (NVS) so they survive power cycles.

**Functions:**
- `load_settings(nvs_partition)`: Loads `ModeConfig` from NVS, returns `None` if not found
- `save_settings(nvs_partition, config)`: Saves `ModeConfig` to NVS, returns success/failure

**NVS Schema:**
- **Namespace:** `bb_cfg`
- **Keys (max 15 chars for NVS):**
- `acc` - acceleration entry threshold (f32)
- `acc_x` - acceleration exit threshold (f32)
- `brk` - brake entry threshold (f32)
- `brk_x` - brake exit threshold (f32)
- `lat` - lateral entry threshold (f32)
- `lat_x` - lateral exit threshold (f32)
- `yaw` - yaw rate threshold (f32)
- `spd` - minimum speed (f32)

**Storage Format:**
Values stored as raw 4-byte little-endian floats (`f32::to_le_bytes()`).

**Usage in main.rs:**
```rust
// On boot: load saved settings
let saved_config = nvs.as_ref().and_then(|p| settings::load_settings(p.clone()));
if let Some(cfg) = saved_config {
estimator.mode_classifier.update_config(cfg);
}

// On settings change: save to NVS
if let Some(ref nvs) = nvs_for_settings {
settings::save_settings(nvs.clone(), &new_config);
}
```

**Note:** `alpha` (EMA filter) is NOT persisted - always uses mode.rs default (0.35).

### Autotune System

**Overview:**
Autotune learns mode detection thresholds by driving in the selected mode. Each profile (City, Canyon, Track, Highway) is calibrated independently - no scaling between profiles.

**Key Design:**
- User selects which mode to calibrate (Track/Canyon/City/Highway)
- Drives in that style for 10-20 minutes
- Thresholds computed directly from that driving style
- No arbitrary scaling factors between profiles
- Each mode calibrated by actually driving in that mode

**Data Flow:**
```
Select Mode → Drive → Event Detection → IQR Filter → Median → NVS + localStorage
│ │ │ │ │ │
└─►Track └─►ax,ay,wz └─►ACCEL/BRAKE/ └─►Outlier └─►P50×0.7 └─►bb_cfg
CORNER bins rejection (active)
```

**Event Detection Algorithm:**
```javascript
// Parameters tuned for 25Hz polling to match 200Hz mode.rs dynamics
const EMA_ALPHA = 0.85; // Adjusted for sample rate mismatch
const NOISE_FLOOR = 0.08; // 0.08g - reject road bumps
const EVENT_MIN_DURATION = 200; // ms - reject transients
const EVENT_COOLDOWN = 400; // ms - gap between events
const MIN_SPEED_KMH = 7.2; // 2.0 m/s

// EMA filter matching mode.rs dynamics
emaLonSim = (1 - EMA_ALPHA) * emaLonSim + EMA_ALPHA * lonG;

// Event detection with noise floor
if (emaLonSim > NOISE_FLOOR && speed > MIN_SPEED_KMH) {
// Start/continue ACCEL event, track peak
}
// BRAKE: emaLonSim < -NOISE_FLOOR
// TURN: |emaLatSim| > NOISE_FLOOR && |emaYawSim| > 0.02 && sign consistent
```

**Threshold Calculation:**
```javascript
// IQR outlier rejection
function filterOutliers(arr) {
const q1 = percentile(arr, 25), q3 = percentile(arr, 75);
const iqr = q3 - q1;
return arr.filter(v => v >= q1 - 1.5*iqr && v <= q3 + 1.5*iqr);
}

// Compute thresholds from collected events
const peaks = filterOutliers(events.map(e => e.peak));
const P50 = median(peaks);
const entry = P50 * 0.7; // 70% of median
const exit = entry * 0.5; // 50% of entry (hysteresis)
```

**NVS + localStorage Storage:**
```javascript
// On Apply:
// 1. Send to ESP32 → saves to NVS (bb_cfg namespace)
await fetch('/api/settings/set?acc=...&brake=...');

// 2. Save to browser localStorage for selected mode
let profiles = JSON.parse(localStorage.getItem('bb_profiles') || '{}');
profiles[selectedMode] = {...calibratedProfile, calibrated: new Date().toISOString()};
profiles.lastMode = selectedMode;
localStorage.setItem('bb_profiles', JSON.stringify(profiles));
```

**Boot Behavior:**
- ESP32 reads thresholds from NVS on boot
- If NVS has valid data → applies to mode classifier
- If NVS empty → uses City defaults (mode.rs `ModeConfig::default()`)

**Confidence Levels:**
- HIGH: n ≥ 15 events per category
- MEDIUM: n = 8-14 events
- LOW: n = 3-7 events
- INSUFFICIENT: n < 3

**Integration with Dashboard:**
- User calibrates each mode by selecting it and driving in that style
- Clicking preset loads calibrated values (or defaults if uncalibrated)
- Export button downloads comprehensive JSON for analysis
- Progress bar shows real-time event capture during calibration

### udp_stream.rs - UDP Client (Station Mode)

**Features:**
Expand Down
107 changes: 107 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,113 @@ You can trigger recalibration from the dashboard:

---

## Autotune (Threshold Calibration)

The **Autotune** feature learns your vehicle's characteristics by driving in the mode you want to calibrate. Each profile (City, Canyon, Track, Highway) is calibrated independently - no arbitrary scaling. Calibrated settings persist across power cycles via NVS.

### Why Autotune?

Default presets use generic thresholds, but every vehicle and driver is different:
- A sports car can pull 1.0g lateral; a minivan might max at 0.5g
- Track driving produces different g-forces than city driving
- Calibrate each mode by actually driving in that style

### How to Use Autotune

1. **Open the dashboard** at `http://192.168.71.1`
2. **Tap "Autotune"** in the menu
3. **Select the mode to calibrate** (City, Canyon, Track, or Highway)
4. **Wait for GPS lock** (green GPS status required)
5. **Tap "Start Calibration"** and drive in that style for 10-20 minutes
6. **Collect events**: accelerations, brakes, and turns
7. **Review thresholds** - computed from YOUR driving
8. **Tap "Apply"** to save to ESP32 (NVS) and browser

### Mode-Specific Calibration

Unlike previous versions that scaled from a single baseline, each mode is now calibrated independently:

| Mode | Calibration Drive Style |
|------|------------------------|
| **City** | Normal commute - stop lights, lane changes, parking |
| **Canyon** | Spirited mountain roads - tighter turns, harder braking |
| **Track** | Racing/track day - aggressive accel, hard braking, high-g turns |
| **Highway** | Highway cruising - gentle lane changes, gradual accel/brake |

**Calibrate each mode separately** by driving in that style. This eliminates guesswork from scaling factors.

### Algorithm Details

Autotune uses physics-informed signal processing matching `mode.rs`:

**Event Detection:**
- **EMA filtering** at α=0.85 (tuned for 25Hz polling to match 200Hz firmware)
- **Noise floor** of 0.08g rejects road bumps and vibration
- **Duration filter** of 200ms ensures intentional inputs
- **Cooldown** of 400ms between events prevents double-counting
- **Sign consistency** for turns: lateral G and yaw rate must agree

**Threshold Calculation:**
1. Collect peak G-forces from each event type (accel, brake, turn)
2. Apply **IQR outlier rejection** (1.5× interquartile range)
3. Compute **median (P50)** of filtered peaks
4. Entry threshold = **70% of median**
5. Exit threshold = **50% of entry** (hysteresis)

**min_speed** is fixed at 2.0 m/s (7.2 km/h) - physics doesn't change with driving style.

### NVS Persistence

Calibrated settings persist in ESP32's Non-Volatile Storage:

**On Apply:**
- Thresholds sent to ESP32 via `/api/settings/set`
- ESP32 saves to NVS namespace `bb_cfg`
- Browser saves profile to `localStorage` for dashboard

**On Boot:**
- ESP32 reads thresholds from NVS
- If valid, applies to mode classifier automatically
- If NVS is empty/invalid, uses **City defaults** (acc=0.10g, brake=0.18g, lat=0.12g)
- No manual configuration needed after calibration

**Storage Keys:** `acc`, `acc_x`, `brk`, `brk_x`, `lat`, `lat_x`, `yaw`, `spd`

### Confidence Levels

| Events | Confidence | Recommendation |
|--------|------------|----------------|
| 3-7 each | Low | Keep driving - thresholds may be noisy |
| 8-14 each | Medium | Good for most uses |
| 15+ each | High | Robust outlier rejection, accurate medians |

### Export Data

Exports JSON with mode-specific calibration data:
```json
{
"version": 2,
"mode": "track",
"exportDate": "2024-01-15T10:30:00Z",
"calibrated": { "acc": 0.35, "brake": 0.50, ... },
"events": {
"accel": [{"peak": 0.45, "dur": 450, "dv": 18.3}, ...],
"brake": [...],
"turn": [...]
}
}
```

### Tips for Best Results

- **Drive in the style you're calibrating** - Track mode = aggressive, City = normal
- **Calibrate each mode separately** - switch modes and recalibrate
- **Include variety** - different turn directions, brake intensities
- **More driving = better** - 15+ events each gives high confidence
- **GPS lock required** - ensures accurate speed filtering

---

## Mobile Dashboard

The firmware includes a built-in web dashboard that runs directly on the ESP32. No external server needed - just connect your phone and view live telemetry.
Expand Down
29 changes: 29 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,16 @@ <h3>Commercial Systems</h3>
</div>
</div>
<p class="comparison-note">Examples: AiM Solo 2 DL (~$1,200), Racelogic VBOX Sport (~$1,800), Motec systems ($3,000+)</p>

<div style="margin-top: 40px; padding: 24px 28px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px;">
<h3 style="font-size: 15px; font-weight: 600; margin-bottom: 12px; color: var(--text-primary);">What about phone apps?</h3>
<p style="font-size: 14px; color: var(--text-secondary); line-height: 1.7; margin: 0;">
Apps like Harry's LapTimer use your phone's 1Hz GPS—one position per second. At 100 km/h, that's <strong style="color: var(--text-primary);">28 meters between updates</strong>. Racing lines are interpolated guesses. Lap times are ±0.3s at best. No sensor fusion, just raw GPS dots.
</p>
<p style="font-size: 14px; color: var(--text-secondary); line-height: 1.7; margin: 12px 0 0 0;">
Blackbox with 25Hz GPS captures position every <strong style="color: var(--text-primary);">1-2 meters</strong>. The EKF fuses 200Hz IMU between GPS updates for continuous tracking—the same approach in commercial systems. Phone apps are fine for casual use. Dedicated hardware is for actual accuracy.
</p>
</div>
</div>
</section>

Expand Down Expand Up @@ -1063,6 +1073,21 @@ <h3>ZUPT</h3>
<p>Zero-velocity updates prevent drift. Bias continuously estimated when stationary.</p>
</div>

<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 20V10"/>
<path d="M18 20V4"/>
<path d="M6 20v-4"/>
<circle cx="12" cy="7" r="2"/>
<circle cx="6" cy="13" r="2"/>
<circle cx="18" cy="7" r="2"/>
</svg>
</div>
<h3>Autotune</h3>
<p>Learn vehicle-specific thresholds by driving in each mode. Calibrate Track, City, Canyon, and Highway independently.</p>
</div>

<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
Expand Down Expand Up @@ -1127,6 +1152,10 @@ <h3>Dashboard Features</h3>
<svg class="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6L9 17l-5-5"/></svg>
Driving presets: Track, Canyon, City, Highway, Custom
</li>
<li>
<svg class="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6L9 17l-5-5"/></svg>
Autotune: calibrate thresholds to your vehicle
</li>
<li>
<svg class="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6L9 17l-5-5"/></svg>
Record & export to CSV
Expand Down
Loading