diff --git a/CLAUDE.md b/CLAUDE.md index ba94932..0555441 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -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:** diff --git a/README.md b/README.md index 62d68f2..dc6ec5c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/index.html b/docs/index.html index 9270172..f9cc4e5 100644 --- a/docs/index.html +++ b/docs/index.html @@ -986,6 +986,16 @@
Examples: AiM Solo 2 DL (~$1,200), Racelogic VBOX Sport (~$1,800), Motec systems ($3,000+)
+ ++ Apps like Harry's LapTimer use your phone's 1Hz GPS—one position per second. At 100 km/h, that's 28 meters between updates. Racing lines are interpolated guesses. Lap times are ±0.3s at best. No sensor fusion, just raw GPS dots. +
++ Blackbox with 25Hz GPS captures position every 1-2 meters. 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. +
+Zero-velocity updates prevent drift. Bias continuously estimated when stationary.
+Learn vehicle-specific thresholds by driving in each mode. Calibrate Track, City, Canyon, and Highway independently.
+