Guest User

OBD Thingy

a guest
Nov 10th, 2025
10
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 23.33 KB | Software | 0 0
  1. #!/usr/bin/env python3
  2. """
  3. mode01_tester.py
  4. Minimal script to test OBD-II Mode 01 (live data) using an ELM327.
  5. """
  6.  
  7. import time
  8. import sys
  9.  
  10. PORT = "/dev/rfcomm0"  # Change to your port (e.g., COM6, /dev/ttyUSB0)
  11. # Protocol will determine baud; do not set baud manually here
  12. BAUD = None
  13. TIMEOUT = 1.0
  14.  
  15. OBD_PIDS = {
  16.     "010C": ("Engine RPM", lambda A, B: ((A * 256) + B) / 4, "rpm"),
  17.     "010D": ("Vehicle Speed", lambda A: A, "km/h"),
  18.     "0105": ("Coolant Temp", lambda A: A - 40, "°C"),
  19.     "0111": ("Throttle Position", lambda A: A * 100 / 255, "%"),
  20.     "0104": ("Engine Load", lambda A: A * 100 / 255, "%"),
  21.     "010F": ("Intake Air Temp", lambda A: A - 40, "°C"),
  22.     "0110": ("MAF Rate", lambda A, B: ((A * 256) + B) / 100, "g/s"),
  23.     "012F": ("Fuel Level", lambda A: A * 100 / 255, "%"),
  24.     "1303": ("Fuel System Status", lambda A, B: f"Status A: {A}, Status B: {B}", ""),
  25.     "1501": ("DTC Count", lambda A: A & 0x7F, "count"),  # Number of stored DTCs
  26.     "1503": ("Pending DTC Count", lambda A: A & 0x7F, "count"),
  27. }
  28.  
  29.  
  30. def _decode_obd_value(val):
  31.     """Decode common python-obd returned values into readable strings.
  32.  
  33.    Handles bytes/bytearray (decodes to ASCII/UTF-8), lists/tuples of bytes
  34.    or ints (joins sensible representations), and falls back to str().
  35.    """
  36.     if val is None:
  37.         return "NO DATA"
  38.  
  39.     # Helper to produce hex representation for bytes-like
  40.     def _hex(b):
  41.         try:
  42.             return b.hex().upper()
  43.         except Exception:
  44.             try:
  45.                 return bytes(b).hex().upper()
  46.             except Exception:
  47.                 return None
  48.  
  49.     try:
  50.         # If it's an object that wraps a value (many python-obd result types), unwrap common attrs
  51.         if not isinstance(val, (str, bytes, bytearray, list, tuple, int, float, memoryview)):
  52.             for attr in ('value', 'raw', 'data', 'payload'):
  53.                 if hasattr(val, attr):
  54.                     try:
  55.                         inner = getattr(val, attr)
  56.                         # recurse
  57.                         return _decode_obd_value(inner)
  58.                     except Exception:
  59.                         pass
  60.  
  61.         # bytes/bytearray/memoryview -> try decode and also show hex
  62.         if isinstance(val, (bytes, bytearray, memoryview)):
  63.             try:
  64.                 b = bytes(val)
  65.             except Exception:
  66.                 b = val
  67.             # try textual decode
  68.             try:
  69.                 text = b.decode('utf-8', errors='replace')
  70.             except Exception:
  71.                 try:
  72.                     text = b.decode('latin-1', errors='replace')
  73.                 except Exception:
  74.                     text = None
  75.  
  76.             hexs = _hex(b)
  77.             # prefer printable text, but show hex in parenthesis for clarity
  78.             if text:
  79.                 text_clean = ''.join(ch for ch in text if ch.isprintable()).strip()
  80.                 if text_clean:
  81.                     return f"{text_clean} (hex:{hexs})" if hexs else text_clean
  82.             # fallback: show hex only
  83.             return f"hex:{hexs}" if hexs else repr(val)
  84.  
  85.         # list/tuple often contains ints (bytes) or mixed items
  86.         if isinstance(val, (list, tuple)):
  87.             # if all ints in 0-255, treat as byte array
  88.             if all(isinstance(x, int) and 0 <= x <= 255 for x in val):
  89.                 b = bytes(val)
  90.                 hexs = _hex(b)
  91.                 try:
  92.                     text = b.decode('utf-8', errors='replace')
  93.                     text_clean = ''.join(ch for ch in text if ch.isprintable()).strip()
  94.                     if text_clean:
  95.                         return f"{text_clean} (hex:{hexs})"
  96.                 except Exception:
  97.                     pass
  98.                 return f"hex:{hexs}"
  99.  
  100.             # otherwise join items sensibly
  101.             parts = []
  102.             for item in val:
  103.                 try:
  104.                     parts.append(_decode_obd_value(item))
  105.                 except Exception:
  106.                     parts.append(str(item))
  107.             return ','.join(parts)
  108.  
  109.         # primitive types
  110.         if isinstance(val, str):
  111.             return val
  112.         if isinstance(val, (int, float)):
  113.             return str(val)
  114.  
  115.         # fallback to repr
  116.         try:
  117.             return repr(val)
  118.         except Exception:
  119.             return '<undecodable>'
  120.     except Exception:
  121.         try:
  122.             return repr(val)
  123.         except Exception:
  124.             return '<undecodable>'
  125.  
  126.  
  127. # Note: Raw serial helpers and AT command fallbacks removed.
  128. # This script now uses only the `python-obd` library for all communication.
  129.  
  130.  
  131. def parse_pid_response(resp, pid):
  132.     """Parse Mode 01 response (41 xx A B)."""
  133.     tokens = resp.split()
  134.     if "41" not in tokens:
  135.         return None
  136.     try:
  137.         idx = tokens.index("41")
  138.         pid_returned = tokens[idx + 1]
  139.         A = int(tokens[idx + 2], 16)
  140.         B = int(tokens[idx + 3], 16) if len(tokens) > idx + 3 else 0
  141.         name, func, unit = OBD_PIDS[pid]
  142.         value = func(A, B) if func.__code__.co_argcount == 2 else func(A)
  143.         return f"{name}: {value:.1f} {unit}"
  144.     except Exception:
  145.         return None
  146.  
  147.  
  148. def main():
  149.     import argparse as _arg
  150.  
  151.     p = _arg.ArgumentParser(description="OBD scanner using python-obd")
  152.     p.add_argument("-p", "--port", required=False, default=PORT, help="serial port (e.g. /dev/rfcomm0)")
  153.     p.add_argument("--timeout", type=float, default=TIMEOUT, help="connection timeout")
  154.     p.add_argument("--protocol", type=str, default="3", help="diagnostic protocol to request (e.g. '6' for ISO15765-4). Default is KWP-like '3'")
  155.     p.add_argument("--scan-protos", action="store_true", help="try multiple common protocols until one connects")
  156.     p.add_argument("--discover", choices=['none','kwp','uds','all'], default='kwp', help="discovery mode to run: 'kwp' tries KWP protocol variants; 'uds' tries UDS/CAN variants; 'all' tries both. Default: kwp")
  157.     p.add_argument("--protocols", type=str, default="6,1,2,3,5", help="comma-separated list of protocols to try when --scan-protos is used (eg '6,1,2,3,5')")
  158.     p.add_argument("--dry-run", action="store_true", help="don't open real port")
  159.     p.add_argument("--no-log", action="store_true", help="disable logging")
  160.     p.add_argument("--live", action="store_true", help="query common PIDs after DTC scan")
  161.     p.add_argument("--probe-vin", action="store_true", help="query Mode 09 VIN and basic vehicle info after connect")
  162.     p.add_argument("--did", type=str, default=None, help="optional hex DID to probe using ReadDataByIdentifier (0x22). Example: --did F190")
  163.     p.add_argument("--did-discover", type=str, default=None, help="discover DIDs by range (e.g. 1000-10FF) or comma list (e.g. F190,1001). Scans using 0x22 reads.")
  164.     p.add_argument("--did-delay", type=float, default=0.1, help="delay seconds between DID probes (default 0.1)")
  165.     p.add_argument("--did-out", type=str, default=None, help="optional file to write discovered DIDs")
  166.     p.add_argument("--did-header", type=str, default=None, help="optional header (CAN ID) to use for DID probes, e.g. 7E0")
  167.     p.add_argument("--did-try-headers", action="store_true", help="try a small set of common headers (7E0,7E1,7E8,7DF)")
  168.     p.add_argument("--did-preset", type=str, default=None, help="use a curated preset list of DIDs (e.g. 'bmw')")
  169.     args = p.parse_args()
  170.  
  171.     if args.no_log:
  172.         global LOG_ENABLED
  173.         LOG_ENABLED = False
  174.  
  175.     # Dry-run: just print intended actions
  176.     if args.dry_run:
  177.         print("[DRY-RUN] Would open OBD connection on", args.port)
  178.         if args.live:
  179.             print("[DRY-RUN] Would query live PIDs: RPM, SPEED, COOLANT_TEMP, THROTTLE_POS, ENGINE_LOAD")
  180.         return
  181.  
  182.     # Import obd library
  183.     # Ensure serial.SerialException exists on the serial module (some pyserial installs expose it under serial.serialutil)
  184.     try:
  185.         import serial
  186.         if not hasattr(serial, 'SerialException'):
  187.             try:
  188.                 from serial.serialutil import SerialException as _SerialException
  189.                 serial.SerialException = _SerialException
  190.             except Exception:
  191.                 pass
  192.         # Ensure serial_for_url exists (pyserial usually provides this helper).
  193.         if not hasattr(serial, 'serial_for_url'):
  194.             try:
  195.                 # Use the POSIX backend directly for simple device paths (e.g. /dev/rfcomm0)
  196.                 from serial import serialposix
  197.  
  198.                 def _serial_for_url(url, **kwargs):
  199.                     # For simple filesystem device paths, use Serial directly
  200.                     return serialposix.Serial(url, **kwargs)
  201.  
  202.                 serial.serial_for_url = _serial_for_url
  203.             except Exception:
  204.                 # best-effort only
  205.                 pass
  206.         # Ensure common constants and classes exist on serial module
  207.         try:
  208.             from serial import serialutil as _su
  209.             from serial import serialposix as _sp
  210.             if not hasattr(serial, 'SerialException') and hasattr(_su, 'SerialException'):
  211.                 serial.SerialException = _su.SerialException
  212.             # copy common constants
  213.             for const in ('PARITY_NONE','PARITY_EVEN','PARITY_ODD','PARITY_MARK','PARITY_SPACE',
  214.                           'STOPBITS_ONE','STOPBITS_ONE_POINT_FIVE','STOPBITS_TWO',
  215.                           'FIVEBITS','SIXBITS','SEVENBITS','EIGHTBITS'):
  216.                 if hasattr(_su, const) and not hasattr(serial, const):
  217.                     setattr(serial, const, getattr(_su, const))
  218.             # ensure top-level Serial points to posix Serial when available
  219.             if not hasattr(serial, 'Serial') and hasattr(_sp, 'Serial'):
  220.                 serial.Serial = _sp.Serial
  221.             # ensure serial_for_url exists
  222.             if not hasattr(serial, 'serial_for_url') and hasattr(_su, 'serial_for_url'):
  223.                 serial.serial_for_url = _su.serial_for_url
  224.         except Exception:
  225.             pass
  226.     except Exception:
  227.         # ignore: serial might not be installed yet; obd import will surface that error
  228.         pass
  229.  
  230.     try:
  231.         import obd
  232.         from obd import commands as cmds
  233.     except Exception as e:
  234.         print(f"[ERROR] could not import 'obd' library: {e}\nInstall it with: conda activate obd-scraper && pip install obd")
  235.         return
  236.  
  237.     port = args.port
  238.     timeout = args.timeout
  239.     protocol = args.protocol
  240.  
  241.     # If RFCOMM, give the link a short moment to settle before opening.
  242.     if port and ("rfcomm" in str(port) or "/dev/rf" in str(port)):
  243.         print("[INFO] RFCOMM port detected; waiting briefly for the link to settle...")
  244.         time.sleep(2.0)
  245.  
  246.     print(f"[INFO] Opening OBD connection on {port} (protocol-selected baud) ...")
  247.     con = None
  248.     max_attempts = 3
  249.  
  250.     # Build protocol list if scanning requested
  251.     # Build protocol list based on discover mode or explicit scanning
  252.     proto_list = [protocol]
  253.     if args.scan_protos:
  254.         try:
  255.             proto_list = [p.strip() for p in args.protocols.split(',') if p.strip()]
  256.         except Exception:
  257.             proto_list = [protocol]
  258.     else:
  259.         # If discover mode is provided and not 'none', pick protocol sets
  260.         if args.discover and args.discover != 'none':
  261.             if args.discover == 'kwp':
  262.                 # KWP variants: ISO 9141-2 / KWP2000 slow/fast (common numeric aliases used by ELM/obd lib)
  263.                 proto_list = ['3', '1', '5']  # 3=ISO9141-2, 1=KWP? (varies), 5=ISO14230 (fast)
  264.             elif args.discover == 'uds':
  265.                 # UDS/CAN variants (ISO15765-4 and others)
  266.                 proto_list = ['6', '7', '8']  # 6=ISO15765-4, 7/8 placeholder CAN variants
  267.             elif args.discover == 'all':
  268.                 proto_list = ['6', '1', '5', '3', '7', '8']
  269.  
  270.     for proto in proto_list:
  271.         for attempt in range(1, max_attempts + 1):
  272.             try:
  273.                 # Do not pass baudrate; let the protocol/adapter negotiation decide
  274.                 print(f"[INFO] Trying protocol '{proto}' (attempt {attempt}) ...")
  275.                 con = obd.OBD(portstr=port, fast=False, timeout=timeout, protocol=proto)
  276.             except Exception as e:
  277.                 print(f"[ERROR] Could not open OBD connection (proto={proto} attempt {attempt}): {e}")
  278.                 con = None
  279.  
  280.             if con is not None and con.is_connected():
  281.                 print(f"[OK] OBD connected with protocol {proto} (attempt {attempt}).")
  282.                 # update the active protocol variable
  283.                 protocol = proto
  284.                 break
  285.             else:
  286.                 if con is not None:
  287.                     try:
  288.                         con.close()
  289.                     except Exception:
  290.                         pass
  291.                 print(f"[WARN] OBD not connected (proto={proto} attempt {attempt}). Retrying in 2s...")
  292.                 time.sleep(2.0)
  293.  
  294.         if con is not None and con.is_connected():
  295.             break
  296.  
  297.     if con is None or not con.is_connected():
  298.         print("[ERROR] Unable to establish OBD connection after retries. Check pairing, port, and adapter baud settings.")
  299.         # continue to attempt queries — the library will return null responses
  300.  
  301.     # Query DTCs using the library
  302.     # Protocol selection is handled by the python-obd library and adapter auto-detection.
  303.  
  304.     print("[SCAN] Querying DTCs via python-obd ...")
  305.     try:
  306.         resp = con.query(cmds.GET_DTC)
  307.     except Exception as e:
  308.         print(f"[ERROR] GET_DTC query failed: {e}")
  309.         resp = None
  310.  
  311.     if resp is None or resp.is_null():
  312.         print("[OK] No DTCs or GET_DTC not supported/returned no data.")
  313.     else:
  314.         # resp.value could be a list of DTC tuples or similar; print defensively
  315.         val = getattr(resp, 'value', None)
  316.         if val is None:
  317.             print("[OK] GET_DTC returned no value.")
  318.         else:
  319.             try:
  320.                 # Some versions return list of tuples (code, count)
  321.                 if isinstance(val, (list, tuple)):
  322.                     if len(val) == 0:
  323.                         print("[OK] No DTCs reported.")
  324.                     else:
  325.                         print("[RESULT] DTCs found:")
  326.                         for entry in val:
  327.                             print(" ", entry)
  328.                 else:
  329.                     print("[RESULT] ", val)
  330.             except Exception:
  331.                 print("[RESULT] ", val)
  332.  
  333.             # Probe VIN and basic vehicle info (Mode 09) if requested
  334.             if args.probe_vin:
  335.                 print("\n[PROBE] Querying Mode 09 (VIN/calibration IDs) ...")
  336.                 # python-obd exposes some Mode-09 commands; try VIN and a few common fields
  337.                 vin_cmd = getattr(cmds, 'VIN', None)
  338.                 if vin_cmd is not None:
  339.                     try:
  340.                         r = con.query(vin_cmd)
  341.                         if r is None or r.is_null():
  342.                             print("VIN: NO DATA")
  343.                         else:
  344.                             print("VIN:", _decode_obd_value(r.value))
  345.                     except Exception as e:
  346.                         print("VIN query failed:", e)
  347.                 else:
  348.                     print("VIN command not available in this python-obd build.")
  349.  
  350.                 # Some adapters/vehicles expose calibration or module IDs via specific Mode-09 PIDs.
  351.                 # Try a few common names if present in obd.commands
  352.                 cal_like = ['CAL_ID', 'CALIBRATION_ID', 'CVN', 'ECU_NAME']
  353.                 for name in cal_like:
  354.                     c = getattr(cmds, name, None)
  355.                     if c is None:
  356.                         continue
  357.                     try:
  358.                         r = con.query(c)
  359.                         print(f"{name}:", _decode_obd_value(r.value) if r and not r.is_null() else "NO DATA")
  360.                     except Exception as e:
  361.                         print(f"{name} query failed:", e)
  362.  
  363.             # If user provided a DID, attempt a ReadDataByIdentifier (0x22) probe
  364.             if args.did:
  365.                 did_hex = args.did.strip()
  366.                 # normalize: accept F190 or 0xF190
  367.                 if did_hex.startswith('0x') or did_hex.startswith('0X'):
  368.                     did_hex = did_hex[2:]
  369.  
  370.                 # ensure even-length
  371.                 if len(did_hex) % 2 != 0:
  372.                     did_hex = '0' + did_hex
  373.  
  374.                 try:
  375.                     # Build raw command bytes for service 0x22 + DID
  376.                     raw = '22' + did_hex.upper()
  377.                     print(f"\n[DID] Sending ReadDataByIdentifier (0x22) => {raw}")
  378.                     # Create a minimal decode function that returns raw hex bytes
  379.                     from obd import OBDCommand, Unit
  380.  
  381.                     def decode_raw(messages):
  382.                         # messages is a list of obd.replies.Response or raw strings depending on python-obd
  383.                         try:
  384.                             # If python-obd returns a Response object, get .value or .data
  385.                             m = messages[0]
  386.                             txt = str(m)
  387.                         except Exception:
  388.                             txt = repr(messages)
  389.                         return txt
  390.  
  391.                     custom = OBDCommand(name=f"DID_{did_hex}", command=raw, ECU=None, value=decode_raw)
  392.                     try:
  393.                         r = con.query(custom)
  394.                         if r is None or r.is_null():
  395.                             print(f"DID {did_hex}: NO DATA or not supported")
  396.                         else:
  397.                             print(f"DID {did_hex}: {r.value}")
  398.                     except Exception as e:
  399.                         print(f"DID probe failed: {e}")
  400.                 except Exception as e:
  401.                     print(f"DID probe setup failed: {e}")
  402.  
  403.     # Automated DID discovery
  404.     if args.did_discover:
  405.         print(f"\n[DID-DISCOVER] Starting DID discovery: {args.did_discover}")
  406.         # parse range or csv
  407.         did_targets = []
  408.         s = args.did_discover.strip()
  409.         # support presets
  410.         if args.did_preset:
  411.             preset = args.did_preset.lower()
  412.             if preset == 'bmw':
  413.                 # BMW commonly uses DIDs in the F1xx/F19x ranges for body/engine modules.
  414.                 # Expand to the F100-F1FF block to increase discovery coverage.
  415.                 did_targets = [format(x, 'X') for x in range(int('F100', 16), int('F1FF', 16) + 1)]
  416.             else:
  417.                 print(f"Unknown DID preset: {args.did_preset}")
  418.                 did_targets = []
  419.         elif '-' in s:
  420.             a, b = s.split('-', 1)
  421.             try:
  422.                 start = int(a, 16)
  423.                 end = int(b, 16)
  424.                 if start > end:
  425.                     start, end = end, start
  426.                 did_targets = [format(x, 'X') for x in range(start, end + 1)]
  427.             except Exception as e:
  428.                 print("Invalid DID range:", e)
  429.                 did_targets = []
  430.         else:
  431.             for part in s.split(','):
  432.                 p = part.strip()
  433.                 if p:
  434.                     did_targets.append(p.upper().lstrip('0X'))
  435.  
  436.         discovered = []
  437.         from obd import OBDCommand
  438.         # build header list
  439.         hdrs = []
  440.         if args.did_header:
  441.             hdrs = [args.did_header.strip()]
  442.         elif args.did_try_headers:
  443.             hdrs = ['7E0', '7E1', '7E8', '7DF']
  444.         else:
  445.             hdrs = [None]
  446.  
  447.         for did_hex in did_targets:
  448.             # normalize
  449.             dh = did_hex.upper()
  450.             if len(dh) % 2 != 0:
  451.                 dh = '0' + dh
  452.             raw = '22' + dh
  453.  
  454.             def decode_raw_once(messages):
  455.                 try:
  456.                     m = messages[0]
  457.                     return str(m)
  458.                 except Exception:
  459.                     return repr(messages)
  460.  
  461.             # try each header until a response or exhaustion
  462.             for h in hdrs:
  463.                 header_kw = {}
  464.                 if h:
  465.                     # python-obd expects header as bytes
  466.                     try:
  467.                         header_kw['header'] = bytes(h, 'ascii')
  468.                     except Exception:
  469.                         header_kw['header'] = None
  470.  
  471.                 try:
  472.                     # pass bytes for command and _bytes to avoid string/bytes concat issues
  473.                     cmd = OBDCommand(f"DID_{dh}", f"DID_{dh}", bytes.fromhex(raw), bytes.fromhex(raw), decode_raw_once, **header_kw)
  474.                 except TypeError:
  475.                     # fallback signature without header
  476.                     cmd = OBDCommand(f"DID_{dh}", f"DID_{dh}", bytes.fromhex(raw), bytes.fromhex(raw), decode_raw_once)
  477.  
  478.                 try:
  479.                     r = con.query(cmd)
  480.                     if r is not None and not r.is_null():
  481.                         print(f"DID {dh} (header={h or 'default'}): {r.value}")
  482.                         discovered.append((dh, r.value))
  483.                         break
  484.                     else:
  485.                         # no data; try next header
  486.                         pass
  487.                 except Exception as e:
  488.                     # show error and try next header
  489.                     print(f"DID {dh} probe error (header={h}): {e}")
  490.                 time.sleep(max(0.0, args.did_delay))
  491.  
  492.         if args.did_out:
  493.             try:
  494.                 with open(args.did_out, 'w') as f:
  495.                     for dh, val in discovered:
  496.                         f.write(f"{dh},{val}\n")
  497.                 print(f"Wrote {len(discovered)} discovered DIDs to {args.did_out}")
  498.             except Exception as e:
  499.                 print("Failed to write DID output:", e)
  500.         print("[DID-DISCOVER] Finished")
  501.  
  502.     # Optionally query common PIDs
  503.     if args.live:
  504.         print("\n[MODE 01] Querying common PIDs...")
  505.         pid_cmds = [
  506.             ("RPM", getattr(cmds, 'RPM', None)),
  507.             ("SPEED", getattr(cmds, 'SPEED', None)),
  508.             ("COOLANT_TEMP", getattr(cmds, 'COOLANT_TEMP', None)),
  509.             ("THROTTLE_POS", getattr(cmds, 'THROTTLE_POS', None)),
  510.             ("ENGINE_LOAD", getattr(cmds, 'ENGINE_LOAD', None)),
  511.             ("FUEL_LEVEL", getattr(cmds, 'FUEL_LEVEL', None)),
  512.         ]
  513.         for name, c in pid_cmds:
  514.             if c is None:
  515.                 print(f"[SKIP] {name} not available in obd.commands")
  516.                 continue
  517.  
  518.             # if connection dropped, try to reconnect
  519.             if not con.is_connected():
  520.                 print(f"[WARN] Connection lost before querying {name}; attempting reconnect...")
  521.                 try:
  522.                     con = obd.OBD(port, fast=False, timeout=timeout, protocol=protocol)
  523.                 except Exception:
  524.                     try:
  525.                         con = obd.OBD(port, fast=False, timeout=timeout, protocol=protocol)
  526.                     except Exception as e:
  527.                         print(f"[ERROR] reconnect failed: {e}")
  528.                         break
  529.  
  530.             try:
  531.                 r = con.query(c)
  532.                 if r is None or r.is_null():
  533.                     print(f"{name}: NO DATA")
  534.                 else:
  535.                     print(f"{name}: {r.value}")
  536.             except Exception as e:
  537.                 print(f"{name}: query failed: {e}")
  538.  
  539.     # Close connection
  540.     try:
  541.         con.close()
  542.     except Exception:
  543.         pass
  544.     print("\n[COMPLETE] OBD scan finished.")
  545.  
  546.  
  547. if __name__ == "__main__":
  548.     main()
  549.  
Advertisement
Add Comment
Please, Sign In to add comment