Skip to content

feat(parser): add Motorsport Electronics ME221/ME442 support#66

Merged
SomethingNew71 merged 4 commits intomainfrom
feat/motorsport-electronics-parser
Apr 21, 2026
Merged

feat(parser): add Motorsport Electronics ME221/ME442 support#66
SomethingNew71 merged 4 commits intomainfrom
feat/motorsport-electronics-parser

Conversation

@SomethingNew71
Copy link
Copy Markdown
Collaborator

Summary

  • Adds a dedicated parser for Motorsport Electronics ME Tuner CSV exports (ME221/ME442)
  • Fixes a time-scaling regression where ME221 logs displayed ~1000x too short
  • Ships with integration tests against two real example logs

Background

A user running a turbo ME221 in their MX-5 reported that UltraLog was importing ME221 CSVs with the wrong time scale (otherwise, all the data was there). ME Tuner exports are popular on turbo/supercharged MX-5, Ford Focus ST150, Honda S2000, Toyota Starlet, and Mitsubishi Evo builds.

Root cause

Both ME Tuner and RomRaider CSVs start with a Time, header. RomRaider's detect() was permissive enough to claim ME221 files, and its parser divides the Time column by 1000 on the assumption it is milliseconds. ME Tuner's Time column is in seconds, so a 111-second log rendered as 0.111 seconds.

Fix

  • New MotorsportElectronics parser with strict detection: first column must be exactly Time and at least 2 ME221 fingerprint columns must be present (Sync status, Lost sync count, Injector duty, MAP Raw, TPS Raw, Ign. Adv. Base Table Output, WBO2 Curr. AFR, DBW Status)
  • Dispatched before RomRaider in app.rs so it wins the ambiguous detection
  • Time treated as seconds, first timestamp normalized to zero
  • Units inferred from channel names (ME Tuner doesn't embed units in headers)
  • Trailing Dummy column stripped

Test plan

  • cargo test — all tests pass
  • cargo fmt --all -- --check
  • cargo clippy -- -D warnings
  • Unit tests cover detection, RomRaider rejection, generic CSV rejection, time-seconds scaling, first-timestamp normalization, unit inference
  • Integration tests parse both bundled real example logs (ME221_2025_12_22_13_13_40.csv, ME221_2026_04_12_11_59_52.csv) and assert time range is in seconds

ME Tuner CSV exports start with "Time," which the RomRaider parser
was also claiming via loose detection. RomRaider then treated Time as
milliseconds and divided by 1000, so a 111-second ME221 log rendered
as 0.111 seconds.

Add a dedicated parser with strict detection (exact "Time" first
column plus ME221 fingerprint columns like Sync status, Injector
duty, WBO2 Curr. AFR, DBW Status) and dispatch it before RomRaider.
Time is treated as seconds, the first timestamp is normalized to
zero, units are inferred from channel names since ME Tuner does not
embed them, and the trailing Dummy column is stripped.

Reported by an MX-5 user running a turbo ME221.
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for Motorsport Electronics (ME221/ME442) ECU log files. The implementation includes a new parser that handles CSV exports from ME Tuner, featuring automatic unit inference for common channels and timestamp normalization. The parser is integrated into the application's detection logic with specific ordering to avoid conflicts with the RomRaider format. Comprehensive tests and example logs are also included. Feedback is provided regarding the efficiency of the parsing loop, suggesting the use of iterators to avoid unnecessary per-line vector allocations.

Comment thread src/parsers/motorsport_electronics.rs Outdated
Comment on lines +289 to +312
let parts: Vec<&str> = line.split(',').collect();
if parts.is_empty() {
continue;
}

let time_str = parts[0].trim();
let Ok(time_val) = time_str.parse::<f64>() else {
continue;
};

let relative_time = match first_time {
Some(first) => time_val - first,
None => {
first_time = Some(time_val);
0.0
}
};
times.push(relative_time);

let mut row: Vec<Value> = Vec::with_capacity(channels.len());
for part in parts.iter().skip(1).take(effective_count.saturating_sub(1)) {
let value = part.trim().parse::<f64>().unwrap_or(0.0);
row.push(Value::Float(value));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Allocating a Vec for every line in the log file is inefficient, especially for large logs. You can use the iterator directly to avoid these allocations.

            let mut parts = line.split(',');
            let Some(time_str) = parts.next() else {
                continue;
            };

            let Ok(time_val) = time_str.trim().parse::<f64>() else {
                continue;
            };

            let relative_time = match first_time {
                Some(first) => time_val - first,
                None => {
                    first_time = Some(time_val);
                    0.0
                }
            };
            times.push(relative_time);

            let mut row: Vec<Value> = Vec::with_capacity(channels.len());
            for part in parts.take(effective_count.saturating_sub(1)) {
                let value = part.trim().parse::<f64>().unwrap_or(0.0);
                row.push(Value::Float(value));
            }

Avoid allocating a Vec<&str> per line by using the split iterator
directly. Addresses PR review feedback from gemini-code-assist.
@SomethingNew71 SomethingNew71 merged commit cb65f93 into main Apr 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant