Player side TCP
OCP can expose a TCP server on the player (operator) side for external applications to communicate with OCP. This is the player-side equivalent of the vehicle-side TCP interface.
The player-side TCP server is always started, listening on port 4001 by default. The port can be overridden with the ocp_player_tcp_port plugin parameter.
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).
Configuration
The port can be overridden by setting the ocp_player_tcp_port plugin parameter:
ocp_player_tcp_port=4001
Protocol
The framing is the same as the vehicle-side TCP: each message starts with 4 bytes (unsigned int, little endian) defining the message length in bytes, followed by a UTF-8 JSON payload. 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),
data: JSON blob, size defined by `message_size`
}
Receiving data from OCP
OCP sends vehicle feedback data to the connected TCP client. The JSON follows the same schema as the ocp_vehicle_user_data message sent to the web view (see Operator side).
{
"vehicle_feedback": {
"<vehicle_name>": {
"ping_latency_ms": 10,
"message_latency_roundtrip": 25,
"ms_since_last_vehicle_data": 5,
"missing_cameras": null,
"sender_fault": [],
"receiver_fault": [],
"vehicle_data": {
"pos": { "lat": 57.7, "lon": 11.9 },
"user_data": {}
},
"last_remote_input": null,
"ack_time": 12345,
"ack_time_mac": 67890
}
},
"last_input": null
}
Sending data to OCP
The TCP client sends data to OCP using the same format as the web view ocp_client_user_data message:
{
"active_vehicle": "my_vehicle",
"client_user_data": {
"my_vehicle": {
"user_data": {
"steering_mode": "manual"
},
"ocp_disable_gamepad": false,
"ack_time_returned": 12345,
"ack_time_mac_returned": 67890
}
}
}
The active_vehicle field selects which vehicle receives gamepad input. With a single vehicle this can be left out. With multiple vehicles it must be set — if it is not set, no vehicle will receive gamepad input and all vehicles will raise the InputLost fault.
For latency measurement to work, the ack_time_returned and ack_time_mac_returned fields must be set to the latest ack_time and ack_time_mac values received from OCP. See Latency Measurement for details.
|
| OCP accepts client data from only one source. The first source that sends data (web view, custom plugin, or player-side TCP) becomes the locked source. If OCP detects data from any other source after locking, it logs an error and stops all client data communication, including from the source that was working. A restart is required to recover. See Client data source locking for details. |
Rust Example
This example connects to the player-side TCP server, prints the vehicle feedback data, and sends back client user data.
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
error::Error,
io::{Read, Write},
net::TcpStream,
};
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct InputClientUserData {
pub user_data: serde_json::Value,
pub ocp_disable_gamepad: Option<bool>,
pub ack_time_returned: Option<u64>,
pub ack_time_mac_returned: Option<u32>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ClientOcpShared {
pub active_vehicle: Option<String>,
pub client_user_data: HashMap<String, InputClientUserData>,
}
fn main() -> Result<(), Box<dyn Error>> {
loop {
match TcpStream::connect("127.0.0.1:4001") {
Ok(mut stream) => {
loop {
// Read vehicle feedback 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 feedback: serde_json::Value = serde_json::from_slice(buf.as_slice())?;
println!("Vehicle feedback: {{{feedback}}}");
// Extract ack_time from vehicle feedback for latency measurement
let (ack_time, ack_time_mac) = feedback["vehicle_feedback"]
.as_object()
.and_then(|vf| vf.values().next())
.map(|v| {
(
v["ack_time"].as_u64().unwrap_or(0),
v["ack_time_mac"].as_u64().unwrap_or(0) as u32,
)
})
.unwrap_or((0, 0));
// Send client data back to OCP
let vehicle_name = "my_vehicle".to_string();
let mut client_user_data = HashMap::new();
client_user_data.insert(
vehicle_name.clone(),
InputClientUserData {
user_data: serde_json::json!({"status": "ok"}),
ocp_disable_gamepad: Some(false),
ack_time_returned: Some(ack_time),
ack_time_mac_returned: Some(ack_time_mac),
},
);
let response = ClientOcpShared {
active_vehicle: Some(vehicle_name),
client_user_data,
};
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}"),
}
}
}