Skip to content

[POC] feat: add proxy-sovd crate with SOVD REST client and UDS <->SOVD mapper#12

Open
arvindr19 wants to merge 21 commits into
eclipse-opensovd:mainfrom
arvindr19:feat/proxy-sovd
Open

[POC] feat: add proxy-sovd crate with SOVD REST client and UDS <->SOVD mapper#12
arvindr19 wants to merge 21 commits into
eclipse-opensovd:mainfrom
arvindr19:feat/proxy-sovd

Conversation

@arvindr19
Copy link
Copy Markdown
Contributor

@arvindr19 arvindr19 commented Apr 22, 2026

Summary

Adds the proxy-sovd crate: SOVD gateway HTTP client, UDS <-> SOVD JSON translation, and the SovdDiagHandler implementation.

Depends on: PR #11 (proxy-core)

Changes

  • SovdClient: OAuth2 auth, GET /data/{service} reads, PUT /configurations/{service} writes, mock gateway mode for offline testing, MDD-driven mock response generation
  • SovdMapper: Translates RDBI/WDBI UDS requests into SOVD REST calls and encodes JSON responses back to UDS bytes via MDD metadata
  • SovdDiagHandler: Concrete struct with inherent pub async fn handle_read_did / handle_write_did — wires ServiceResolver + SovdMapper for end-to-end UDS processing.
  • DataResponse: SOVD response schema (mirrors sovd_interfaces::ObjectDataItem with Deserialize)

Scope

Read (0x22) and Write (0x2E) services only. Mock gateway enabled by default.

Checklist

  • I have tested my changes locally
  • I have added or updated documentation
  • I have linked related issues or discussions
  • I have added or updated tests

Related

Notes for Reviewers

Since this part of stacked PR, you could see changes from #11 , until it gets merged
Review required for proxy-sovd crate

…nd workspace setup

Signed-off-by: Arvind Rathod <Arvind.RA.Rathod@bti.bmwgroup.com>
@arvindr19 arvindr19 requested a review from a team as a code owner April 22, 2026 08:04
@arvindr19 arvindr19 marked this pull request as draft April 22, 2026 08:04
@lh-sag
Copy link
Copy Markdown

lh-sag commented Apr 23, 2026

@arvindr19 did you took a look at the client in core? https://github.com/eclipse-opensovd/opensovd-core/tree/inc/liebherr/opensovd-client

If

Signed-off-by: Arvind Rathod <Arvind.RA.Rathod@bti.bmwgroup.com>
@arvindr19
Copy link
Copy Markdown
Contributor Author

arvindr19 commented Apr 27, 2026

@arvindr19 did you took a look at the client in core? https://github.com/eclipse-opensovd/opensovd-core/tree/inc/liebherr/opensovd-client

If

Hi @lh-sag , I briefly glanced over the SOVD client repo, it might be useful in the future.

As of now, the goal is to parse UDS requests using the MDD request/response structure and generate a mocked response. The REST API request is just a placeholder for now. I think the proxy might need something like the SOVD client, or we could potentially reuse it here.

