# 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) ```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) ```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 ```bash python tools/uart_capture.py --port /dev/cu.usbmodem* --timeout 30 ``` ### Using screen (manual) ```bash screen /dev/cu.usbmodem* 57600 # Press Ctrl-A, H to start logging to file # Press Ctrl-A, K to kill session ``` ### Using Python serial ```python 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 ```python 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() ```