Operator-Side Control
Operator-side OCP integration is optional. Add it when the operator station needs a custom dashboard, custom input path, active-vehicle selection, automation, or an external process that exchanges data with the vehicle through OCP.
For the complete control-path model, start with Oden Control Pipeline overview. For webview layout and general JavaScript SDK behavior, see Webview and JavaScript SDK.
Choose an operator-side path
| Path | Use it for | What handles timestamps |
|---|---|---|
Webview |
Browser-based operator UI inside OdenVR / Player. |
The injected |
Player-side TCP |
A local external application, service, or test tool. |
Your TCP client must copy |
Custom Oden plugin |
A plugin that needs direct access to Oden shared data or another native input device. |
Your plugin must publish the returned timestamp fields in |
Only one client data source may send OCP client data during a Player session.
The first source that sends data locks the session to Webview, TCP, or Plugin.
If another source sends data later, OCP logs a source-conflict error, stops client data processing, and requires a Player restart.
A web page that creates OdenLayoutClient can auto-echo OCP timestamps even when your application code has not sent custom OCP data yet.
If you intend to use Player-side TCP or a custom plugin as the operator data source, do not also run a webview that participates in OCP.
|
Receive vehicle feedback
All operator-side paths receive the same logical feedback object.
It is keyed by vehicle name, so use those keys when setting active_vehicle or sending per-vehicle user data.
{
"vehicle_feedback": {
"vehicle-1": {
"ping_latency_ms": 8,
"message_latency_roundtrip": 42,
"ms_since_last_vehicle_data": 12,
"missing_cameras": null,
"sender_fault": [],
"receiver_fault": [],
"vehicle_data": {
"pos": { "lat": 57.7, "lon": 11.9 },
"user_data": { "battery": 0.82 }
},
"last_remote_input": null,
"ack_time": 123456,
"ack_time_mac": 654321
}
},
"last_input": null
}
Use these fields first:
vehicle_feedback-
Map from vehicle name to feedback for that vehicle.
vehicle_data.user_data-
Custom JSON returned by the vehicle-side integration. OCP repeats the latest received vehicle data; use
ms_since_last_vehicle_datato detect stale data. sender_fault/receiver_fault-
Operator-side and vehicle-side OCP faults as strings.
last_remote_input-
Controller input last reported back from the vehicle side.
ack_time/ack_time_mac-
Timestamp values for the OCP latency loop. Webview code normally does not handle them directly. TCP clients and custom plugins must echo them back.
last_input-
Last local gamepad/controller input sent by the operator station. It is
nullwhen no input is available or OCP gamepad forwarding is disabled. last_remote_input-
Last input reported back from the vehicle side. Use it to show what command the vehicle most recently received, not just what the operator most recently pressed.
missing_cameras-
List of monitored camera names whose drop detector reported missing frames. It is
nullwhen monitored cameras are healthy. Cameras without drop detection are not listed.
Send operator data
Send ClientOcpShared data back to OCP when you need to select a vehicle, attach operator-side user data, or disable OCP’s built-in gamepad forwarding.
{
"active_vehicle": "vehicle-1",
"client_user_data": {
"vehicle-1": {
"user_data": {
"mode": "manual",
"requested_tool": "bucket"
},
"ocp_disable_gamepad": false,
"ack_time_returned": 123456,
"ack_time_mac_returned": 654321
}
}
}
active_vehicle-
Selects which vehicle receives OCP gamepad input. With one connected vehicle, it may be omitted. With multiple vehicles, set it to one of the keys from
vehicle_feedback; otherwise OCP marks vehicle control inactive. client_user_data-
Map from vehicle name to data for that vehicle. The vehicle-side TCP message exposes this as
client_user_data. user_data-
Your custom JSON payload. If you use it as a custom control command path, the vehicle-side integration must explicitly read and validate it.
ocp_disable_gamepad-
Set to
truewhen OCP should not forward its gamepad controller object. This also suppresses the operator-sideInputLostfault. ack_time_returned/ack_time_mac_returned-
Required for Player-side TCP and custom plugin sources. Copy the latest values from that vehicle’s feedback. Webview sends these fields automatically.
Webview task
Use the webview path when the operator UI runs inside OdenVR / Player.
The page receives ocp_vehicle_user_data and sends ocp_client_user_data through the injected JavaScript SDK.
-
Enable the
Oden Control Pipelineglobal plugin. -
Enable the
WebViewglobal plugin. -
Point the WebView
Addresssetting to your web app. -
Create one shared
OdenLayoutClientinstance in the page. -
Register a callback for
ocp_vehicle_user_data. -
Send
ocp_client_user_dataonly when this webview is the intended OCP client data source.
const odenClient = getOrCreateOdenLayoutClient();
function onOcpFeedback(payload) {
const entries = Object.entries(payload.vehicle_feedback ?? {});
if (entries.length === 0) return;
const [vehicleName, feedback] = entries[0];
const vehicleData = feedback.vehicle_data?.user_data ?? {};
console.log("Vehicle data", vehicleName, vehicleData);
odenClient?.sendNamedUserMessage("ocp_client_user_data", {
active_vehicle: vehicleName,
client_user_data: {
[vehicleName]: {
user_data: {
selected_mode: "work",
},
ocp_disable_gamepad: false,
},
},
});
}
odenClient?.registerUserMessageCallback("ocp_vehicle_user_data", onOcpFeedback);
// Later, when the component or page is destroyed:
// odenClient?.unregisterUserMessageCallback("ocp_vehicle_user_data", onOcpFeedback);
The WebView client tracks the latest ack_time and ack_time_mac per vehicle.
When your code calls sendNamedUserMessage("ocp_client_user_data", …), the client injects ack_time_returned and ack_time_mac_returned into each matching vehicle entry.
If no custom JavaScript sends ocp_client_user_data, the client can still auto-echo timestamps with an empty user-data payload.
That keeps latency measurement alive for webview-only operator stations, but it also means the webview becomes the OCP client data source.
Player-side TCP task
Use Player-side TCP when an external application should run beside OdenVR / Player. OCP starts the Player-side TCP server during OCP initialization.
Defaults:
-
Listen address:
127.0.0.1 -
Port:
4001 -
Port parameter:
ocp_player_tcp_port -
Remote bind parameter:
ocp_tcp_allow_remote -
Active clients: one at a time
-
Frame format: 4-byte unsigned little-endian payload length, then UTF-8 JSON
-
Maximum payload: 16 KB
To change the port:
oden-vr --plugin-param ocp_player_tcp_port 4101
To allow non-local TCP clients, set ocp_tcp_allow_remote.
This binds OCP TCP servers to 0.0.0.0 instead of 127.0.0.1.
Use this only on a protected network and open the firewall deliberately.
oden-vr --plugin-param ocp_tcp_allow_remote 1
A TCP client loop should:
-
Connect to
127.0.0.1:4001unless the port was changed. -
Read a 4-byte little-endian length.
-
Read that many JSON bytes and parse
VehicleOcpShared. -
Store the latest
ack_timeandack_time_macfor each vehicle. -
Send
ClientOcpSharedwith a 4-byte length prefix. -
Include the latest
ack_time_returnedandack_time_mac_returned. -
Reconnect if the socket closes.
// Pseudocode: frame every TCP JSON payload as:
writeUInt32LE(Buffer.byteLength(json));
write(json);
This minimal Rust example shows the Player-side TCP framing and timestamp echo:
use serde_json::{json, Value};
use std::{
error::Error,
io::{Read, Write},
net::TcpStream,
};
fn read_json(stream: &mut TcpStream) -> Result<Value, Box<dyn Error>> {
let mut len_buf = [0_u8; 4];
stream.read_exact(&mut len_buf)?;
let len = u32::from_le_bytes(len_buf);
if len > 16 * 1024 {
return Err("OCP frame too large".into());
}
let mut payload = vec![0_u8; len as usize];
stream.read_exact(&mut payload)?;
Ok(serde_json::from_slice(&payload)?)
}
fn write_json(stream: &mut TcpStream, value: &Value) -> Result<(), Box<dyn Error>> {
let payload = serde_json::to_vec(value)?;
stream.write_all(&(payload.len() as u32).to_le_bytes())?;
stream.write_all(&payload)?;
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
let mut stream = TcpStream::connect("127.0.0.1:4001")?;
stream.set_nodelay(true)?;
loop {
let feedback = read_json(&mut stream)?;
let (vehicle_name, vehicle) = feedback["vehicle_feedback"]
.as_object()
.and_then(|vehicles| vehicles.iter().next())
.ok_or("no vehicle feedback")?;
let ack_time = vehicle["ack_time"].as_u64().unwrap_or(0);
let ack_time_mac = vehicle["ack_time_mac"].as_u64().unwrap_or(0) as u32;
let mut client_user_data = serde_json::Map::new();
client_user_data.insert(
vehicle_name.clone(),
json!({
"user_data": { "operator_app": "running" },
"ocp_disable_gamepad": false,
"ack_time_returned": ack_time,
"ack_time_mac_returned": ack_time_mac
}),
);
let response = json!({
"active_vehicle": vehicle_name,
"client_user_data": client_user_data
});
write_json(&mut stream, &response)?;
}
}
The example at plugins/ocp_operator_example contains a fuller Rust implementation.
Custom plugin task
Use a custom Oden plugin when the operator integration needs native Oden APIs or shared memory instead of TCP.
OCP publishes feedback as named shared data:
-
vehicle_ocp_size: 32-bit little-endian byte count. -
vehicle_ocp: CBOR-encodedVehicleOcpShared.
The plugin sends client data back as:
-
client_ocp_size: 32-bit little-endian byte count. -
client_ocp: CBOR-encodedClientOcpShared.
The same source-locking rule applies.
If a plugin publishes client_ocp, do not also send ocp_client_user_data from a webview or Player-side TCP client in the same Player session.
Vehicle selection
OCP discovers vehicles on the Player side before it can route operator data.
- With Fleet active
-
OCP uses Fleet’s vehicle-name-to-UUID mapping and supports multiple vehicles.
- Without Fleet
-
OCP falls back to the scene and supports exactly one
Remote Streamer.
For multi-vehicle operation, send active_vehicle from the operator-side path.
Use the exact vehicle name from vehicle_feedback.
Only the active vehicle receives OCP gamepad input.
Troubleshooting
- No
ocp_vehicle_user_datain the webview -
Check that the OCP global plugin is enabled, the WebView page creates
OdenLayoutClient, and the Player has discovered a vehicle. Without Fleet, the scene must contain exactly oneRemote Streamer. - Player-side TCP cannot connect
-
Confirm OdenVR / Player is running with OCP enabled and listening on
127.0.0.1:4001or the configuredocp_player_tcp_port. Only use a remote host ifocp_tcp_allow_remoteis set. Only one TCP client is served at a time. - OCP reports a client data source conflict
-
Restart the Player and send OCP client data from only one source. Watch for a webview
OdenLayoutClientauto-echoing timestamps while a TCP app or plugin is also sending data. - Multiple vehicles are connected but none receives input
-
Send
active_vehicleand make sure it matches avehicle_feedbackkey. Without Fleet, OCP does not support more than oneRemote Streamer. InputLostappears while using custom controls-
Set
ocp_disable_gamepad: truein the per-vehicle client data. Then make sure the vehicle-side integration reads your customuser_data; OCP will no longer send a gamepadcontrollerobject for that vehicle. - Latency measurement stops or
Latency Loop Not Closed/InvalidMacappears -
For TCP or plugin integrations, echo the latest
ack_timeandack_time_macunchanged asack_time_returnedandack_time_mac_returned. For webviews, include aclient_user_dataentry for the same vehicle key so the SDK can inject the values. Also verify the vehicle-side integration returns the Streamer-sideack_timeandack_time_mac. - Vehicle data is stale
-
Check
ms_since_last_vehicle_data. The operator side repeats the last vehicle data until the vehicle-side TCP integration sends newvehicle_user_data. - TCP messages are ignored
-
Keep payloads under 16 KB and send valid UTF-8 JSON after the 4-byte little-endian length prefix. Oversized messages are skipped and malformed JSON is logged as a parse warning.