Skip to content

MoTeC .ld / .ldx Binary File Format Specification ​

Reverse-engineered from MoTeC i2 Pro telemetry files. Verified against real Oreca 07 LMP2 data. Sufficient to implement a complete parser.

Overview ​

A MoTeC telemetry session consists of two files:

FileFormatContents
.ldBinary, little-endianHeader, channel metadata (linked list), channel sample data
.ldxXML (UTF-8)Lap beacon timestamps, session metadata

.ld File Structure ​

All multi-byte integers are little-endian. Strings are Latin-1 (ISO 8859-1), null-terminated, zero-padded.

File Header (offset 0x00, ~512 bytes) ​

OffsetSizeTypeFieldNotes
0x004uint32magicAlways 0x00000040 (64 decimal). Reject file if different.
0x044uint32reservedAlways 0
0x084uint32channel_meta_ptrByte offset to first channel metadata block
0x0C4uint32channel_data_ptrByte offset to start of channel data area
0x10–0x2320—reservedZeros
0x244uint32event_ptrByte offset to event info block (0 = none)
0x28–0x5D54—reserved
0x5E16stringdateSession date, format "dd/MM/yyyy", e.g. "27/01/2023"
0x6E16—reserved
0x7E16stringtimeSession start time, format "HH:mm:ss", e.g. "12:00:00"
0x8E16—reserved
0x9E64stringdriverDriver name, e.g. "Mikkel Jensen"
0xDE64stringvehicle_idVehicle identifier, e.g. "MQ12Di_LMP2 #477"
0x11E64—reserved
0x15E64stringvenueTrack/venue name, e.g. "DIS"

Event Info Block (at event_ptr) ​

Optional. Only present when event_ptr > 0 and points within the file:

OffsetSizeTypeField
event_ptr + 064stringevent_name

Channel Metadata Block (124 bytes each, linked list) ​

Channels form a doubly-linked list. Each block is exactly 124 bytes (0x7C).

Start at channel_meta_ptr, follow next_addr until 0 or beyond file end.

OffsetSizeTypeFieldNotes
+0x004uint32prev_addrByte offset to previous channel block (0 for first)
+0x044uint32next_addrByte offset to next channel block (0 for last)
+0x084uint32data_ptrByte offset to this channel's sample data
+0x0C4uint32n_dataNumber of samples
+0x102uint16_idInternal channel ID (not used for parsing)
+0x122uint16datatype_aData type class (see table below)
+0x142uint16datatypeBytes per sample: 2 or 4
+0x162uint16rec_freqRecording frequency in Hz
+0x182int16shiftInteger conversion: additive offset
+0x1A2int16mulInteger conversion: multiplicative factor
+0x1C2int16scaleInteger conversion: divisor
+0x1E2int16dec_placesInteger conversion: decimal exponent
+0x2032stringnameChannel name, e.g. "Corr Speed"
+0x4012stringunitUnit string, e.g. "m/s", "km/h", "bar", "deg", "ratio"
+0x4C8string_short_nameShort name (often empty, not essential)
+0x544uint32_display_minDisplay range minimum (not needed for data)
+0x584uint32_display_maxDisplay range maximum
+0x5C4float32_range_maxPhysical range maximum (e.g. 200.0 for speed)
+0x604float32_range_minPhysical range minimum (e.g. 0.0 or -31.1)
+0x648—reserved
+0x6C16—paddingZeros to fill 124 bytes

Note: Fields prefixed with _ are informational/display only. Only the fields without _ prefix are required for correct data extraction.

Data Type Classes (datatype_a) ​

ValueMeaningData encoding
0x07FloatIEEE 754 float. No conversion needed.
0x03Signed integerApply integer-to-physical conversion formula.
OtherSigned integerTreat same as 0x03.

Channel Data ​

Located at data_ptr. Contiguous array of n_data samples.

Float data (datatype_a == 0x07) ​

datatypeRead asSize
4IEEE 754 float32 (little-endian)4 bytes × n_data
2float16 (rare, fallback to zeros)2 bytes × n_data

Values are already in physical units — no conversion needed.

Integer data (datatype_a != 0x07) ​

datatypeRead asSize
2Signed int16 (little-endian)2 bytes × n_data
4Signed int32 (little-endian)4 bytes × n_data

Conversion formula (integer → physical value):

scale_eff = (scale == 0) ? 1 : scale
mul_eff   = (mul == 0)   ? 1 : mul

value = (raw / scale_eff * 10^(-dec_places) + shift) * mul_eff

Example: P_F_BRAKE channel with scale=1, dec_places=1, shift=0, mul=1:

  • raw int16 value = 423
  • value = (423 / 1 × 10⁻¹ + 0) × 1 = 42.3 bar

Linked List Iteration ​

python
addr = header.channel_meta_ptr
channels = []
while addr > 0 and addr + 124 <= file_size:
    ch = parse_channel_meta(data, addr)
    channels.append(ch)
    if ch.next_addr == 0 or ch.next_addr <= addr:
        break  # end of list or backward pointer (corrupt)
    addr = ch.next_addr

Safety limit: cap at ~200 channels to protect against corrupt files.

.ldx File Structure ​

XML file, UTF-8 encoded. Same base filename as the .ld file.

Structure ​

xml
<?xml version="1.0"?>
<LDXFile>
  <Layers>
    <Layer>
      <MarkerBlock>
        <MarkerGroup>
          <Marker Time="0" />
          <Marker Time="67340000" />
          <Marker Time="135280000" />
        </MarkerGroup>
      </MarkerBlock>
    </Layer>
  </Layers>
</LDXFile>

Parsing ​

Extract all Time attributes from <Marker> elements using regex or XML parser:

Pattern: <Marker[^>]* Time="(\d+)"

Time Units ​

Time values are in microseconds (µs). Convert to seconds:

time_seconds = Time / 1_000_000

Lap Computation ​

Sort beacon times ascending. Each consecutive pair defines a lap:

for i in 0..(beacons.count - 2):
    lap.start = beacons[i]
    lap.end   = beacons[i + 1]
    lap.duration_ms = (lap.end - lap.start) * 1000

Edge cases:

  • 0 or 1 beacons: treat entire session as one lap (start=0, end=max_channel_duration)
  • .ldx file missing: same fallback

Common Channel Names ​

Names are case-insensitive for matching. Listed by priority (use first match).

Speed ​

NameTypical FreqTypeUnit
Corr Speed100 Hzfloat32m/s or km/h
Ground Speed100 Hzfloat32km/h
Wheel Speed AVG100 Hzfloat32km/h
Aero Speed20 Hzfloat32km/h

Throttle ​

NameTypical FreqTypeUnit
Driver Throttle Pos50 Hzfloat32ratio (0.0–1.0) or % (0–100)
Throttle Pos50 Hzfloat32ratio or %

Brake ​

NameTypical FreqTypeUnit
P_F_BRAKE100 Hzint16bar (via conversion)
Brake Pos100 Hzfloat32ratio (0.0–1.0)

Steering ​

NameTypical FreqTypeUnit
Steering Angle50 Hzfloat32deg (steering wheel degrees)

Gear ​

NameTypical FreqTypeUnit
Gear10 Hzfloat32integer (0=N, 1–6)

G-Forces ​

NameTypical FreqTypeUnit
G Force Long50–100 Hzfloat32g

Dampers ​

NameTypical FreqTypeUnit
X_FL_DAMPER200 Hzfloat32mm
X_FR_DAMPER200 Hzfloat32mm
X_RL_DAMPER200 Hzfloat32mm
X_RR_DAMPER200 Hzfloat32mm

Engine ​

NameTypical FreqTypeUnit
Engine RPM100 Hzfloat32rpm

Unit Normalization ​

Speed → km/h ​

UnitFactor
m/s× 3.6
km/h× 1.0
mph× 1.60934

Brake Pressure → bar ​

UnitFactor
bar× 1.0
psi× 0.0689476
kpa× 0.01

Throttle → 0.0–1.0 ​

  • Unit ratio: already 0.0–1.0
  • Unit % or values > 1.5: divide by 100
  • Unit deg: divide by 100
  • Unit rad: divide by 1.745 (100° in radians)

Steering → degrees ​

  • Unit deg: already degrees
  • Unit rad: multiply by 180/π

Resampling ​

Channels have different native frequencies (10 Hz to 200 Hz). Resample to a unified rate (typically 50 Hz) using linear interpolation:

output_count = floor(duration * target_freq) + 1

for i in 0..<output_count:
    t = i / target_freq           // seconds
    src_pos = t * src_freq        // fractional source index
    lo = floor(src_pos)
    hi = min(lo + 1, src_count - 1)
    frac = src_pos - lo
    output[i] = data[lo] + (data[hi] - data[lo]) * frac

Implementation Checklist ​

  1. ☐ Read file, validate magic == 0x40
  2. ☐ Parse header strings (date, time, driver, vehicle, venue)
  3. ☐ Walk channel linked list from channel_meta_ptr (124-byte blocks)
  4. ☐ For each channel: read n_data samples from data_ptr
  5. ☐ Float channels (datatype_a == 0x07): read as float32, no conversion
  6. ☐ Integer channels: read as int16/int32, apply conversion formula
  7. ☐ Parse .ldx companion file for beacon timestamps (microseconds)
  8. ☐ Compute laps from consecutive beacon pairs
  9. ☐ Map channel names (case-insensitive) to unified fields
  10. ☐ Apply unit conversions (speed→km/h, brake→bar, throttle→0-1)
  11. ☐ Resample all channels to common frequency
  12. ☐ Compute cumulative distance from speed (if no distance channel)

Robustness Notes ​

  • Bounds checking: data_ptr + n_data * datatype may exceed file size — clamp n_data
  • Empty channels: n_data == 0 is valid, return empty array
  • Missing .ldx: fall back to single-lap mode (entire session = one lap)
  • Corrupt linked list: next_addr == 0, next_addr <= current_addr, or next_addr >= file_size all terminate iteration
  • Division by zero: scale == 0 → use 1, mul == 0 → use 1
  • String encoding: Latin-1 (ISO 8859-1), not UTF-8