Vehicle side

All communication with the Oden Control Pipeline (OCP) plugin is done by connecting to the TCP server. OCP sends control commands and telemetry data, and the vehicle integration code must send a response containing timestamps and optional vehicle data. The TCP server uses port 4000 unless changed via plugin arguments. By default, the server only accepts connections from localhost (127.0.0.1). To allow connections from remote hosts, set the ocp_tcp_allow_remote plugin parameter (see Settings).

Messages sent from OCP start with 4 bytes (unsigned int, little endian) defining the message length in bytes, followed by a UTF-8 JSON message containing the controller state. The maximum message size is 16 KB — messages exceeding this limit will be dropped and generate a warning message.

{
    message_size: Unsigned int (4 bytes, little endian, max 16384),
    ocp_data: JSON blob, size defined by `message_size`
}

The JSON fields follow this schema:

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "OCP control commands",
    "type": "object",
    "properties": {
        "controller": {
            "type": "object",
            "properties": {
                "buttons": {
                    "type": "object",
                    "properties": {
                        "a": {
                            "type": "boolean"
                        },
                        "b": {
                            "type": "boolean"
                        },
                        "x": {
                            "type": "boolean"
                        },
                        "y": {
                            "type": "boolean"
                        },
                        "left_bumper": {
                            "type": "boolean"
                        },
                        "right_bumper": {
                            "type": "boolean"
                        },
                        "back": {
                            "type": "boolean"
                        },
                        "start": {
                            "type": "boolean"
                        },
                        "guide": {
                            "type": "boolean"
                        },
                        "left_thumb": {
                            "type": "boolean"
                        },
                        "right_thumb": {
                            "type": "boolean"
                        },
                        "dpad_up": {
                            "type": "boolean"
                        },
                        "dpad_right": {
                            "type": "boolean"
                        },
                        "dpad_down": {
                            "type": "boolean"
                        },
                        "dpad_left": {
                            "type": "boolean"
                        }
                    },
                    "required": [
                        "a",
                        "b",
                        "x",
                        "y",
                        "left_bumper",
                        "right_bumper",
                        "back",
                        "start",
                        "guide",
                        "left_thumb",
                        "right_thumb",
                        "dpad_up",
                        "dpad_right",
                        "dpad_down",
                        "dpad_left"
                    ]
                },
                "axes": {
                    "type": "object",
                    "properties": {
                        "left_x": {
                            "type": "number"
                        },
                        "left_y": {
                            "type": "number"
                        },
                        "right_x": {
                            "type": "number"
                        },
                        "right_y": {
                            "type": "number"
                        },
                        "left_trigger": {
                            "type": "number"
                        },
                        "right_trigger": {
                            "type": "number"
                        }
                    },
                    "required": [
                        "left_x",
                        "left_y",
                        "right_x",
                        "right_y",
                        "left_trigger",
                        "right_trigger"
                    ]
                }
            },
            "required": [
                "buttons",
                "axes"
            ]
        },
        "telemetry": {
            "type": "object",
            "properties": {
                "latency": {
                    "type": "number"
                },
                "fault": {
                    "type": "boolean"
                },
                "fault_reason": {
                    "type": "string"
                }
            },
            "required": [
                "latency",
                "fault",
                "fault_reason"
            ]
        },
        "ack_time": {
            "type": "number",
            "description": "Timestamp used for latency measurement (u64), must be passed back to OCP"
        },
        "ack_time_mac": {
            "type": "number",
            "description": "Hashed timestamp (u32), must be passed back to OCP"
        },
        "client_user_data": {
            "type": "object"
        }
    },
    "required": [
        "telemetry",
        "ack_time",
        "ack_time_mac"
    ]
}

The client_user_data field is optional and contains any data that is passed to OCP on the operator side.

Response message

The vehicle MUST send a response message back to OCP over the same TCP connection. The response MUST include ack_time and ack_time_mac copied unmodified from the control message — these are required for round-trip latency measurement. If no response is sent, or the timestamps are missing, the HighLatency fault will be raised. See Latency Measurement for how timestamps flow through the system.

