Skip to content

Commit c0facb6

Browse files
bri3dpd0wm
andauthored
J2534 Support (#101)
* Add J2534 support * fix comment style, do not set stmin_tx ioctl unless requested * run cargo fmt * dont use 5 year old winreg version * remove some stray comments * move constants to separate file with proper types * move dll magic into own file, might reuse later * Simplify J2534CanAdapter, make fully blocking * loopback flag already set * some more refactoring of common stuff into J2534Channel struct * return disconnected error * remove comment * move stmin helper into isotp code, ensure rounding up * refactor J2534NativeIsoTpTransport to take an IsoTPConfig * parse more fields out of the IsoTPConfig * unified error handling * add isotp example * attempt to fix 29 bit ids * handle isotp rx timeouts * this should fix RX of extended IDs * Turns those logs into errors * Those don't result in valid rx ids * Improve query_fw_versions for adapters without proper ACK support * assert on invalid 11 bit id * remove testing change --------- Co-authored-by: Willem Melching <willem.melching@gmail.com>
1 parent d6fb1cb commit c0facb6

17 files changed

Lines changed: 1661 additions & 18 deletions

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ repository = "https://github.com/I-CAN-hack/automotive"
1313
default = ["default-adapters"]
1414
all = ["all-adapters", "serde"]
1515
default-adapters = ["panda", "socketcan"]
16-
all-adapters = ["default-adapters", "vector-xl"]
16+
all-adapters = ["default-adapters", "vector-xl", "j2534"]
1717
serde = ["dep:serde"]
1818

1919
# adapters
2020
vector-xl = ["dep:bindgen", "dep:libloading"]
21+
j2534 = ["dep:libloading", "dep:winreg"]
2122
panda = []
2223
socketcan = []
2324

@@ -46,6 +47,7 @@ libc = "0.2.154"
4647

4748
[target.'cfg(target_os = "windows")'.dependencies]
4849
libloading = { version = "0.8", optional = true }
50+
winreg = { version = "0.55", optional = true }
4951

5052
[dev-dependencies]
5153
futures = "0.3.30"

examples/query_fw_versions.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
use std::time::Duration;
2+
13
use automotive::can::AsyncCanAdapter;
2-
use automotive::isotp::IsoTPAdapter;
4+
use automotive::isotp::{IsoTPAdapter, IsoTPConfig};
35
use automotive::Result;
46

57
use automotive::uds::DataIdentifier;
@@ -9,7 +11,13 @@ use bstr::ByteSlice;
911
use strum::IntoEnumIterator;
1012

1113
async fn get_version(adapter: &AsyncCanAdapter, identifier: u32) -> Result<()> {
12-
let isotp = IsoTPAdapter::from_id(adapter, identifier);
14+
let mut config = IsoTPConfig::new(0, identifier.into());
15+
16+
// Increased timeout for adapters not supporting real ACKs
17+
// We send a lot of frames, so the timeout might start counting before the relevant frame is sent
18+
config.timeout = Duration::from_secs(1);
19+
20+
let isotp = IsoTPAdapter::new(adapter, config);
1321
let uds = UDSClient::new(&isotp);
1422

1523
uds.tester_present().await?;
@@ -35,7 +43,7 @@ async fn main() {
3543

3644
let adapter = automotive::can::get_adapter().unwrap();
3745

38-
let standard_ids = 0x700..=0x7ff;
46+
let standard_ids = 0x700..=0x7f7;
3947
let extended_ids = (0x00..=0xff).map(|i| 0x18da0000 + (i << 8) + 0xf1);
4048

4149
let ids: Vec<u32> = standard_ids.chain(extended_ids).collect();
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#[cfg(all(target_os = "windows", feature = "j2534"))]
2+
mod app {
3+
use automotive::isotp::IsoTPConfig;
4+
use automotive::j2534::J2534NativeIsoTpTransport;
5+
use automotive::uds::{DataIdentifier, UDSClient};
6+
use automotive::Result;
7+
8+
use bstr::ByteSlice;
9+
use strum::IntoEnumIterator;
10+
11+
async fn get_version(isotp: &J2534NativeIsoTpTransport, identifier: u32) -> Result<()> {
12+
let uds = UDSClient::new(isotp);
13+
14+
uds.tester_present().await?;
15+
16+
for did in DataIdentifier::iter() {
17+
if let Ok(resp) = uds.read_data_by_identifier(did as u16).await {
18+
println!(
19+
"{:x} 0x{:x} {:?}: {:?}",
20+
identifier,
21+
did as u16,
22+
did,
23+
resp.as_bstr()
24+
);
25+
}
26+
}
27+
28+
Ok(())
29+
}
30+
31+
pub async fn run() -> Result<()> {
32+
tracing_subscriber::fmt::init();
33+
34+
let dll_path = None;
35+
let bitrate = 500_000;
36+
37+
let standard_ids = 0x700..=0x7f7;
38+
let extended_ids = (0x00..=0xff).map(|i| 0x18da0000 + (i << 8) + 0xf1);
39+
let mut ids = standard_ids.chain(extended_ids);
40+
41+
let Some(first_id) = ids.next() else {
42+
return Ok(());
43+
};
44+
45+
let config = IsoTPConfig::new(0, first_id.into());
46+
let isotp = J2534NativeIsoTpTransport::new(dll_path, bitrate, config)?;
47+
let _ = get_version(&isotp, first_id).await;
48+
let mut device = isotp.into_device();
49+
50+
for id in ids {
51+
let config = IsoTPConfig::new(0, id.into());
52+
let isotp = match J2534NativeIsoTpTransport::new_on_device(device, bitrate, config) {
53+
Ok(isotp) => isotp,
54+
Err(_) => J2534NativeIsoTpTransport::new(dll_path, bitrate, config)?,
55+
};
56+
57+
let _ = get_version(&isotp, id).await;
58+
device = isotp.into_device();
59+
}
60+
61+
Ok(())
62+
}
63+
}
64+
65+
#[cfg(all(target_os = "windows", feature = "j2534"))]
66+
#[tokio::main]
67+
async fn main() -> automotive::Result<()> {
68+
app::run().await
69+
}
70+
71+
#[cfg(not(all(target_os = "windows", feature = "j2534")))]
72+
fn main() {
73+
eprintln!("This example requires Windows and the `j2534` feature.");
74+
}

src/can/adapter.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,12 @@ pub fn get_adapter() -> Result<crate::can::AsyncCanAdapter, crate::error::Error>
2828
};
2929
}
3030

31+
#[cfg(all(target_os = "windows", feature = "j2534"))]
32+
{
33+
if let Ok(adapter) = crate::j2534::J2534CanAdapter::new_async(None, 500_000) {
34+
return Ok(adapter);
35+
};
36+
}
37+
3138
Err(crate::error::Error::NotFound)
3239
}

