Skip to main content

the_bus_telemetry/
api.rs

1//! This module handles the raw interaction with The Bus Telemetry API.
2
3use serde::Deserialize;
4use std::string::ToString;
5use std::time::Duration;
6
7/// Configuration for API requests.
8pub struct RequestConfig {
9    /// Host address of the telemetry server (default: "127.0.0.1").
10    pub host: String,
11    /// Port of the telemetry server (default: "37337").
12    pub port: String,
13    /// Name of the vehicle to query (default: "Current").
14    pub vehicle_name: String,
15    /// Model of the vehicle to query (default: "Current").
16    pub vehicle_model: String,
17    /// Request timeout (default: 300ms).
18    pub timeout: Duration,
19    /// Enable debug logging of URLs and data.
20    pub debugging: bool,
21}
22
23impl RequestConfig {
24    /// Creates a new `RequestConfig` with default values.
25    pub fn new() -> Self {
26        Self {
27            host: "127.0.0.1".to_string(),
28            port: "37337".to_string(),
29            vehicle_name: "Current".to_string(),
30            vehicle_model: "Current".to_string(),
31            timeout: Duration::from_millis(300),
32            debugging: false,
33        }
34    }
35    /// Sets the host address.
36    pub fn host(mut self, host: String) -> Self {
37        self.host = host;
38        self
39    }
40
41    /// Sets the port.
42    pub fn port(mut self, port: String) -> Self {
43        self.port = port;
44        self
45    }
46    /// Sets the vehicle name.
47    pub fn vehicle_name(mut self, vehicle_name: String) -> Self {
48        self.vehicle_name = vehicle_name;
49        self
50    }
51    /// Sets the vehicle model.
52    pub fn vehicle_model(mut self, vehicle_model: String) -> Self {
53        self.vehicle_model = vehicle_model;
54        self
55    }
56
57    /// Sets the request timeout.
58    pub fn timeout(mut self, timeout: Duration) -> Self {
59        self.timeout = timeout;
60        self
61    }
62    /// Sets the debugging flag.
63    pub fn debugging(mut self, debugging: bool) -> Self {
64        self.debugging = debugging;
65        self
66    }
67}
68
69/// World telemetry data.
70#[derive(Deserialize, Debug, PartialEq)]
71pub struct ApiWorldType {
72    /// Name of the current level.
73    #[serde(rename = "LevelName")]
74    pub level_name: String,
75    /// Current date and time in the game.
76    #[serde(rename = "DateTime")]
77    pub date_time: String,
78    /// Time acceleration factor.
79    #[serde(rename = "TimeFactor")]
80    pub time_factor: f32,
81    /// Latitude of the world origin.
82    #[serde(rename = "BaseLatitude")]
83    pub base_latitude: f64,
84    /// Longitude of the world origin.
85    #[serde(rename = "BaseLongitude")]
86    pub base_longitude: f64,
87}
88
89/// Vehicle telemetry data.
90#[derive(Deserialize, Debug, PartialEq, Default)]
91pub struct ApiVehicleType {
92    /// Internal actor name.
93    #[serde(rename = "ActorName")]
94    pub actor_name: String,
95    /// Vehicle model name.
96    #[serde(rename = "VehicleModel")]
97    pub vehicle_model: String,
98    /// Whether the ignition is enabled (string "true"/"false").
99    #[serde(rename = "IgnitionEnabled")]
100    pub ignition_enabled: String,
101    /// Whether the engine is started (string "true"/"false").
102    #[serde(rename = "EngineStarted")]
103    pub engine_started: String,
104    /// Whether warning lights are active (string "true"/"false").
105    #[serde(rename = "WarningLights")]
106    pub warning_lights: String,
107    /// Whether any passenger door is open (string "true"/"false").
108    #[serde(rename = "PassengerDoorsOpen")]
109    pub passenger_doors_open: String,
110    /// Whether the fixing (parking) brake is engaged (string "true"/"false").
111    #[serde(rename = "FixingBrake")]
112    pub fixing_brake: String,
113    /// Current speed in km/h.
114    #[serde(rename = "Speed")]
115    pub speed: f32,
116    /// Allowed speed limit.
117    #[serde(rename = "AllowedSpeed")]
118    pub allowed_speed: f32,
119    /// Fuel level on display (0.0 to 1.0).
120    #[serde(rename = "DisplayFuel")]
121    pub display_fuel: f32,
122    /// Indicator state (-1: left, 0: off, 1: right).
123    #[serde(rename = "IndicatorState")]
124    pub indicator_state: i8,
125    /// Status of all external and internal lamps.
126    #[serde(rename = "AllLamps")]
127    pub all_lamps: ApiLamps,
128    /// List of buttons and their states.
129    #[serde(rename = "Buttons", default)]
130    pub buttons: Vec<ApiButton>,
131}
132
133/// Represents various lamp intensities or states.
134#[derive(Deserialize, Debug, PartialEq, Default)]
135pub struct ApiLamps {
136    #[serde(
137        rename = "LightHeadlight",
138        alias = "LightHeadlight1",
139        alias = "Light Headlight"
140    )]
141    pub light_headlight: f32,
142    #[serde(
143        rename = "Light Parking",
144        alias = "LightParking1",
145        alias = "LightParking"
146    )]
147    pub light_parking: f32,
148    #[serde(
149        rename = "Light MAIN",
150        alias = "Light Main",
151        alias = "LightMain",
152        default
153    )] // we need default man_lionscity does not have this field
154    pub light_main: f32,
155    /// High beam / traveller light intensity (0.0 or 1.0).
156    #[serde(
157        rename = "LightTraveling",
158        alias = "LightTraveling1",
159        alias = "Light Travelling"
160    )]
161    pub traveller_light: f32,
162    /// Front door light state.
163    #[serde(rename = "Door Button 1", alias = "ButtonLight Door 1")]
164    pub front_door_light: f32,
165    /// Second door light state.
166    #[serde(
167        rename = "Door Button 2",
168        alias = "ButtonLight Door 2",
169        alias = "LightDoorMiddle"
170    )]
171    pub second_door_light: f32,
172    /// Third door light state.
173    #[serde(rename = "Door Button 3", alias = "ButtonLight Door 3", default)]
174    pub third_door_light: f32,
175    /// Fourth door light state.
176    #[serde(rename = "Door Button 4", alias = "ButtonLight Door 4", default)]
177    pub fourth_door_light: f32,
178    /// Stop request LED intensity.
179    #[serde(
180        rename = "LED StopRequest",
181        alias = "DB Stop Request",
182        alias = "TachoStopRequest",
183        default
184    )] // we need default vdl_citea does not have this field
185    pub led_stop_request: f32,
186    /// Bus stop brake light intensity.
187    #[serde(rename = "ButtonLight BusStopBrake", alias = "LED Stop Brake", default)]
188    // we need default man_lionscity does not have this field
189    pub light_stopbrake: f32,
190    #[serde(
191        rename = "ButtonLight DoorClearance",
192        alias = "DoorClearanceButton",
193        default
194    )] // we need default man_lionscity does not have this field
195    pub door_clearance_light: f32,
196}
197
198/// Represents a button in the vehicle and its current state.
199#[derive(Deserialize, Debug, PartialEq, Default, Clone)]
200pub struct ApiButton {
201    /// Button name.
202    #[serde(rename = "Name")]
203    pub name: String,
204    /// Tooltip text for the button.
205    #[serde(rename = "Tooltip", default)]
206    pub tooltip: String,
207    /// Current state of the button (e.g. "on", "off", "Drive", "Neutral").
208    #[serde(rename = "State", default)]
209    pub state: String,
210    /// Numeric value as string, if applicable.
211    #[serde(rename = "Value", default)]
212    pub value: String,
213    /// Possible actions for this button.
214    #[serde(rename = "Actions", default)]
215    pub actions: Vec<String>,
216    /// Possible states for this button.
217    #[serde(rename = "States", default)]
218    pub states: Vec<String>,
219}
220
221impl ApiVehicleType {
222
223    pub fn new() -> Self {
224        Self::default()
225    }
226
227    /// Returns the button with the given name, if found.
228    pub fn get_button(&self, name: &str) -> Option<ApiButton> {
229        self.buttons.iter().find(|b| b.name == name).cloned()
230    }
231    /// Returns the state of the button with the given name, or an empty string if not found.
232    pub fn get_button_state(&self, name: &str) -> String {
233        self.buttons
234            .iter()
235            .find(|b| b.name == name)
236            .map(|b| b.state.clone())
237            .unwrap_or_else(|| "".to_string())
238    }
239
240    /// Returns the state of the first button whose name contains the given part.
241    pub fn get_button_state_contains(&self, part: &str) -> String {
242        self.buttons
243            .iter()
244            .find(|b| b.name.contains(part))
245            .map(|b| b.state.clone())
246            .unwrap_or_else(|| "".to_string())
247    }
248
249    /// Returns all buttons with the exact given name.
250    pub fn filtered_buttons(&self, name: &str) -> Vec<ApiButton> {
251        self.buttons
252            .iter()
253            .filter(|b| b.name == name)
254            .cloned()
255            .collect()
256    }
257
258    /// Keeps only the buttons with the exact given name in the vehicle.
259    pub fn retain_buttons_by_name(&mut self, name: &str) {
260        self.buttons.retain(|b| b.name == name);
261    }
262
263    /// Returns a vector of tuples containing (name, state) for all buttons.
264    pub fn buttons_name_state(&self) -> Vec<(String, String)> {
265        self.buttons
266            .iter()
267            .map(|b| (b.name.clone(), b.state.clone()))
268            .collect()
269    }
270}
271
272/// Sends a command to the vehicle via the telemetry API.
273pub async fn send_telemetry_bus_cmd(
274    config: &RequestConfig,
275    cmd: &str,
276) -> Result<(), Box<dyn std::error::Error>> {
277    let url = format!(
278        "http://{}:{}/vehicles/{}/{}",
279        config.host, config.port, config.vehicle_name, cmd
280    );
281    if config.debugging {
282        println!("send_telemetry_bus_cmd URL: {}", url);
283    }
284
285    let _ = reqwest::Client::new()
286        .get(url)
287        .timeout(config.timeout)
288        .send()
289        .await?;
290
291    Ok(())
292}
293
294/// Fetches raw JSON telemetry data from a specific API path.
295pub async fn get_telemetry_data(
296    config: &RequestConfig,
297    path: &str,
298) -> reqwest::Result<serde_json::Value> {
299    let url = format!("http://{}:{}/{}", config.host, config.port, path);
300
301    if config.debugging {
302        println!("get_telemetry_data URL: {}", url);
303    }
304
305    let value = reqwest::Client::new()
306        .get(url)
307        .timeout(config.timeout)
308        .send()
309        .await?
310        .json::<serde_json::Value>()
311        .await?;
312
313    Ok(value)
314}
315
316/// Returns the current vehicle name from the "player" telemetry endpoint.
317/// Returns an empty string if the player is not in a vehicle or if the request fails.
318pub async fn get_current_vehicle_name(config: &RequestConfig) -> String {
319    let result = get_telemetry_data(&config, "player").await;
320
321    if result.is_err() {
322        return "".to_string();
323    }
324    let data = result.unwrap();
325
326    if config.debugging {
327        println!("get_current_vehicle_name data: {:?}", data);
328    }
329
330    let mode: Option<String> = data
331        .get("Mode")
332        .and_then(|v| v.as_str())
333        .map(|s| s.to_string());
334
335    let mut mo = "".to_string();
336    if let Some(ref m) = mode {
337        mo = m.clone();
338    }
339
340    if mo != "Vehicle" {
341        return "".to_string();
342    }
343
344    let current_vehicle: Option<String> = data
345        .get("CurrentVehicle")
346        .and_then(|v| v.as_str())
347        .map(|s| s.to_string());
348
349    let mut bus = "".to_string();
350
351    if let Some(ref cv) = current_vehicle {
352        bus = cv.clone();
353    }
354    bus
355}
356
357/// Fetches telemetry data for the vehicle specified in `config`.
358pub async fn get_vehicle(
359    config: &RequestConfig,
360) -> Result<ApiVehicleType, Box<dyn std::error::Error>> {
361    let path = format!("vehicles/{}", config.vehicle_name);
362
363    if config.debugging {
364        println!("get_vehicle path: {}", path);
365    }
366
367    let body = get_telemetry_data(&config, &path).await?;
368
369    let api_vehicle: ApiVehicleType = serde_json::from_value(body).map_err(|e| {
370        eprintln!("Failed to parse API response as Vehicle JSON: {}", e);
371        Box::new(e) as Box<dyn std::error::Error>
372    })?;
373
374    if config.debugging {
375        println!("{:?}", &api_vehicle);
376    }
377
378    Ok(api_vehicle)
379}
380
381/// Fetches world telemetry data (time, weather, etc).
382pub async fn get_world(config: &RequestConfig) -> Result<ApiWorldType, Box<dyn std::error::Error>> {
383    let path = "world";
384
385    if config.debugging {
386        println!("get_world path: {}", path);
387    }
388
389    let body = get_telemetry_data(&config, &path).await?;
390
391    let api_world: ApiWorldType = serde_json::from_value(body).map_err(|e| {
392        eprintln!("Failed to parse API response as World JSON: {}", e);
393        Box::new(e) as Box<dyn std::error::Error>
394    })?;
395
396    if config.debugging {
397        println!("{:?}", &api_world);
398    }
399    Ok(api_world)
400}
401
402/// Extracts the state of a button by name from a raw JSON value containing a "Buttons" array.
403pub fn get_button_by_name(data: &serde_json::Value, name: &str) -> String {
404    let ret = data
405        .get("Buttons")
406        .and_then(|v| v.as_array())
407        .and_then(|arr| {
408            arr.iter().find(|entry| {
409                entry
410                    .get("Name")
411                    .and_then(|n| n.as_str())
412                    .map_or(false, |s| s == name)
413            })
414        })
415        .and_then(|entry| entry.get("State"))
416        .and_then(|v| v.as_str())
417        .map(|s| s.to_string());
418
419    ret.unwrap_or_else(|| "".to_string())
420}