The vehicle_user_data field is optional and can be used to send custom data back to the operator.

The response follows the same framing as the control message: 4 bytes (unsigned int, little endian) specifying the size of the JSON message. The following JSON schema applies:

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "OCP respond message",
    "type": "object",
    "properties": {
        "ack_time": {
            "type": "number",
            "description": "Timestamp used for latency measurement(u64). The value should be the latest received by OCP."
        },
        "ack_time_mac": {
            "type": "number",
            "description": "Hashed timestamp for latency measurement(u32). The value should be the latest received by OCP."
        },
        "vehicle_user_data": {
            "type": "object",
            "properties": {
                "pos": {
                    "type": "object",
                    "properties": {
                        "lat": {
                            "type": "number"
                        },
                        "lon": {
                            "type": "number"
                        }
                    },
                    "required": [
                        "lat",
                        "lon"
                    ]
                },
                "user_data": {
                    "type": "object"
                }
            },
            "required": [
                "user_data"
            ]
        }
    },
    "required": [
        "ack_time",
        "ack_time_mac"
    ]
}

Rust Example

This example connects to the TCP server, prints the control message received from OCP, and sends back a dummy message with user data. The Rust program uses the crates serde and serde_json.

use serde::{Deserialize, Serialize};
use std::{
    error::Error,
    io::{Read, Write},
    net::TcpStream,
};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Controller {
    pub buttons: Buttons,
    pub axes: Axes,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Buttons {
    pub a: bool,
    pub b: bool,
    pub x: bool,
    pub y: bool,
    pub left_bumper: bool,
    pub right_bumper: bool,
    pub back: bool,
    pub start: bool,
    pub guide: bool,
    pub left_thumb: bool,
    pub right_thumb: bool,
    pub dpad_up: bool,
    pub dpad_right: bool,
    pub dpad_down: bool,
    pub dpad_left: bool,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Axes {
    pub left_x: f32,
    pub left_y: f32,
    pub right_x: f32,
    pub right_y: f32,
    pub left_trigger: f32,
    pub right_trigger: f32,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Telemetry {
    pub latency: u32,
    pub fault: bool,
    pub fault_reason: String,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct VehicleControlMessage {
    pub controller: Option<Controller>,
    pub telemetry: Telemetry,
    pub ack_time: u64,
    pub ack_time_mac: u32,
    pub client_user_data: Option<serde_json::Value>, // Optional data sent from operator side.
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct VehicleData {
    pub speed: f32,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct LatLong {
    pub lat: f64,
    pub lon: f64,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct VehicleUserData {
    pub pos: Option<LatLong>,
    pub user_data: VehicleData,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct VehicleResponseMessage {
    pub ack_time: u64,
    pub ack_time_mac: u32,
    pub vehicle_user_data: Option<VehicleUserData>,
}

fn main() -> Result<(), Box<dyn Error>> {
    loop {
        match TcpStream::connect("127.0.0.1:4000") {
            Ok(mut stream) => {
                loop {
                    // Read message from OCP
                    let length = {
                        let mut buf: Vec<u8> = vec![0; 4];

                        stream.read_exact(buf.as_mut_slice())?;
                        u32::from_le_bytes(buf.try_into().unwrap())
                    };

                    let mut buf: Vec<u8> = vec![0; length as usize];
                    stream.read_exact(buf.as_mut_slice())?;

                    let control_msg: VehicleControlMessage = serde_json::from_slice(buf.as_slice())?;
                    println!("{control_msg:?}");

                    // Send response message back to OCP
                    let response = VehicleResponseMessage {
                        ack_time: control_msg.ack_time,
                        ack_time_mac: control_msg.ack_time_mac,
                        vehicle_user_data: Some(VehicleUserData {
                            pos: None,
                            user_data: VehicleData { speed: 1.0 },
                        }),
                    };

                    let message_json = serde_json::to_vec(&response)?;
                    let message_length = message_json.len() as u32;
                    stream.write_all(&message_length.to_le_bytes())?;
                    stream.write_all(&message_json)?;
                }
            }
            Err(e) => println!("Error: {e}"),
        }
    }
}