Telemetry Format

The AddVantage PPG outputs telemetry data via UART at 57600 baud, 8N1. This guide documents the telemetry format and how to parse it.

Overview

There are three types of serial output:

  1. Boot Banner - Version string on startup

  2. Boot Telemetry - Initial status with firmware info (contains e99)

  3. Regular Telemetry - Continuous 1Hz updates

Boot Banner

Sent once after reset:

addvantage PPG V3.2.7 250kbit

Format: addvantage PPG V{version} {baud}kbit

Field

Description

version

Firmware version (e.g., 3.2.7)

baud

CAN baud rate in kbit (e.g., 250)

Legacy Format: Older firmware uses V327 instead of V3.2.7

Boot Telemetry

Sent once after banner, identified by e99 marker:

d0g0e99p0r0t0c0v0k0b250f327w1x0y100z0V3.2.7

Field Breakdown

Prefix

Field

Description

Units

d

diesel_rate

Diesel fuel rate

L/h × 10

g

gas_rate

Gas fuel rate

L/h × 10

e

error_code

Always 99 for boot

-

p

pressure

Gas pressure

PSI

r

rpm

Engine RPM

RPM

t

torque

Engine torque

%

c

coolant_temp

Coolant temperature

°C offset +40

v

gas_level

Gas tank level

%

k

distance

Trip distance

km

b

baud_div10

CAN baud / 10

25 = 250kbit

f3

firmware_ver

Firmware version

327 = 3.2.7

w

fuel_map

Active fuel map

1-4

x

rpm_offset

RPM axis offset

bins

y

multiplier

Base fuel multiplier

%

z

deadtime

Injector dead time

0.1ms

V

version_string

Version string (optional)

X.Y.Z

Parsing Example (Python)

import re

def parse_boot_telemetry(line):
    pattern = (
        r'd(\d+)g(\d+)e99p(\d+)r(\d+)t(\d+)c(\d+)'
        r'v(\d+)k(\d+)b(\d+)0f3(\d+)w(\d+)x(\d+)y(\d+)z(\d+)'
        r'(?:V([0-9.]+))?'
    )
    match = re.search(pattern, line)
    if match:
        return {
            'diesel_rate': int(match.group(1)),
            'gas_rate': int(match.group(2)),
            'pressure': int(match.group(3)),
            'rpm': int(match.group(4)),
            'torque': int(match.group(5)),
            'coolant_temp': int(match.group(6)) - 40,  # Offset
            'gas_level': int(match.group(7)),
            'distance': int(match.group(8)),
            'baud_kbit': int(match.group(9)) * 10,
            'firmware_ver': match.group(10),
            'fuel_map': int(match.group(11)),
            'rpm_offset': int(match.group(12)),
            'multiplier': int(match.group(13)),
            'deadtime': int(match.group(14)) / 10.0,  # Convert to ms
            'version': match.group(15),
        }
    return None

Regular Telemetry

Sent every 1 second during operation:

d45g23e0p35r1250t55c85v75k123j...x...y...z...n...o...q...

Core Fields

Prefix

Field

Description

Units

d

diesel_rate

Diesel fuel rate

L/h × 10

g

gas_rate

Gas fuel rate

L/h × 10

e

error_code

Current error (0 = none)

code

p

pressure

Gas pressure

PSI

r

rpm

Engine RPM

RPM

t

torque

Engine torque

%

c

coolant_temp

Coolant temperature

°C + 40

v

gas_level

Gas tank level

%

k

distance

Trip distance

km

Extended Fields (j onwards)

Prefix

Field

Description

j

injector_pw

Injector pulse width (μs)

x

map_correction

MAP-based correction (%)

y

temp_correction

Temperature correction (%)

z

pressure_correction

Diff pressure correction (%)

n

raw_adc_pressure

Raw ADC gas pressure

o

raw_adc_map

Raw ADC manifold pressure

q

timing_advance

Calculated timing advance

Parsing Example (Python)

def parse_telemetry(line):
    pattern = r'd(\d+)g(\d+)e(\d+)p(\d+)r(\d+)t(\d+)c(\d+)v(\d+)k(\d+)'
    match = re.search(pattern, line)
    if match:
        return {
            'diesel_rate': int(match.group(1)) / 10.0,  # L/h
            'gas_rate': int(match.group(2)) / 10.0,     # L/h
            'error_code': int(match.group(3)),
            'pressure': int(match.group(4)),            # PSI
            'rpm': int(match.group(5)),                 # RPM
            'torque': int(match.group(6)),              # %
            'coolant_temp': int(match.group(7)) - 40,   # °C
            'gas_level': int(match.group(8)),           # %
            'distance': int(match.group(9)),            # km
        }
    return None

Error Codes

Code

Description

Action

0

No error

Normal operation

1

Low gas pressure

Check tank/regulator

2

High coolant temp

Reduce load, check cooling

3

CAN timeout

Check CAN bus connection

4

Sensor fault

Check ADC inputs

5

Over-speed

RPM exceeded limit

99

Boot marker

Not an error

Temperature Offset

Coolant temperature is transmitted with a +40°C offset to allow negative temperatures without signed values:

Transmitted

Actual

0

-40°C

40

0°C

60

20°C

100

60°C

140

100°C

Capturing Telemetry

Using uart_capture.py

python tools/uart_capture.py --port /dev/cu.usbmodem* --timeout 30

Using screen (manual)

screen /dev/cu.usbmodem* 57600
# Press Ctrl-A, H to start logging to file
# Press Ctrl-A, K to kill session

Using Python serial

import serial

with serial.Serial('/dev/cu.usbmodem*', 57600, timeout=1) as ser:
    while True:
        line = ser.readline().decode('utf-8', errors='ignore').strip()
        if line:
            data = parse_telemetry(line)
            if data:
                print(f"RPM: {data['rpm']}, Torque: {data['torque']}%")

VT100 Display Mode

Press v in the serial console to toggle VT100 display mode. This shows a formatted dashboard:

+---------------------------+
| AddVantage PPG v3.2.7     |
+---------------------------+
| RPM:     1250  Torque: 55%|
| Coolant:  85°C Gas P: 35  |
| Diesel: 4.5 L/h Gas: 2.3  |
| Level:   75%   Error: 0   |
+---------------------------+

VT100 mode uses ANSI escape codes for positioning and color. Use a terminal that supports VT100 (PuTTY, iTerm2, minicom).

Data Logging Best Practices

  1. Timestamp locally - Add timestamps when receiving, not from device

  2. Buffer lines - Telemetry may span multiple reads

  3. Handle corruption - Ignore unparseable lines

  4. Log raw data - Store raw lines alongside parsed data

Example Logger

import serial
import time
import csv

with serial.Serial('/dev/cu.usbmodem*', 57600, timeout=1) as ser:
    with open('telemetry.csv', 'w', newline='') as f:
        writer = csv.writer(f)
        writer.writerow(['timestamp', 'rpm', 'torque', 'coolant', 'gas_rate'])

        while True:
            line = ser.readline().decode('utf-8', errors='ignore').strip()
            if line and line.startswith('d'):
                data = parse_telemetry(line)
                if data and data['error_code'] != 99:  # Skip boot
                    writer.writerow([
                        time.time(),
                        data['rpm'],
                        data['torque'],
                        data['coolant_temp'],
                        data['gas_rate'],
                    ])
                    f.flush()