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:
Boot Banner - Version string on startup
Boot Telemetry - Initial status with firmware info (contains
e99)Regular Telemetry - Continuous 1Hz updates
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
Timestamp locally - Add timestamps when receiving, not from device
Buffer lines - Telemetry may span multiple reads
Handle corruption - Ignore unparseable lines
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()