103103}
104104
105105
106- def get_argparser ():
106+ def _get_argparser ():
107107 """Get the command line argument parser."""
108108
109109 parser = argparse .ArgumentParser (
@@ -128,7 +128,7 @@ def get_argparser():
128128 return parser
129129
130130
131- def create_bus (args ):
131+ def _create_bus (args ):
132132 """Create and return a CAN bus, or None if initialization fails."""
133133 try :
134134 if args .devicename == "virtual" :
@@ -158,94 +158,98 @@ def create_bus(args):
158158 return None
159159
160160
161- def read_all_dids (args , bus , notifier ):
162- """Read all EDR DIDs via 11bits functional, 11bits physical, and 29bits addresses."""
163-
164- # Abbreviated name
165- func = isotp .TargetAddressType .Functional
166- phys = isotp .TargetAddressType .Physical
167-
168- # Read with 11bits functional address (See GB39732-2020 for the address values)
169- # rxid=0x700 is a dummy; the functional broadcast (txid=0x7DF) does not listen on a fixed ID.
170- tx_addr = isotp .Address (isotp .AddressingMode .Normal_11bits , txid = _TX_FUNC_11BIT , rxid = 0x700 )
171- rx_addrs = []
172- # Pre-allocate one receive stack per plausible physical CAN ID pair in the 0x700–0x7FF range.
173- # GB39732-2020 physical pairs follow the convention: ECU TX = tester TX + 8
161+ def _build_11func (bus , notifier , params ):
162+ """11bits functional: emit-only tx_stack on 0x7DF + per-ECU rx_stacks (0x700-0x7FF)."""
163+ # rxid=0x700 is a placeholder to satisfy Address validation; no ECU transmits on it.
164+ tx_addr = isotp .Address (
165+ isotp .AddressingMode .Normal_11bits , txid = _TX_FUNC_11BIT , rxid = 0x700
166+ )
167+ tx_stack = isotp .NotifierBasedCanStack (
168+ bus = bus , notifier = notifier , address = tx_addr , params = params
169+ )
170+ # Pre-allocate one rx stack per plausible per-ECU physical pair (DESIGN.md library-gap),
171+ # so the FF and the subsequent FC/CFs are received on the responder's own pair.
172+ rx_stacks = []
174173 for i in range (0x100 - 0x8 ):
175- rx_addrs .append (isotp .Address (
176- isotp .AddressingMode .Normal_11bits , txid = 0x700 + i , rxid = 0x700 + i + 8 ))
174+ rx_addr = isotp .Address (
175+ isotp .AddressingMode .Normal_11bits , txid = 0x700 + i , rxid = 0x700 + i + 8
176+ )
177+ rx_stacks .append (isotp .NotifierBasedCanStack (
178+ bus = bus , notifier = notifier , address = rx_addr , params = params
179+ ))
180+ return tx_stack , rx_stacks , isotp .TargetAddressType .Functional , "11bits functional address"
177181
178- try :
179- for did in _EDR_DID_LIST :
180- payload = read_did (did , bus , notifier , tx_addr , rx_addrs , func , _ISOTP_PARAMS , args .timeout )
181- output_data (payload )
182- except Exception as err :
183- print (err )
184182
185- # Read with 11bits physical address (See GB39732-2020 for the address values)
186- tx_addr = isotp .Address (isotp .AddressingMode .Normal_11bits , txid = _TX_PHYS_11BIT , rxid = _RX_PHYS_11BIT )
187- rx_addrs = []
183+ def _build_11phys (bus , notifier , params ):
184+ """11bits physical: single symmetric stack used for both send and receive."""
185+ addr = isotp .Address (
186+ isotp .AddressingMode .Normal_11bits , txid = _TX_PHYS_11BIT , rxid = _RX_PHYS_11BIT
187+ )
188+ stack = isotp .NotifierBasedCanStack (
189+ bus = bus , notifier = notifier , address = addr , params = params
190+ )
191+ return stack , [stack ], isotp .TargetAddressType .Physical , "11bits physical address"
188192
189- try :
190- for did in _EDR_DID_LIST :
191- payload = read_did (did , bus , notifier , tx_addr , rx_addrs , phys , _ISOTP_PARAMS , args .timeout )
192- output_data (payload )
193- except Exception as err :
194- print (err )
195193
196- # Read with 29bits address (See GB39732-2020 for the address values)
194+ def _build_29bit (bus , notifier , params ):
195+ """29bits NormalFixed: emit-only broadcast tx_stack + per-ECU rx_stacks."""
197196 tx_addr = isotp .Address (
198197 isotp .AddressingMode .NormalFixed_29bits ,
199198 target_address = _BROADCAST_29BIT ,
200- source_address = _TESTER_ADDR
199+ source_address = _TESTER_ADDR ,
200+ )
201+ tx_stack = isotp .NotifierBasedCanStack (
202+ bus = bus , notifier = notifier , address = tx_addr , params = params
201203 )
202- rx_addrs = []
204+ # Cover every plausible responder address (excluding the OBD functional broadcast).
205+ rx_stacks = []
203206 for i in range (0xF0 ):
204- if i != _OBD_FUNC_ADDR :
205- rx_addrs .append (isotp .Address (
206- isotp .AddressingMode .NormalFixed_29bits ,
207- target_address = i ,
208- source_address = _TESTER_ADDR
209- ))
210-
211- try :
212- for did in _EDR_DID_LIST :
213- payload = read_did (did , bus , notifier , tx_addr , rx_addrs , func , _ISOTP_PARAMS , args .timeout )
214- output_data (payload )
215- except Exception as err :
216- print (err )
207+ if i == _OBD_FUNC_ADDR :
208+ continue
209+ rx_addr = isotp .Address (
210+ isotp .AddressingMode .NormalFixed_29bits ,
211+ target_address = i ,
212+ source_address = _TESTER_ADDR ,
213+ )
214+ rx_stacks .append (isotp .NotifierBasedCanStack (
215+ bus = bus , notifier = notifier , address = rx_addr , params = params
216+ ))
217+ return tx_stack , rx_stacks , isotp .TargetAddressType .Functional , "29bits address"
217218
218219
219- def read_did (did , bus , notifier , tx_addr , rx_addrs , addr_type , isotp_params ,
220- timeout = _DEFAULT_TIMEOUT_S ) -> bytearray | None :
220+ def _read_all_dids (args , bus , notifier ):
221+ """Read all EDR DIDs via 11bits functional, 11bits physical, and 29bits addresses."""
222+ for builder in (_build_11func , _build_11phys , _build_29bit ):
223+ try :
224+ tx_stack , rx_stacks , addr_type , mode_label = builder (
225+ bus , notifier , _ISOTP_PARAMS
226+ )
227+ # The 11bits physical builder returns the same instance as tx_stack and
228+ # rx_stacks[0]; set() dedupes so start() / stop() run once per stack.
229+ # python-can-isotp's TransportLayer is designed for long-lived stacks:
230+ # construct once per scheme, reuse across DIDs, tear down at the end.
231+ all_stacks = {tx_stack , * rx_stacks }
232+ for s in all_stacks :
233+ s .start ()
234+ try :
235+ for did in _EDR_DID_LIST :
236+ payload = _read_did (
237+ did , tx_stack , rx_stacks , addr_type , mode_label , args .timeout
238+ )
239+ _output_data (payload )
240+ finally :
241+ for s in all_stacks :
242+ s .stop ()
243+ except Exception as err :
244+ print (err )
245+
246+
247+ def _read_did (did , tx_stack , rx_stacks , addr_type , mode_label ,
248+ timeout = _DEFAULT_TIMEOUT_S ) -> bytearray | None :
221249 """Read one data by identifier (DID) from the target ECU."""
222250
223251 print ("" )
224- if tx_addr .is_tx_29bits ():
225- print ("Reading data id" , hex (did ), "with 29bits address." )
226- elif addr_type == isotp .TargetAddressType .Functional :
227- print ("Reading data id" , hex (did ), "with 11bits functional address." )
228- else :
229- print ("Reading data id" , hex (did ), "with 11bits physical address." )
230-
231- # Setup ISOTP stacks
232- tx_stack = isotp .NotifierBasedCanStack (
233- bus = bus ,
234- notifier = notifier ,
235- address = tx_addr ,
236- params = isotp_params
237- )
238- rx_stacks = []
239- # In Physical addressing, request and response share the same address pair (tx_addr),
240- # so rx_addrs can be void. tx_stack is included in rx_stacks to receive the ECU's response.
241- rx_stacks .append (tx_stack )
242- for rx_addr in rx_addrs :
243- rx_stack = isotp .NotifierBasedCanStack (
244- bus = bus , notifier = notifier ,
245- address = rx_addr ,
246- params = isotp_params
247- )
248- rx_stacks .append (rx_stack )
252+ print (f"Reading data id { hex (did )} with { mode_label } ." )
249253
250254 # Build the UDS ReadDataByIdentifier request payload.
251255 # didconfig maps DID to a string codec; udsoncan requires it even though we
@@ -262,46 +266,35 @@ def read_did(did, bus, notifier, tx_addr, rx_addrs, addr_type, isotp_params,
262266 data = bytes ([(did >> 8 ) & 0xFF , did & 0xFF ]) # DID encoded as big-endian 2-byte
263267 )
264268
265- # Start stacks
266- for rx_stack in rx_stacks :
267- rx_stack .start ()
268-
269- # Send request
269+ # Send request. Stacks are started/stopped by the caller (_read_all_dids)
270+ # once per addressing scheme, not per DID.
270271 tx_stack .send (request .get_payload (), addr_type )
271272
272273 try :
273- # Wait for response
274- waiting = True
275- start_time = time .time ()
276- while waiting :
277- # Response timeout
278- if time .time () - start_time > timeout :
279- payload = None
280- break
281-
282- # Check response for all stacks
274+ # Wait for response.
275+ # Non-blocking recv(): a sweep over hundreds of rx_stacks takes microseconds
276+ # when empty, so the user-supplied timeout is honored within ~1 ms granularity.
277+ # monotonic() is immune to wall-clock adjustments mid-wait.
278+ payload = None
279+ deadline = time .monotonic () + timeout
280+ while payload is None and time .monotonic () < deadline :
283281 for rx_stack in rx_stacks :
284- # TODO: #42
285- payload = rx_stack .recv (block = True , timeout = 0.01 )
286- if payload is not None :
287- # Compare only the header portion of the received payload against
288- # the expected positive-response bytes. The remainder is data.
289- if payload [:len (response )] == response .get_payload ():
290- # Positive response
291- waiting = False
292- break
293- else :
294- # No Negative response handling. See the DESIGN.md.
295- pass
296-
282+ received = rx_stack .recv (block = False )
283+ if received is None :
284+ continue
285+ # Compare only the header portion of the received payload against
286+ # the expected positive-response bytes. The remainder is data.
287+ if received [:len (response )] == response .get_payload ():
288+ # Positive response
289+ payload = received
290+ break
291+ # Non-matching payload (e.g., negative response). See DESIGN.md.
292+ if payload is None :
293+ time .sleep (0.001 )
297294 except Exception as err :
298295 print (err )
299296 return None
300297
301- # Stop stacks
302- for rx_stack in rx_stacks :
303- rx_stack .stop ()
304-
305298 if payload is not None :
306299 print (len (payload ), "bytes of data received." )
307300 else :
@@ -310,7 +303,7 @@ def read_did(did, bus, notifier, tx_addr, rx_addrs, addr_type, isotp_params,
310303 return payload
311304
312305
313- def output_data (payload ) -> None :
306+ def _output_data (payload ) -> None :
314307 """Output the data to a CSV file according to the format defined in the 'format' folder."""
315308
316309 # Get target did from payload
@@ -379,7 +372,7 @@ def output_data(payload) -> None:
379372 return
380373
381374
382- def copy_readme ():
375+ def _copy_readme ():
383376 """Copy the README file from the format folder to the result folder."""
384377 try :
385378 shutil .copy ("format/README.md" , "result/README.md" )
@@ -397,11 +390,11 @@ def main():
397390 """Main process."""
398391
399392 # Parse command line arguments
400- argparser = get_argparser ()
393+ argparser = _get_argparser ()
401394 args = argparser .parse_args ()
402395
403396 # Setup and start a CAN bus
404- bus = create_bus (args )
397+ bus = _create_bus (args )
405398 if bus is None :
406399 return
407400
@@ -413,14 +406,14 @@ def main():
413406
414407 try :
415408 # Read all EDR DIDs
416- read_all_dids (args , bus , notifier )
409+ _read_all_dids (args , bus , notifier )
417410 finally :
418411 # Shutdown the CAN bus
419412 notifier .stop ()
420413 bus .shutdown ()
421414
422415 # Copy the README file
423- copy_readme ()
416+ _copy_readme ()
424417
425418
426419if __name__ == "__main__" :
0 commit comments