Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
18 changes: 16 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ src/
├── units.rs # Unit preference types and conversions
├── normalize.rs # Field name normalization system
├── computed.rs # Computed channels data types and library
├── colormap.rs # Viridis/Turbo colormap LUTs for track coloring
├── laps.rs # GPS coordinate-format normalization, track sanitization, lap detection
├── tiles.rs # Map tile providers, bounded worker pool, disk/texture caches
├── expression/
│ ├── mod.rs # Formula parsing, channel refs, time shifts, evaluation
│ └── engine.rs # Built-in expression compiler/evaluator (replaced meval)
Expand Down Expand Up @@ -117,6 +120,10 @@ src/
├── settings_panel.rs # Consolidated settings (display, units, normalization, updates)
├── tool_properties_panel.rs # Dynamic panel showing controls for the active tool (channels / histogram / scatter)
├── analysis_panel.rs # Window for running analysis algorithms (src/analysis) on the active log
├── data_panel.rs # Right-side data panel hosting DataWidget panes (rail, header, hide/restore)
├── widgets/
│ ├── mod.rs # DataWidget trait + static widget registry
│ └── track_map.rs # GPS Track Map widget (polyline, laps, tile backgrounds)
├── sidebar.rs # Legacy files panel, superseded by files_panel (kept, not wired into render path)
├── channels.rs # Selected-channel cards; legacy channel-picker superseded by tool_properties_panel
├── chart.rs # Chart rendering, legends, LTTB algorithm
Expand Down Expand Up @@ -377,7 +384,7 @@ because it also leads with a `Time` column, and only matches a first column of e

### Settings Persistence Contract

`UserSettings` (`src/settings.rs`) is a `Serialize`/`Deserialize` struct persisted as JSON at `{app_data_dir}/UltraLog/settings.json` (or `.../ultralog/settings.json` on Linux). It currently covers: `language`, `scroll_to_zoom`, `show_grid`, `grid_opacity`, `unit_preferences`, `font_scale`, `color_blind_mode`, `field_normalization`, `cursor_tracking`, `auto_check_updates`, and `custom_normalizations`.
`UserSettings` (`src/settings.rs`) is a `Serialize`/`Deserialize` struct persisted as JSON at `{app_data_dir}/UltraLog/settings.json` (or `.../ultralog/settings.json` on Linux). It currently covers: `language`, `scroll_to_zoom`, `show_grid`, `grid_opacity`, `unit_preferences`, `font_scale`, `color_blind_mode`, `field_normalization`, `cursor_tracking`, `auto_check_updates`, `custom_normalizations`, and the Track Map preferences (`tile_provider`, `tile_cache_max_mb`, `tiles_enabled`, `tile_opacity`, `tile_grayscale`, `tile_privacy_notice_seen`, `hidden_widgets`).

- **Load** - `UltraLogApp::new` calls `UserSettings::load()` and copies each field into the corresponding live `UltraLogApp` field (e.g. `app.color_blind_mode = user_settings.color_blind_mode`).
- **Save** - `UltraLogApp` implements `eframe::App::save`, which eframe calls on its auto-save interval (~30s) and again at shutdown. `save()` rebuilds a `UserSettings` from the current live fields, and only writes to disk if it differs from the last-loaded/saved value.
Expand All @@ -393,11 +400,18 @@ Several `UltraLogApp` fields cache expensive per-file, per-channel computations