Signed-off-by: Arvind Rathod <Arvind.RA.Rathod@bti.bmwgroup.com>
Signed-off-by: Arvind Rathod <Arvind.RA.Rathod@bti.bmwgroup.com>
@arvindr19 arvindr19 force-pushed the feat/proxy-sovd branch 2 times, most recently from 5cf6e04 to 4c373b4 Compare April 28, 2026 06:48
Comment thread proxy-sovd/src/client.rs Outdated
.json()
.await
.map_err(|e| SovdError::Http(e.to_string()))?;
info!("[SOVD] Response: HTTP 200 OK");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about moving this log before data parsing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread proxy-sovd/src/client.rs
Comment on lines +269 to +277
if p.name.contains('/') {
if let Some(prefix) = &mux_case_prefix {
if !p.name.starts_with(prefix.as_str()) {
return false;
}
} else {
return false;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if p.name.contains('/') {
if let Some(prefix) = &mux_case_prefix {
if !p.name.starts_with(prefix.as_str()) {
return false;
}
} else {
return false;
}
}
if p.name.contains('/') {
match &mux_case_prefix {
Some(prefix) if name.starts_with(prefix.as_str()) => {}
_ => return false,
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread proxy-sovd/src/client.rs
continue;
}
// Skip params from non-matching MUX cases.
if p.name.contains('/') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here like above mentioned.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread proxy-sovd/src/client.rs Outdated
if self.config.mock_gateway {
let token = "mock-access-token".to_string();
*self.access_token.write().await = Some(token.clone());
info!("[SOVD] Mock authentication enabled");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are using tracing::Info in service provider and here you directly using , please keep consistency

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, using tracing

Copy link
Copy Markdown

@bharatGoswami8 bharatGoswami8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arvindr19
I’ve reviewed proxy-sovd and added some comments and suggestions.
In my opinion, we should ensure tracing is used consistently across the entire codebase

Signed-off-by: Arvind Rathod <Arvind.RA.Rathod@bti.bmwgroup.com>
.process_write_data_request(did, uds_request, &service_name, parsed_data)
.await
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: don't we need tests in this file?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handle_read_did/write_did require ServiceResolver backed by a CDA EcuManager and an mdd file laoded on disk, there's no way mock for now.

Comment thread proxy-sovd/src/client.rs
///
/// # Errors
/// Returns an error if the read request fails.
pub async fn read_data(&self, component: &str, data_id: &str) -> Result<DataResponse> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if user interchange str value in input to component and data_id, can we use newType here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The call sites are both internal, always passing &self.config.ecu.default_name as the component and service_name.to_lowercase() as the data_id a swap would be immediately obvious at runtime error. Adding newtypes is just boilerplate

Comment thread proxy-sovd/src/client.rs
&self,
component: &str,
data_id: &str,
data: serde_json::Map<String, Value>,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if user interchange str value in input to component and data_id, can we use newType here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above.

Signed-off-by: Arvind Rathod <Arvind.RA.Rathod@bti.bmwgroup.com>
Comment thread proxy-sovd/src/diag_handler.rs Outdated
self.ecu_managers.get(ecu_name).or_else(|| {
warn!(
"[ECU] Default ECU '{}' not found, falling back to first available",
ecu_name
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why to use the first available ecu. in case ecu name is not found, can we return none?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed fallback, return None if ECU name not found

Comment thread proxy-sovd/src/mapper.rs Outdated
/// write requests that need request-parameter context.
pub struct SovdMapper {
/// Proxy configuration (ECU, SOVD endpoint, mock settings).
config: Config,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

config is already stored in sovddiaghandler. why 2nd copy is maintained here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated, Extract only 4 needed fields at construction time (ecu_name, mock_gateway, gateway_url, api_version)

Comment thread proxy-sovd/src/diag_handler.rs Outdated

fn ecu_manager(&self) -> Option<&ServiceResolver> {
let ecu_name = &self.config.ecu.default_name;
self.ecu_managers.get(ecu_name).or_else(|| {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why so you need ecu_managers as map when you want to get serviceResolver for your own ECU
If it is for any future requirement extension better to mention in comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added comment

.ecu_manager()
.ok_or_else(|| ProxyError::Mdd("No MDD database loaded".to_string()))?;

let ResolvedService {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

USD service resolution, processing received data request and forming the response for specific request can be done in more clean way considering extendable solution approach. I understand, this is not current focus of the implementation but consider design approach where different UDS services can be implemented independently without impacting the existing source code, may be command pattern would help here, one command per serivce

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree that we need something like this. but will plan it in future PRs

Comment thread proxy-core/src/lib.rs Outdated

//! Core types and traits shared across all proxy crates.
//!
//! Defines configuration, error types, the [`DiagHandler`] trait for
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is DiagHandler trait?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was part of initial changes, removed later. Updated comment

.or_else(|| response_data.get(short_name))
.or_else(|| response_data.get(&param.name.to_ascii_lowercase()))
.or_else(|| response_data.get(&short_name.to_ascii_lowercase()))
.or_else(|| response_data.get("data"));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why hardcoded to "data"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The data fallback is a legitimate SOVD protocol detail: when a service returns a single scalar value, the SOVD ObjectDataItem stores it under the key data regardless of the MDD parameter name. Without this fallback, any response whose MDD param name doesn't match any key in the SOVD JSON would encode as zeroes. The constant replaces the magic string with a named, documented symbol, it doesn't change behaviour.

/// Returns `None` when no metadata is available. On success returns the
/// encoded bytes and the effective service name (which may differ when
/// enriched MUX sibling metadata is used).
async fn encode_response_from_metadata(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very huge function, please try to split it in to meaning full smaller functions. Also some part of the code looks repetitive line 210 to 245 (client.rs 270)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate_mock_response_data() from client.rs will be removed once sovd-server provides real responses

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please split this function into meaningful smaller funtions

&self,
sid: u8,
did: u16,
_uds_bytes: &[u8],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does _uds_bytes really needed in future, if not remove

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed.

Comment thread proxy-sovd/src/diag_handler.rs Outdated
* https://www.apache.org/licenses/LICENSE-2.0
*/

use std::{collections::HashMap, sync::Arc};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here HashMap is used from collections where as at other places its used from cda_interfaces. No functional change but better to keep consistency with same set of interfaces unless their is any valid reason behind using it differently

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated, using cda_interfaces

Comment thread proxy-core/src/config.rs Outdated
impl Default for SovdConfig {
fn default() -> Self {
Self {
gateway_url: "http://localhost:20002".to_string(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding magic values/strings directly, add it as const or macros

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added const for config fields

Signed-off-by: Arvind Rathod <Arvind.RA.Rathod@bti.bmwgroup.com>
@arvindr19 arvindr19 changed the title feat: add proxy-sovd crate with SOVD REST client and UDS <->SOVD mapper [POC] feat: add proxy-sovd crate with SOVD REST client and UDS <->SOVD mapper May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants