1/*
2 * SPDX-FileCopyrightText: 2025 Copyright (c) Contributors to the Eclipse Foundation
3 *
4 * See the NOTICE file(s) distributed with this work for additional
5 * information regarding copyright ownership.
6 *
7 * This program and the accompanying materials are made available under the
8 * terms of the Apache License Version 2.0 which is available at
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * SPDX-License-Identifier: Apache-2.0
12 */
13
14use aide::{axum::IntoApiResponse, transform::TransformOperation};
15use axum::{
16 Json,
17 body::Bytes,
18 extract::{Query, State},
19 response::{IntoResponse, Response},
20};
21use axum_extra::extract::WithRejection;
22use cda_interfaces::{
23 DiagComm, DynamicPlugin, SchemaProvider, UdsEcu,
24 diagservices::{DiagServiceJsonResponse, DiagServiceResponse, DiagServiceResponseType},
25 file_manager::FileManager,
26};
27use cda_plugin_security::SecurityPlugin;
28use http::{HeaderMap, StatusCode};
29
30use crate::{
31 openapi,
32 sovd::{
33 IntoSovd, WebserverEcuState,
34 components::get_content_type_and_accept,
35 create_response_schema, create_schema,
36 error::{ApiError, ErrorWrapper, api_error_from_diag_response},
37 field_parse_errors_to_json, get_payload_data,
38 },
39};
40
41pub(crate) mod configurations;
42pub(crate) mod data;
43pub(crate) mod faults;
44pub(crate) mod genericservice;
45pub(crate) mod modes;
46pub(crate) mod operations;
47pub(crate) mod x_single_ecu_jobs;
48pub(crate) mod x_sovd2uds_bulk_data;
49pub(crate) mod x_sovd2uds_download;
50
[docs] 51// [[ dimpl~sovd-api-component-sdgsd, GET /components/{ecu} SDG handler ]]
52pub(crate) async fn get<T: UdsEcu + Clone, U: FileManager>(
53 State(WebserverEcuState { ecu_name, uds, .. }): State<WebserverEcuState<T, U>>,
54 WithRejection(Query(query), _): WithRejection<
55 Query<sovd_interfaces::components::ComponentQuery>,
56 ApiError,
57 >,
58) -> impl IntoApiResponse {
59 let include_schema = query.include_schema;
60 let base_path = format!("http://localhost:20002/vehicle/v15/components/{ecu_name}");
61 let variant = match uds.get_variant(&ecu_name).await {
62 Ok(v) => v,
63 Err(e) => {
64 return ErrorWrapper {
65 error: e.into(),
66 include_schema,
67 }
68 .into_response();
69 }
70 };
71
72 let sdgs = if query.include_sdgs {
73 match uds.get_sdgs(&ecu_name, None).await {
74 Ok(v) => Some(
75 v.into_iter()
76 .map(super::super::IntoSovd::into_sovd)
77 .collect(),
78 ),
79 Err(e) => {
80 return ErrorWrapper {
81 error: e.into(),
82 include_schema,
83 }
84 .into_response();
85 }
86 }
87 } else {
88 None
89 };
90
91 let schema = if include_schema {
92 Some(create_schema!(
93 sovd_interfaces::components::ecu::get::Response
94 ))
95 } else {
96 None
97 };
98
99 (
100 StatusCode::OK,
101 Json(sovd_interfaces::components::ecu::get::Response {
102 id: ecu_name.to_lowercase(),
103 name: ecu_name.clone(),
104 variant: variant.into_sovd(),
105 locks: format!("{base_path}/locks"),
106 operations: format!("{base_path}/operations"),
107 configurations: format!("{base_path}/configurations"),
108 data: format!("{base_path}/data"),
109 sdgs,
110 single_ecu_jobs: format!("{base_path}/x-single-ecu-jobs"),
111 faults: format!("{base_path}/faults"),
112 modes: format!("{base_path}/modes"),
113 schema,
114 }),
115 )
116 .into_response()
117}
118
119pub(crate) fn docs_get(op: TransformOperation) -> TransformOperation {
120 op.description("Get ECU details")
121 .response_with::<200, Json<sovd_interfaces::components::ecu::Ecu>, _>(|res| {
122 res.example(sovd_interfaces::components::ecu::Ecu {
123 id: "my_ecu".to_string(),
124 name: "My ECU".to_string(),
125 variant: sovd_interfaces::components::ecu::Variant {
126 name: "Variant Name".to_owned(),
127 is_base_variant: false,
128 state: sovd_interfaces::components::ecu::State::Online,
129 logical_address: "0x42".to_owned(),
130 },
131 locks: "http://localhost:20002/vehicle/v15/components/my_ecu/locks".to_string(),
132 operations: "http://localhost:20002/vehicle/v15/components/my_ecu/operations"
133 .to_string(),
134 data: "http://localhost:20002/vehicle/v15/components/my_ecu/data".to_string(),
135 configurations:
136 "http://localhost:20002/vehicle/v15/components/my_ecu/configurations"
137 .to_string(),
138 sdgs: None,
139 single_ecu_jobs:
140 "http://localhost:20002/vehicle/v15/components/my_ecu/x-single-ecu-jobs"
141 .to_string(),
142 faults: "http://localhost:20002/vehicle/v15/components/my_ecu/faults".to_string(),
143 modes: "http://localhost:20002/vehicle/v15/components/my_ecu/modes".to_string(),
144 schema: None,
145 })
146 .description("Response with ECU information (i.e. detected variant) and service URLs")
147 })
148}
149
150pub(crate) async fn post<T: UdsEcu + Clone, U: FileManager>(
151 State(WebserverEcuState { ecu_name, uds, .. }): State<WebserverEcuState<T, U>>,
152) -> Response {
153 update(&ecu_name, uds).await
154}
155
[docs]156// [[ dimpl~sovd-api-ecu-variant-detection, PUT endpoint for ECU variant detection ]]
157//
158// Handles PUT requests on /components/{ecuName} to trigger variant detection.
159// Delegates to the UDS layer which sends diagnostic requests to the ECU and
160// evaluates the responses against known variant patterns. Returns 201 on
161// success or an error response if detection fails.
162pub(crate) async fn put<T: UdsEcu + Clone, U: FileManager>(
163 State(WebserverEcuState { ecu_name, uds, .. }): State<WebserverEcuState<T, U>>,
164) -> Response {
165 update(&ecu_name, uds).await
166}
167
168pub(crate) fn docs_put(op: TransformOperation) -> TransformOperation {
169 op.description("Trigger ECU variant detection")
170 .response_with::<201, (), _>(|res| res.description("ECU variant detection triggered."))
171}
172
173async fn update<T: UdsEcu + Clone>(ecu_name: &str, uds: T) -> Response {
174 match uds.detect_variant(ecu_name).await {
175 Ok(()) => (StatusCode::CREATED, ()).into_response(),
176 Err(e) => ErrorWrapper {
177 error: e.into(),
178 include_schema: false,
179 }
180 .into_response(),
181 }
182}
183
184impl IntoSovd for cda_interfaces::datatypes::ComplexComParamValue {
185 type SovdType = sovd_interfaces::components::ecu::operations::comparams::ComplexComParamValue;
186
187 fn into_sovd(self) -> Self::SovdType {
188 self.into_iter()
189 .map(|(key, value)| (key, value.into_sovd()))
190 .collect()
191 }
192}
193
194impl IntoSovd for cda_interfaces::datatypes::ComParamValue {
195 type SovdType = sovd_interfaces::components::ecu::operations::comparams::ComParamValue;
196
197 fn into_sovd(self) -> Self::SovdType {
198 match self {
199 Self::Simple(simple) => Self::SovdType::Simple(simple.into_sovd()),
200 Self::Complex(complex) => Self::SovdType::Complex(complex.into_sovd()),
201 }
202 }
203}
204
205impl IntoSovd for cda_interfaces::datatypes::ComParamSimpleValue {
206 type SovdType = sovd_interfaces::components::ecu::operations::comparams::ComParamSimpleValue;
207
208 fn into_sovd(self) -> Self::SovdType {
209 Self::SovdType {
210 value: self.value.clone(),
211 unit: self.unit.map(|u| {
212 sovd_interfaces::components::ecu::operations::comparams::Unit {
213 factor_to_si_unit: u.factor_to_si_unit,
214 offset_to_si_unit: u.offset_to_si_unit,
215 }
216 }),
217 }
218 }
219}
220
221openapi::aide_helper::gen_path_param!(DiagServicePathParam service String);
222#[allow(
223 clippy::too_many_lines,
224 reason = "Splitting this function is not worth it here"
225)]
226async fn data_request<T: UdsEcu + SchemaProvider + Clone>(
227 service: DiagComm,
228 ecu_name: &str,
229 gateway: &T,
230 headers: HeaderMap,
231 body: Option<Bytes>,
232 security_plugin: Box<dyn SecurityPlugin>,
233 include_schema: bool,
234) -> Response {
235 let (content_type, accept) = match get_content_type_and_accept(&headers) {
236 Ok(v) => v,
237 Err(e) => {
238 return ErrorWrapper {
239 error: e,
240 include_schema,
241 }
242 .into_response();
243 }
244 };
245
246 let data = if let Some(body) = body {
247 match get_payload_data::<sovd_interfaces::components::ecu::data::DataRequestPayload>(
248 content_type.as_ref(),
249 &headers,
250 &body,
251 ) {
252 Ok(value) => value,
253 Err(e) => {
254 return ErrorWrapper {
255 error: e,
256 include_schema,
257 }
258 .into_response();
259 }
260 }
261 } else {
262 None
263 };
264
265 let map_to_json = match (accept.type_(), accept.subtype()) {
266 (mime::APPLICATION, mime::JSON) => true,
267 (mime::APPLICATION, mime::OCTET_STREAM) => false,
268 unsupported => {
269 return ErrorWrapper {
270 error: ApiError::BadRequest(format!("Unsupported Accept: {unsupported:?}")),
271 include_schema,
272 }
273 .into_response();
274 }
275 };
276
277 if !map_to_json && include_schema {
278 return ErrorWrapper {
279 error: ApiError::BadRequest(
280 "Cannot use include-schema with non-JSON response".to_string(),
281 ),
282 include_schema,
283 }
284 .into_response();
285 }
286
287 let schema = if include_schema {
288 match gateway
289 .schema_for_responses(ecu_name, &service)
290 .await
291 .map(cda_interfaces::SchemaDescription::into_schema)
292 {
293 Ok(data_schema) => Some(create_response_schema!(
294 sovd_interfaces::ObjectDataItem<VendorErrorCode>,
295 "data",
296 data_schema
297 )),
298 Err(e) => {
299 return ErrorWrapper {
300 error: e.into(),
301 include_schema,
302 }
303 .into_response();
304 }
305 }
306 } else {
307 None
308 };
309
310 let response = match gateway
311 .send(
312 ecu_name,
313 service.clone(),
314 &(security_plugin as DynamicPlugin),
315 data,
316 map_to_json,
317 )
318 .await
319 .map_err(Into::into)
320 {
321 Err(e) => {
322 return ErrorWrapper {
323 error: e,
324 include_schema,
325 }
326 .into_response();
327 }
328 Ok(v) => v,
329 };
330
331 if let DiagServiceResponseType::Negative = response.response_type() {
332 return api_error_from_diag_response(&response, include_schema).into_response();
333 }
334
335 if response.is_empty() {
336 return StatusCode::NO_CONTENT.into_response();
337 }
338
339 if map_to_json {
340 let (mapped_data, errors) = match response.into_json() {
341 Ok(DiagServiceJsonResponse {
342 data: serde_json::Value::Object(mapped_data),
343 errors,
344 }) => (mapped_data, errors),
345 Ok(DiagServiceJsonResponse {
346 data: serde_json::Value::Null,
347 errors,
348 }) => {
349 if errors.is_empty() {
350 return StatusCode::NO_CONTENT.into_response();
351 }
352 (serde_json::Map::new(), errors)
353 }
354 Ok(v) => {
355 return ErrorWrapper {
356 error: ApiError::InternalServerError(Some(format!(
357 "Expected JSON object but got: {}",
358 v.data
359 ))),
360 include_schema,
361 }
362 .into_response();
363 }
364 Err(e) => {
365 return ErrorWrapper {
366 error: ApiError::InternalServerError(Some(format!("{e:?}"))),
367 include_schema,
368 }
369 .into_response();
370 }
371 };
372 (
373 StatusCode::OK,
374 Json(sovd_interfaces::ObjectDataItem {
375 id: service.name.to_lowercase(),
376 data: mapped_data,
377 errors: field_parse_errors_to_json(errors, "data"),
378 schema,
379 }),
380 )
381 .into_response()
382 } else {
383 let data = response.get_raw().to_vec();
384 (StatusCode::OK, Bytes::from_owner(data)).into_response()
385 }
386}