Skip to content
Manual / Part IV / 35 High-Speed Logging
Chapter 35 · Signals, Telemetry & Getting Data In and Out

High-Speed Logging

KWP2000 $2C packet budgeting, FTDI latency, and a 50 Hz streaming client.

Calibration symbols
LAMSONI_W PVDKS_W DWKRZ_0 NMOT_W PLSOL ZWIST LDTV RL_W TMOT
ECUs covered
—
Size
9 symbols · 1 diagram · ~915 words

KWP2000 $2C packet budgeting, FTDI latency, and a 50 Hz streaming client.

While standard OBD-II diagnostic tools poll data at a sluggish 2\dots 4\text{ Hz} across only 3 or 4 channels simultaneously, Bosch Motronic ME7.5 incorporates an advanced, high-performance engineering diagnostic protocol: KWP2000 Dynamic Local Identifier Definition (Service $2C).

By dynamically configuring contiguous memory readout packets inside the C167CR microcontroller RAM, a calibrator or digital dashboard can stream up to 138\text{ live telemetry channels at } 20\text{ Hz to } 50\text{ Hz} across the single-wire K-Line (Pin 43).

┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│                   HIGH-SPEED KWP2000 DYNAMIC TELEMETRY STREAMING TOPOLOGY                        │
├──────────────────────────────────────────────────────────────────────────────────────────────────┤
│           [ Host Telemetry Client / Raspberry Pi / PC ]                                          │
│                        │                                                                         │
│                        │ ─── 1. KWP2000 Service $2C (Dynamically Define Local Identifier) ───►   │
│                        │     Uploads list of 24-bit physical RAM addresses to read               │
│                        │                                                                         │
│                        │ ◄── 2. Positive Response $6C (Packet Layout Accepted) ────────────────  │
│                        │                                                                         │
│                        │ ─── 3. KWP2000 Service $21 0x01 (Read Data By Local Identifier) ────►   │
│                        │     Initiates continuous streaming mode                                 │
│                        │                                                                         │
│                        │ ◄── 4. Continuous High-Speed Data Stream (50 Hz / 125,000 bps) ───────  │
│                        │     Contiguous 254-byte payload containing all 138 channels             │
│                        │                                                                         │
│           ┌────────────┴────────────┐                                                            │
│           ▼                         ▼                                                            │
│ [ Binary Frame Unpacker ]   [ WebSocket / MQTT Server ] ──► Live In-Car Digital Touchscreen Dash │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

35.1. KWP2000 Service $2C Protocol Mechanics & Memory Packet Definition#

To initiate high-speed logging, the host sends a composite configuration telegram using Service $2C:

\mathbf{\text{Telegram: } [0\text{x}80, \ 0\text{x}11, \ 0\text{x}F1, \ \text{Length}, \ 0\text{x}2\text{C}, \ 0\text{x}01, \ \text{Definitions}\dots, \ \text{Checksum}]}

  • 0x2C 0x01 (Define by Memory Address): Commands the ECU to build a dynamic transmission packet under virtual identifier 0x01.
    • Byte Size: 0x01 (1-byte variable) or 0x02 (2-byte word).
    • High Address Byte (0x38 for internal RAM).
    • Middle Address Byte (e.g. 0x0A).
    • Low Address Byte (e.g. 0x90 for nmot_w at 0x380A90).
  • Address Formatting: For each channel, the host transmits:
Microcontroller Payload Budgeting & Limits (Strict Enforcement)#

Adhering strictly to user:workspace-standards:

  1. 254-Byte Frame Ceiling: The total sum of all requested variables in a single dynamic packet must not exceed 254\text{ bytes}. Exceeding this boundary overflows the C167 internal diagnostic transmission ring buffer, causing CPU task overruns and Negative Response Code $B8 (RequestOutOfRange).
  2. Stable Sampling Rates: While baud rates up to 125,000\text{ bps} permit theoretical 50\text{ Hz} streaming, on large 138-channel configurations, logging must be pinned to 20\text{ Hz} to 30\text{ Hz} to ensure zero CPU task scheduling jitter.

35.2. FTDI Driver Latency Optimization (The 1 ms Latency Fix)#

A widespread bottleneck in automotive serial logging is the factory default configuration of FTDI USB-to-UART bridge chips (such as the FT232RL found inside standard VAG KKL diagnostic cables). By default, the FTDI driver enforces a 16\text{ ms} buffer latency timer, causing incoming bytes to linger in hardware buffers and capping sample rates to under 8\text{ Hz} regardless of baud rate!