**Load-bearing invariant:** these caches are keyed by index (file index / channel index), not by identity. Any code path that mutates or reindexes channel data — removing a file, or removing/editing a computed channel (which shifts later computed channels' indices down) — **must clear the relevant caches**, or stale entries will silently render the wrong data against a different channel's index. See `remove_computed_channel` and the file-removal path in `src/app.rs` for the current call sites (`self.downsample_cache.clear()`, `self.minmax_cache.clear()`, `self.scatter_histogram_cache.clear()`).

### Map Tile Fetching Contract (src/tiles.rs)

The Track Map widget can draw map tile backgrounds. Tiles are **opt-in** (off by default) and only two hardcoded HTTPS providers exist: Esri World Imagery and OpenStreetMap. Requests carry integer tile coordinates plus the UltraLog User-Agent, nothing else. The first time a user enables tiles, a one-time toast (`tile_privacy_notice_seen`) notes that the track's approximate location is shared with the provider.

**Load-bearing invariant:** network fetches are gated by per-provider permits (`FetchPermits` + `TileProvider::max_concurrent_fetches`). The OSM tile usage policy allows at most **2 simultaneous download connections** (<https://operations.osmfoundation.org/policies/tiles/>), so `OpenStreetMap::max_concurrent_fetches` returns 2. Do not raise it and do not bypass the permit gate — exceeding the policy risks a per-IP block that would hit every UltraLog user at once.

## Key Features

- **Multi-ECU Support** - Haltech, ECUMaster, RomRaider, Speeduino, rusEFI, AiM, Link, Emerald, MegaSquirt, MHD Tuning, Motorsport Electronics, Woolich Racing Tuned, BlueDriver, DynamicEFI, and Locomotive log formats
- **Computed Channels** - Create virtual channels from mathematical formulas with time-shifting (e.g., `RPM[-1]`, `Boost@-0.5s`)
- **Analysis Algorithms** - AFR/Lambda drift and zone detection, derived metrics (VE, injector duty cycle), signal filters, and descriptive statistics (`src/analysis/`)
- **GPS Track Map** - Right-side data panel with a track map: lap detection, channel-colored polyline (Viridis/Turbo with editable range), hover-scrub/click-seek cursor sync, and opt-in Esri/OSM tile backgrounds (`src/ui/widgets/track_map.rs`, `src/tiles.rs`, `src/laps.rs`). GPS coordinate encodings are auto-detected and normalized to decimal degrees (`GpsCoordSpec` in `src/laps.rs`): NMEA `DDMM.mmmm`, milli/micro/1e-7-scaled integer degrees, and 0-360 longitude. Detection is conservative - values already in valid degree ranges are never transformed, and radians are deliberately not detected (ambiguous with genuine near-equator degree tracks).
- **Claude Desktop / MCP Integration** - Embedded MCP server (`src/mcp/`) lets Claude control the running app over `http://localhost:52385/mcp` — select channels, add computed channels, query log data
- **Unit Preferences** - Users can select display units for temperature, pressure, speed, distance, fuel economy, volume, flow rate, and acceleration
- **Field Normalization** - Maps ECU-specific channel names to standardized names for cross-ECU comparison
Expand Down Expand Up @@ -474,7 +488,7 @@ Example log files are in `exampleLogs/` organized by ECU type:
- `exampleLogs/link/` - Link ECU LLG files
- `exampleLogs/woolich/` - Woolich Racing Tuned CSV exports
- `exampleLogs/emerald/` - Emerald K6/M3D `.lg1`/`.lg2` files
- `exampleLogs/megasquirt/` - MegaSquirt TunerStudio CSV exports
- `exampleLogs/megasquirt/` - MegaSquirt TunerStudio CSV exports, plus a `_gps.mlg` fixture with synthetic GPS channels (generated by `examples/inject_fake_gps_mlg.rs`; the coordinates are a fake closed loop, not a real location)
- `exampleLogs/mhd/` - MHD Tuning CSV exports (VIN redacted)
- `exampleLogs/motorsportElectronics/` - Motorsport Electronics ME Tuner CSV exports
- `exampleLogs/bluedriver/` - BlueDriver OBD-II CSV exports
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ zip = "8.6" # ZIP extraction for Windows updates
flate2 = "1.1" # Gzip decompression for Linux updates
tar = "0.4" # Tar archive extraction for Linux updates

# Image loading (for app icon and PNG export)
image = { version = "0.25", default-features = false, features = ["png"] }
# Image loading (for app icon, PNG export, and map tiles – Esri serves JPEG)
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }

# PDF generation for chart export
printpdf = "0.12"
Expand Down
6 changes: 3 additions & 3 deletions docs/FORMAT_SPECIFICATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,9 @@ The MLG format is used by Speeduino, rusEFI, and MegaSquirt ecosystems. It's a c
| 5 | S32 | 4 |
| 6 | S64 | 8 |
| 7 | F32 | 4 |
| 10 | U08 Bitfield | 1 |
| 11 | U16 Bitfield | 2 |
| 12 | U32 Bitfield | 4 |
| `0x10` | U08 Bitfield | 1 |
| `0x11` | U16 Bitfield | 2 |
| `0x12` | U32 Bitfield | 4 |

### Data Records

Expand Down
Binary file not shown.
43 changes: 43 additions & 0 deletions examples/check_gps_in_log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//! Diagnostic: parse an MLG file and replicate `detect_gps_channels` end-to-end.

use std::env;
use std::fs;

use ultralog::adapters::registry;
use ultralog::parsers::Speeduino;

fn main() {
let path = env::args()
.nth(1)
.unwrap_or_else(|| "exampleLogs/megasquirt/2026-04-12_12.49.36_gps.mlg".to_string());
let data = fs::read(&path).expect("read file");
let log = Speeduino::parse_binary(&data).expect("parse");
println!("path: {}", path);
println!("channels: {}", log.channels.len());

let mut lat_idx: Option<usize> = None;
let mut lon_idx: Option<usize> = None;
let mut gps_hits = Vec::new();
for (i, ch) in log.channels.iter().enumerate() {
let name = ch.name();
if name.to_lowercase().contains("gps")
|| name.to_lowercase().contains("lat")
|| name.to_lowercase().contains("lon")
{
let canon = registry::get_channel_metadata(&name)
.map(|m| m.canonical_id)
.unwrap_or_else(|| "<no metadata>".to_string());
gps_hits.push((i, name.clone(), canon.clone()));
match canon.as_str() {
"gps_latitude" => lat_idx = Some(i),
"gps_longitude" => lon_idx = Some(i),
_ => {}
}
}
}
println!("GPS-ish channels found: {}", gps_hits.len());
for (i, name, canon) in &gps_hits {
println!(" [{}] name={:?} canonical_id={}", i, name, canon);
}
println!("detect_gps_channels result: {:?}", (lat_idx, lon_idx));
}
28 changes: 28 additions & 0 deletions examples/check_gps_lookup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! Quick diagnostic: do "GPS Latitude" / "GPS Longitude" resolve to canonical
//! GPS IDs in the OECUA registry the way the Track Map widget expects?

use ultralog::adapters::registry;

fn main() {
let names = [
"GPS Latitude",
"GPS Longitude",
"gps latitude",
"GPS Lat",
"Latitude",
"Lon",
];
println!("adapters loaded: {}", registry::get_adapters().len());
for name in names {
match registry::get_channel_metadata(name) {
Some(meta) => println!(
" {:<14} -> canonical_id={:<14} display={:<14} vendor={}",
format!("\"{}\"", name),
meta.canonical_id,
meta.display_name,
meta.vendor
),
None => println!(" \"{}\" -> NOT FOUND", name),
}
}
}
Loading