Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b0fb09f
test: add trait-bound assertion helpers for derive consistency
zheylmun Jul 1, 2026
cc5cae9
feat: full derive block on TransferData request/response
zheylmun Jul 1, 2026
c119607
feat: non_exhaustive on ReadDataByIdentifierRequest; document serde+u…
zheylmun Jul 1, 2026
f8a863f
feat: serde + Eq on RequestDownload request/response
zheylmun Jul 1, 2026
e2317c8
feat: serde/utoipa on RoutineControl request/response
zheylmun Jul 1, 2026
160b4a1
feat: serde/utoipa on SecurityAccess request/response
zheylmun Jul 1, 2026
2362d7e
feat: serde/utoipa on WriteDataByIdentifier request/response
zheylmun Jul 1, 2026
b7e9c0b
feat: derive Eq on ControlDTCSettingsResponse
zheylmun Jul 1, 2026
1d09e99
feat: derive Eq across RequestFileTransfer types
zheylmun Jul 1, 2026
5fa88a5
feat: Eq + const new on ReadDTCInfo types; Eq on DTCFormatIdentifier
zheylmun Jul 1, 2026
9bad248
feat: allowed_nack_codes for ControlDTCSettings, ReadDTCInfo, Routine…
zheylmun Jul 1, 2026
20ea447
feat: allowed_nack_codes for RequestFileTransfer, RequestTransferExit…
zheylmun Jul 1, 2026
43a5429
feat: named ClearDiagnosticInfoResponse type for Response symmetry
zheylmun Jul 1, 2026
1bf3063
feat: named ReadDataByIdentifierResponse type for Response symmetry
zheylmun Jul 1, 2026
26605fa
refactor: standardize newtype accessor on const value()
zheylmun Jul 1, 2026
94f4425
fix: address final-review findings (strict ClearDiagnosticInfo decode…
zheylmun Jul 1, 2026
dd43065
.gitignore omniscient files
zheylmun Jul 1, 2026
6d196eb
fix: rename test bindings to satisfy clippy::similar_names
zheylmun Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,8 @@ fuzz/corpus/
fuzz/artifacts/

.omniscient/

omniscient.toml
.DS_Store
.superpowers/

omniscient.toml
2 changes: 1 addition & 1 deletion src/dtc/ext_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ impl DTCExtDataRecordNumber {
/// Return the raw `u8` value of this record number.
#[must_use]
#[allow(clippy::match_same_arms)]
pub fn value(&self) -> u8 {
pub const fn value(&self) -> u8 {
match self {
Self::ISOSAEReserved(value) => *value,
Self::VehicleManufacturer(value) => *value,
Expand Down
2 changes: 1 addition & 1 deletion src/dtc/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ impl DTCSnapshotRecordNumber {
/// Return the raw `u8` value of this snapshot record number.
#[must_use]
#[allow(clippy::match_same_arms)]
pub fn value(&self) -> u8 {
pub const fn value(&self) -> u8 {
match self {
DTCSnapshotRecordNumber::Reserved(value) => *value,
DTCSnapshotRecordNumber::Number(value) => *value,
Expand Down
4 changes: 2 additions & 2 deletions src/dtc/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ impl<'a> Decode<'a> for DTCStatusMask {
#[allow(non_camel_case_types)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
#[repr(u8)]
pub enum DTCFormatIdentifier {
Expand Down Expand Up @@ -299,7 +299,7 @@ pub enum FunctionalGroupIdentifier {
impl FunctionalGroupIdentifier {
/// Return the raw `u8` value of this functional group identifier.
#[must_use]
pub fn value(&self) -> u8 {
pub const fn value(&self) -> u8 {
match self {
FunctionalGroupIdentifier::EmissionsSystemGroup => 0x33,
FunctionalGroupIdentifier::SafetySystemGroup => 0xD0,
Expand Down
27 changes: 14 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,20 @@ pub use service::UdsServiceType;

mod services;
pub use services::{
ClearDiagnosticInfoRequest, CommunicationControlRequest, CommunicationControlResponse,
CommunicationControlType, CommunicationType, ControlDTCSettingsRequest,
ControlDTCSettingsResponse, DiagnosticSessionControlRequest, DiagnosticSessionControlResponse,
DiagnosticSessionType, DirSizePayload, DtcAndStatusIter, DtcFaultDetectionIter, DtcSettings,
DtcSeverityAndStatusIter, EcuResetRequest, EcuResetResponse, FileOperationMode,
FileSizePayload, NamePayload, NegativeResponse, PositionPayload, ReadDTCInfoRequest,
ReadDTCInfoResponse, ReadDTCInfoSubFunction, ReadDataByIdentifierRequest,
RequestDownloadRequest, RequestDownloadResponse, RequestFileTransferRequest,
RequestFileTransferResponse, RequestTransferExitRequest, RequestTransferExitResponse,
ResetType, RoutineControlRequest, RoutineControlResponse, RoutineControlSubFunction,
SecurityAccessLevel, SecurityAccessRequest, SecurityAccessResponse, SecurityAccessType,
SentDataPayload, SizePayload, TesterPresentRequest, TesterPresentResponse, TransferDataRequest,
TransferDataResponse, WriteDataByIdentifierRequest, WriteDataByIdentifierResponse,
ClearDiagnosticInfoRequest, ClearDiagnosticInfoResponse, CommunicationControlRequest,
CommunicationControlResponse, CommunicationControlType, CommunicationType,
ControlDTCSettingsRequest, ControlDTCSettingsResponse, DiagnosticSessionControlRequest,
DiagnosticSessionControlResponse, DiagnosticSessionType, DirSizePayload, DtcAndStatusIter,
DtcFaultDetectionIter, DtcSettings, DtcSeverityAndStatusIter, EcuResetRequest,
EcuResetResponse, FileOperationMode, FileSizePayload, NamePayload, NegativeResponse,
PositionPayload, ReadDTCInfoRequest, ReadDTCInfoResponse, ReadDTCInfoSubFunction,
ReadDataByIdentifierRequest, ReadDataByIdentifierResponse, RequestDownloadRequest,
RequestDownloadResponse, RequestFileTransferRequest, RequestFileTransferResponse,
RequestTransferExitRequest, RequestTransferExitResponse, ResetType, RoutineControlRequest,
RoutineControlResponse, RoutineControlSubFunction, SecurityAccessLevel, SecurityAccessRequest,
SecurityAccessResponse, SecurityAccessType, SentDataPayload, SizePayload, TesterPresentRequest,
TesterPresentResponse, TransferDataRequest, TransferDataResponse, WriteDataByIdentifierRequest,
WriteDataByIdentifierResponse,
};

#[cfg(test)]
Expand Down
47 changes: 31 additions & 16 deletions src/response.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::{
CommunicationControlResponse, ControlDTCSettingsResponse, Decode,
ClearDiagnosticInfoResponse, CommunicationControlResponse, ControlDTCSettingsResponse, Decode,
DiagnosticSessionControlResponse, EcuResetResponse, Encode, Error, NegativeResponse,
ReadDTCInfoResponse, RequestDownloadResponse, RequestFileTransferResponse,
RequestTransferExitResponse, RoutineControlResponse, SecurityAccessResponse,
TesterPresentResponse, TransferDataResponse, UdsServiceType, WriteDataByIdentifierResponse,
ReadDTCInfoResponse, ReadDataByIdentifierResponse, RequestDownloadResponse,
RequestFileTransferResponse, RequestTransferExitResponse, RoutineControlResponse,
SecurityAccessResponse, TesterPresentResponse, TransferDataResponse, UdsServiceType,
WriteDataByIdentifierResponse,
};

/// Parsed zero-copy UDS response. Borrows from the wire buffer.
Expand All @@ -14,7 +15,7 @@ use crate::{
#[non_exhaustive]
pub enum Response<'a> {
/// Positive response to `ClearDiagnosticInfo`.
ClearDiagnosticInfo,
ClearDiagnosticInfo(ClearDiagnosticInfoResponse),
/// Positive response to `CommunicationControl`.
CommunicationControl(CommunicationControlResponse),
/// Positive response to `ControlDTCSettings`.
Expand All @@ -34,7 +35,7 @@ pub enum Response<'a> {
/// cannot split it into `(DID, value)` pairs. Parse it caller-side once you know each
/// DID's record length — read the 2-byte big-endian DID, take the application-defined
/// number of data bytes, then repeat on the remainder.
ReadDataByIdentifier(&'a [u8]),
ReadDataByIdentifier(ReadDataByIdentifierResponse<'a>),
/// Positive response to `ReadDTCInformation` with lazy iterators.
ReadDTCInfo(ReadDTCInfoResponse<'a>),
/// Positive response to `RequestDownload`.
Expand Down Expand Up @@ -74,7 +75,9 @@ impl<'a> Decode<'a> for Response<'a> {
let payload = &buf[1..];

let response = match service {
UdsServiceType::ClearDiagnosticInfo => Self::ClearDiagnosticInfo,
UdsServiceType::ClearDiagnosticInfo => Self::ClearDiagnosticInfo(
<ClearDiagnosticInfoResponse as Decode>::decode_exact(payload)?,
),
UdsServiceType::CommunicationControl => Self::CommunicationControl(
<CommunicationControlResponse as Decode>::decode_exact(payload)?,
),
Expand All @@ -90,7 +93,9 @@ impl<'a> Decode<'a> for Response<'a> {
UdsServiceType::NegativeResponse => {
Self::NegativeResponse(<NegativeResponse as Decode>::decode_exact(payload)?)
}
UdsServiceType::ReadDataByIdentifier => Self::ReadDataByIdentifier(payload),
UdsServiceType::ReadDataByIdentifier => {
Self::ReadDataByIdentifier(ReadDataByIdentifierResponse::new(payload))
}
UdsServiceType::ReadDTCInfo => {
Self::ReadDTCInfo(<ReadDTCInfoResponse as Decode>::decode_exact(payload)?)
}
Expand Down Expand Up @@ -143,7 +148,7 @@ impl Response<'_> {
/// Returns the response service-ID byte that frames this response on the wire.
fn response_sid(&self) -> u8 {
match self {
Self::ClearDiagnosticInfo => UdsServiceType::ClearDiagnosticInfo.response_to_byte(),
Self::ClearDiagnosticInfo(_) => UdsServiceType::ClearDiagnosticInfo.response_to_byte(),
Self::CommunicationControl(_) => {
UdsServiceType::CommunicationControl.response_to_byte()
}
Expand Down Expand Up @@ -175,15 +180,15 @@ impl Response<'_> {
impl Encode for Response<'_> {
fn encoded_size(&self) -> usize {
let payload = match self {
Self::ClearDiagnosticInfo => 0,
Self::ClearDiagnosticInfo(resp) => resp.encoded_size(),
Self::RequestTransferExit(resp) => resp.encoded_size(),
Self::Other { data, .. } => data.len(),
Self::CommunicationControl(resp) => resp.encoded_size(),
Self::ControlDTCSettings(resp) => resp.encoded_size(),
Self::DiagnosticSessionControl(resp) => resp.encoded_size(),
Self::EcuReset(resp) => resp.encoded_size(),
Self::NegativeResponse(resp) => resp.encoded_size(),
Self::ReadDataByIdentifier(bytes) => bytes.len(),
Self::ReadDataByIdentifier(resp) => resp.encoded_size(),
Self::WriteDataByIdentifier(resp) => resp.encoded_size(),
Self::ReadDTCInfo(resp) => resp.encoded_size(),
Self::RequestDownload(resp) => resp.encoded_size(),
Expand All @@ -201,17 +206,14 @@ impl Encode for Response<'_> {
.write_all(&[self.response_sid()])
.map_err(Error::io)?;
let payload = match self {
Self::ClearDiagnosticInfo => 0,
Self::ClearDiagnosticInfo(resp) => resp.encode(writer)?,
Self::RequestTransferExit(resp) => resp.encode(writer)?,
Self::CommunicationControl(resp) => resp.encode(writer)?,
Self::ControlDTCSettings(resp) => resp.encode(writer)?,
Self::DiagnosticSessionControl(resp) => resp.encode(writer)?,
Self::EcuReset(resp) => resp.encode(writer)?,
Self::NegativeResponse(resp) => resp.encode(writer)?,
Self::ReadDataByIdentifier(bytes) => {
writer.write_all(bytes).map_err(Error::io)?;
bytes.len()
}
Self::ReadDataByIdentifier(resp) => resp.encode(writer)?,
Self::WriteDataByIdentifier(resp) => resp.encode(writer)?,
Self::ReadDTCInfo(resp) => resp.encode(writer)?,
Self::RequestDownload(resp) => resp.encode(writer)?,
Expand Down Expand Up @@ -284,4 +286,17 @@ mod tests {
let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap();
assert_eq!(&buf[..written], &frame); // previously became 0x7F (NegativeResponse)
}

#[test]
fn clear_diagnostic_info_response_rejects_trailing_bytes() {
// Bare SID round-trips; a conformant ClearDiagnosticInfo positive response is [0x54].
let (resp, remaining) = Response::decode(&[0x54]).unwrap();
assert!(remaining.is_empty());
assert!(matches!(resp, Response::ClearDiagnosticInfo(_)));
// Trailing bytes after the SID are now rejected (matches every other response arm).
assert!(matches!(
Response::decode(&[0x54, 0xAA]),
Err(crate::Error::IncorrectMessageLengthOrInvalidFormat)
));
}
}
50 changes: 50 additions & 0 deletions src/services/clear_dtc_information.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
//! `ClearDiagnosticInformation` (0x14) service implementation
use crate::{CLEAR_ALL_DTCS, DTCRecord, Decode, Encode, NegativeResponseCode};

/// Positive response to `ClearDiagnosticInformation`. Carries no payload.
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct ClearDiagnosticInfoResponse;

impl ClearDiagnosticInfoResponse {
/// Create a `ClearDiagnosticInfoResponse`.
#[must_use]
pub const fn new() -> Self {
Self
}
}

impl Encode for ClearDiagnosticInfoResponse {
fn encoded_size(&self) -> usize {
0
}
fn encode(&self, _writer: &mut impl embedded_io::Write) -> Result<usize, crate::Error> {
Ok(0)
}
}

impl<'a> Decode<'a> for ClearDiagnosticInfoResponse {
/// Consumes zero bytes and returns the full buffer as the remainder.
/// `decode_exact` at the call site (in `Response::decode`) enforces that no trailing bytes follow the SID.
fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), crate::Error> {
Ok((Self, buf))
}
}

/// Negative response codes
const CLEAR_DIAG_INFO_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 4] = [
NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat,
Expand Down Expand Up @@ -108,3 +140,21 @@ mod request {
assert_eq!(all, compare);
}
}

#[cfg(test)]
mod response {
use super::*;
use crate::{Decode, Encode};

#[test]
fn clear_dtc_response_roundtrips_empty() {
let resp = ClearDiagnosticInfoResponse::new();
let mut buf = [0u8; 4];
let n = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap();
assert_eq!(n, 0);
let (decoded, remaining) =
<ClearDiagnosticInfoResponse as Decode>::decode(&buf[..0]).unwrap();
assert_eq!(decoded, resp);
assert!(remaining.is_empty());
}
}
33 changes: 30 additions & 3 deletions src/services/control_dtc_settings.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! `ControlDTCSetting` (0x85) service implementation
use crate::shared::SuppressablePositiveResponse;
use crate::{Decode, Encode, Error};
use crate::{Decode, Encode, Error, NegativeResponseCode};

#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
Expand Down Expand Up @@ -49,6 +49,13 @@ pub struct ControlDTCSettingsRequest {
pub setting: DtcSettings,
}

const CONTROL_DTC_SETTINGS_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 4] = [
NegativeResponseCode::SubFunctionNotSupported,
NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat,
NegativeResponseCode::ConditionsNotCorrect,
NegativeResponseCode::RequestOutOfRange,
];

impl ControlDTCSettingsRequest {
/// Create a new `ControlDTCSettingsRequest`.
#[must_use]
Expand All @@ -58,6 +65,12 @@ impl ControlDTCSettingsRequest {
setting,
}
}

/// Get the allowed [`NegativeResponseCode`] variants for this request.
#[must_use]
pub fn allowed_nack_codes() -> &'static [NegativeResponseCode] {
&CONTROL_DTC_SETTINGS_NEGATIVE_RESPONSE_CODES
}
}

impl Encode for ControlDTCSettingsRequest {
Expand Down Expand Up @@ -96,7 +109,7 @@ impl<'a> Decode<'a> for ControlDTCSettingsRequest {
/// The ECU will respond with a `ControlDTCSettingsResponse` if the request was successful.
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct ControlDTCSettingsResponse {
/// The DTC logging setting that was set in the request
Expand Down Expand Up @@ -137,7 +150,7 @@ impl<'a> Decode<'a> for ControlDTCSettingsResponse {
#[cfg(test)]
mod request {
use super::*;
use crate::{Decode, Encode, test_util::assert_encode_size_agrees};
use crate::{Decode, Encode, NegativeResponseCode, test_util::assert_encode_size_agrees};
#[cfg(feature = "alloc")]
use alloc::{vec, vec::Vec};

Expand All @@ -164,6 +177,15 @@ mod request {
let err = <ControlDTCSettingsRequest as Decode>::decode(&[0x09]).unwrap_err();
assert!(matches!(err, Error::InvalidDtcSetting(0x09)));
}

#[test]
fn exposes_allowed_nack_codes() {
assert!(!ControlDTCSettingsRequest::allowed_nack_codes().is_empty());
assert!(
ControlDTCSettingsRequest::allowed_nack_codes()
.contains(&NegativeResponseCode::RequestOutOfRange)
);
}
}

#[cfg(test)]
Expand All @@ -187,4 +209,9 @@ mod response {
assert_eq!(parsed.setting, DtcSettings::On);
assert_encode_size_agrees(&req);
}

#[test]
fn response_is_eq() {
crate::test_util::assert_impl_eq::<ControlDTCSettingsResponse>();
}
}
4 changes: 2 additions & 2 deletions src/services/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod clear_dtc_information;
pub use clear_dtc_information::ClearDiagnosticInfoRequest;
pub use clear_dtc_information::{ClearDiagnosticInfoRequest, ClearDiagnosticInfoResponse};

mod communication_control;
pub use communication_control::{
Expand All @@ -24,7 +24,7 @@ mod negative_response;
pub use negative_response::NegativeResponse;

mod read_data_by_identifier;
pub use read_data_by_identifier::ReadDataByIdentifierRequest;
pub use read_data_by_identifier::{ReadDataByIdentifierRequest, ReadDataByIdentifierResponse};

mod read_dtc_information;
pub use read_dtc_information::{
Expand Down
Loading
Loading