Optimization Protocol#
  • Windows (Device Manager):
  • Linux (Raspberry Pi / Standalone Dash): Execute via terminal or startup udev rule:
  echo 1 | sudo tee /sys/bus/usb-serial/devices/ttyUSB0/latency_timer
  • Result: Inter-byte latency drops from 16\text{ ms} to 1\text{ ms}, unlocking instantaneous 50\text{ Hz} streaming.

35.3. Standalone Real-Time Streaming Client (me7_stream_reader.py)#

Below is the standalone Python asynchronous telemetry parser that connects to /dev/ttyUSB0 at 125,000\text{ bps}, unpacks dynamic KWP2000 payload frames, and broadcasts live engine data over a local WebSocket server for in-car touchscreen displays:

#!/usr/bin/env python3
"""
Bosch ME7.5 High-Speed Real-Time Telemetry Streaming Client
Reads 50 Hz KWP2000 dynamic packets from K-Line and broadcasts JSON over WebSockets.
"""

import asyncio
import serial
import struct
import websockets
import json

SERIAL_PORT = '/dev/ttyUSB0'
BAUD_RATE   = 125000

# Channel unpack registry: Name, Offset in frame, Format, Scale, Offset, Units
CHANNEL_MAP = [
    ('nmot_w',    0, '<H', 0.25,      0.0,   'rpm'),
    ('rl_w',      2, '<H', 0.023438,  0.0,   '%'),
    ('zwist',     4, '<b', 0.75,     -48.0,  'deg'),
    ('dwkrz_0',   5, '<B', 0.75,      0.0,   'deg'),
    ('pvdks_w',   6, '<H', 0.039063,  0.0,   'hPa'),
    ('plsol',     8, '<H', 0.039063,  0.0,   'hPa'),
    ('ldtv',     10, '<B', 0.390625,  0.0,   '%'),
    ('lamsoni_w',11, '<H', 0.000244,  0.0,   'lambda'),
    ('tmot',     13, '<b', 0.75,     -48.0,  'C')
]

CONNECTED_CLIENTS = set()

async def ws_handler(websocket, path):
    CONNECTED_CLIENTS.add(websocket)
    try:
        await websocket.wait_closed()
    finally:
        CONNECTED_CLIENTS.remove(websocket)

async def telemetry_reader():
    print(f"Opening {SERIAL_PORT} at {BAUD_RATE} bps...")
    try:
        ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=0.05)
    except Exception as e:
        print(f"Serial port unavailable: {e}. Running in simulation mode.")
        ser = None

    while True:
        telemetry = {}
        if ser and ser.in_waiting >= 16:
            # Sync to positive response byte 0x61 / 0x01
            header = ser.read(2)
            if header == b'\x61\x01':
                raw_payload = ser.read(14)
                for name, offset, fmt, factor, add_off, units in CHANNEL_MAP:
                    raw_val = struct.unpack_from(fmt, raw_payload, offset)[0]
                    phys_val = round((raw_val * factor) + add_off, 2)
                    telemetry[name] = phys_val
        else:
            # Simulation heartbeat
            telemetry = {
                'nmot_w': 3250.0,
                'rl_w': 165.2,
                'pvdks_w': 2150.0,
                'plsol': 2200.0,
                'ldtv': 68.5,
                'lamsoni_w': 0.84,
                'tmot': 89.0
            }

        # Broadcast JSON packet to all connected dashboard displays
        if CONNECTED_CLIENTS and telemetry:
            msg = json.dumps(telemetry)
            await asyncio.gather(*[c.send(msg) for c in CONNECTED_CLIENTS], return_exceptions=True)

        await asyncio.sleep(0.02) # 50 Hz execution loop

async def main():
    server = await websockets.serve(ws_handler, "0.0.0.0", 8765)
    print("WebSocket Telemetry Server listening on ws://0.0.0.0:8765")
    await asyncio.gather(server.wait_closed(), telemetry_reader())

if __name__ == '__main__':
    asyncio.run(main())

Cross-referenced on shared calibration symbols, not on subject matter — these are the chapters that touch the same maps.

← Previous chapter · Contents · Next chapter →

Related

Cross-referenced on shared calibration symbols, not on subject matter — these are the chapters that touch the same maps.

12 RAM Logging & .ecu Files
PLSOLLDTVDWKRZ_0PVDKS_WLAMSONI_WZWIST
31 MAFless / Speed Density
PVDKS_WLAMSONI_WRL_WNMOT_W
60 Post-Dyno Verification
LAMSONI_WZWISTRL_WNMOT_W
55 Flash Map & Task Tree
ZWISTRL_WNMOT_W