src/can/async_can.rs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ use tokio::sync::{broadcast, mpsc, oneshot};
1212
use tracing::debug;
1313

1414
const CAN_TX_BUFFER_SIZE: usize = 128;
15-
const CAN_RX_BUFFER_SIZE: usize = 1024;
15+
// Must be large enough to absorb all CAN frames generated during a
16+
// multi-frame ISO-TP transfer without the recv subscriber being polled.
17+
// A 4 KiB UDS payload at 7 bytes/CF ≈ 585 CFs; each produces a loopback
18+
// echo plus the real frame. Back-to-back transfers or mixed TX/RX traffic
19+
// can double that. 8192 gives ample headroom.
20+
const CAN_RX_BUFFER_SIZE: usize = 8192;
1621
const DEBUG: bool = false;
1722

1823
type BusIdentifier = (u8, Identifier);
@@ -28,7 +33,13 @@ fn process<T: CanAdapter>(
2833
let mut callbacks: HashMap<BusIdentifier, VecDeque<FrameCallback>> = HashMap::new();
2934

3035
while shutdown_receiver.try_recv().is_err() {
31-
let frames: Vec<Frame> = adapter.recv().expect("Failed to Receive CAN Frames");
36+
let frames: Vec<Frame> = match adapter.recv() {
37+
Ok(f) => f,
38+
Err(e) => {
39+
debug!("Adapter recv error: {:?} — shutting down process loop", e);
40+
break;
41+
}
42+
};
3243

3344
for frame in frames {
3445
if DEBUG {
@@ -98,7 +109,7 @@ pub struct AsyncCanAdapter {
98109
}
99110

100111
impl AsyncCanAdapter {
101-
pub fn new<T: CanAdapter + Send + Sync + 'static>(adapter: T) -> Self {
112+
pub fn new<T: CanAdapter + Send + 'static>(adapter: T) -> Self {
102113
let (shutdown_sender, shutdown_receiver) = oneshot::channel();
103114
let (send_sender, send_receiver) = mpsc::channel(CAN_TX_BUFFER_SIZE);
104115
let (recv_sender, recv_receiver) = broadcast::channel(CAN_RX_BUFFER_SIZE);
@@ -145,7 +156,10 @@ impl AsyncCanAdapter {
145156
yield frame
146157
}
147158
},
148-
Err(RecvError::Closed) => panic!("Adapter thread has exited"),
159+
Err(RecvError::Closed) => {
160+
tracing::debug!("Adapter broadcast closed — ending recv stream");
161+
return;
162+
},
149163
Err(RecvError::Lagged(n)) => {
150164
tracing::warn!("Receive too slow, dropping {} frame(s).", n)
151165
},
@@ -158,9 +172,15 @@ impl AsyncCanAdapter {
158172
impl Drop for AsyncCanAdapter {
159173
fn drop(&mut self) {
160174
if let Some(handle) = self.processing_handle.take() {
161-
// Send shutdown signal to background tread
162-
self.shutdown.take().unwrap().send(()).unwrap();
163-
handle.join().unwrap();
175+
// Send shutdown signal to background thread.
176+
// Use `ok()` instead of `unwrap()` because the receiver may already
177+
// be dropped if the process thread exited early (e.g. adapter error).
178+
if let Some(shutdown) = self.shutdown.take() {
179+
let _ = shutdown.send(());
180+
}
181+
// Join the thread; use `ok()` to avoid panicking inside Drop if the
182+
// process thread panicked (double-panic would abort the process).
183+
let _ = handle.join();
164184
}
165185
}
166186
}

src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ pub enum Error {
3131
#[error("Error loading DLL: {0}")]
3232
Libloading(std::sync::Arc<libloading::Error>),
3333

34+
#[cfg(all(target_os = "windows", feature = "j2534"))]
35+
#[error(transparent)]
36+
J2534Error(#[from] crate::j2534::Error),
37+
3438
#[cfg(feature = "panda")]
3539
#[error(transparent)]
3640
PandaError(#[from] crate::panda::Error),

src/isotp/mod.rs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,38 @@ use tracing::debug;
3030
use self::types::FlowControlConfig;
3131

3232
const DEFAULT_TIMEOUT_MS: u64 = 100;
33+
34+
/// Abstraction over anything that can exchange ISO-TP PDUs.
35+
///
36+
/// Two implementations ship with this workspace:
37+
/// * [`IsoTPAdapter`] — software ISO-TP framing on top of any [`crate::can::CanAdapter`].
38+
/// * `J2534NativeIsoTpTransport` — hardware ISO 15765 via a J2534 PassThru device; the
39+
/// adapter firmware handles all framing, flow-control, and STmin timing.
40+
pub trait IsoTpTransport {
41+
/// Transmit a single UDS PDU. Resolves once the transport has accepted the
42+
/// payload for transmission (not necessarily once it has been ACKed on the bus).
43+
fn send<'a>(
44+
&'a self,
45+
data: &'a [u8],
46+
) -> impl std::future::Future<Output = crate::Result<()>> + 'a;
47+
48+
/// Infinite stream of received UDS PDUs. Each item is one complete PDU.
49+
fn recv(&self) -> impl crate::Stream<Item = crate::Result<Vec<u8>>> + Unpin + '_;
50+
}
51+
52+
impl IsoTpTransport for IsoTPAdapter<'_> {
53+
fn send<'a>(
54+
&'a self,
55+
data: &'a [u8],
56+
) -> impl std::future::Future<Output = crate::Result<()>> + 'a {
57+
IsoTPAdapter::send(self, data)
58+
}
59+
60+
fn recv(&self) -> impl crate::Stream<Item = crate::Result<Vec<u8>>> + Unpin + '_ {
61+
IsoTPAdapter::recv(self)
62+
}
63+
}
64+
3365
const DEFAULT_PADDING_BYTE: u8 = 0xAA;
3466

3567
/// N_WFTmax in ISO 15765-2
@@ -39,7 +71,34 @@ const CAN_MAX_DLEN: usize = 8;
3971
const CAN_FD_MAX_DLEN: usize = 64;
4072

4173
const ISO_TP_MAX_DLEN: usize = (1 << 12) - 1;
42-
const ISO_TP_FD_MAX_DLEN: usize = (1 << 32) - 1;
74+
const ISO_TP_FD_MAX_DLEN: usize = u32::MAX as usize;
75+
76+
/// Encode a separation-time value to the ISO 15765-2 STmin byte.
77+
///
78+
/// Values that do not land exactly on a representable STmin step round up to
79+
/// the next valid encoding so the requested delay is never undershot.
80+
///
81+
/// Encoding (ISO 15765-2 §9.6.5.4 / Table 5):
82+
/// - 0 µs -> `0x00` (no delay)
83+
/// - 1-900 µs -> `0xF1`-`0xF9` (100 µs steps, rounded up)
84+
/// - 901-127_000 µs -> `0x01`-`0x7F` (1 ms steps, rounded up)
85+
/// - Values above 127 ms clamp to `0x7F`
86+
pub fn duration_to_stmin_byte(duration: std::time::Duration) -> u8 {
87+
let us = duration.as_micros().min(u128::from(u32::MAX)) as u32;
88+
if us == 0 {
89+
0x00
90+
} else if us < 1_000 {
91+
let steps = us.saturating_add(99) / 100;
92+
if steps <= 9 {
93+
0xF0 + steps as u8
94+
} else {
95+
0x01
96+
}
97+
} else {
98+
let ms = us.saturating_add(999) / 1_000;
99+
ms.min(127) as u8
100+
}
101+
}
43102

44103
/// Configuring passed to the IsoTPAdapter.
45104
#[derive(Debug, Clone, Copy)]
@@ -537,3 +596,36 @@ impl<'a> IsoTPAdapter<'a> {
537596
})
538597
}
539598
}
599+
600+
#[cfg(test)]
601+
mod tests {
602+
use super::duration_to_stmin_byte;
603+
use std::time::Duration;
604+
605+
#[test]
606+
fn duration_to_stmin_byte_rounds_up_to_supported_microsecond_steps() {
607+
assert_eq!(duration_to_stmin_byte(Duration::ZERO), 0x00);
608+
assert_eq!(duration_to_stmin_byte(Duration::from_micros(1)), 0xF1);
609+
assert_eq!(duration_to_stmin_byte(Duration::from_micros(99)), 0xF1);
610+
assert_eq!(duration_to_stmin_byte(Duration::from_micros(100)), 0xF1);
611+
assert_eq!(duration_to_stmin_byte(Duration::from_micros(101)), 0xF2);
612+
assert_eq!(duration_to_stmin_byte(Duration::from_micros(900)), 0xF9);
613+
assert_eq!(duration_to_stmin_byte(Duration::from_micros(901)), 0x01);
614+
assert_eq!(duration_to_stmin_byte(Duration::from_micros(999)), 0x01);
615+
}
616+
617+
#[test]
618+
fn duration_to_stmin_byte_rounds_up_to_supported_millisecond_steps() {
619+
assert_eq!(duration_to_stmin_byte(Duration::from_micros(1_000)), 0x01);
620+
assert_eq!(duration_to_stmin_byte(Duration::from_micros(1_001)), 0x02);
621+
assert_eq!(duration_to_stmin_byte(Duration::from_micros(1_999)), 0x02);
622+
assert_eq!(duration_to_stmin_byte(Duration::from_micros(2_000)), 0x02);
623+
}
624+
625+
#[test]
626+
fn duration_to_stmin_byte_clamps_to_maximum_supported_delay() {
627+
assert_eq!(duration_to_stmin_byte(Duration::from_micros(127_000)), 0x7F);
628+
assert_eq!(duration_to_stmin_byte(Duration::from_micros(127_001)), 0x7F);
629+
assert_eq!(duration_to_stmin_byte(Duration::from_secs(u64::MAX)), 0x7F);
630+
}
631+
}

0 commit comments

Comments
 (0)