kline.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. # kline.py
  2. import serial
  3. import time
  4. import struct
  5. import logging
  6. logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  7. logger = logging.getLogger("KLine")
  8. class KLineException(Exception):
  9. pass
  10. class KLineTransport:
  11. def __init__(self, port: str):
  12. self.port_name = port
  13. self.ser = None
  14. self.BAUD_RATE = 4800
  15. self.INTER_BYTE_DELAY = 0.002 # 2ms
  16. def _open_serial(self, baudrate=9600):
  17. if self.ser and self.ser.is_open:
  18. self.ser.close()
  19. try:
  20. self.ser = serial.Serial(
  21. port=self.port_name,
  22. baudrate=baudrate,
  23. bytesize=serial.EIGHTBITS,
  24. parity=serial.PARITY_NONE,
  25. stopbits=serial.STOPBITS_ONE,
  26. timeout=2.0
  27. )
  28. # Ensure lines are idle high
  29. self.ser.rts = False
  30. self.ser.dtr = False
  31. self.ser.break_condition = False
  32. except Exception as e:
  33. raise KLineException(f"Failed to open port {self.port_name}: {e}")
  34. def _precise_sleep(self, duration: float):
  35. """Busy-wait for precise timing, as time.sleep() can be too inaccurate for bit-banging."""
  36. end_time = time.perf_counter() + duration
  37. while time.perf_counter() < end_time:
  38. pass
  39. def _set_lines_low(self, is_low: bool):
  40. """Sets K and L lines low (dominant) or high (recessive)."""
  41. self.ser.break_condition = is_low
  42. self.ser.rts = is_low
  43. self.ser.dtr = is_low
  44. def slow_init(self, address: int = 0x08) -> bool:
  45. """Performs the 5-baud initialization sequence on K and L lines."""
  46. logger.info(f"Starting slow init for address 0x{address:02X} at {self.BAUD_RATE} baud")
  47. # We need to bit-bang the 5 baud signal
  48. # Each bit is 200ms
  49. self._open_serial(baudrate=self.BAUD_RATE)
  50. # Idle high
  51. self._set_lines_low(False)
  52. time.sleep(1.0)
  53. logger.debug("Bit-banging address...")
  54. # Start bit (low)
  55. self._set_lines_low(True)
  56. self._precise_sleep(0.2)
  57. # 8 data bits (LSB first)
  58. for i in range(8):
  59. bit = (address >> i) & 1
  60. if bit == 0:
  61. self._set_lines_low(True)
  62. else:
  63. self._set_lines_low(False)
  64. self._precise_sleep(0.2)
  65. # Stop bit (high)
  66. self._set_lines_low(False)
  67. self._precise_sleep(0.2)
  68. logger.debug("Address sent. Waiting for sync byte...")
  69. # Clear out any garbage/echo bytes that arrived in our RX buffer while we were bit-banging TX
  70. self.ser.reset_input_buffer()
  71. # Now wait for response at normal baud rate
  72. # ECU should respond with 0x55 (Sync), then two keyword bytes (0x01, 0x8A)
  73. self.ser.timeout = 0.5
  74. sync_byte = None
  75. # Read up to 10 times to clear any lingering line noise and find the 0x55
  76. for _ in range(10):
  77. b = self.ser.read(1)
  78. if not b:
  79. continue
  80. if b[0] == 0x55:
  81. sync_byte = b
  82. break
  83. else:
  84. logger.debug(f"Ignoring unexpected byte before sync: {b.hex()}")
  85. if not sync_byte:
  86. raise KLineException("No response to 5-baud init (timeout).")
  87. logger.debug("Received sync byte 0x55")
  88. kw1 = self.ser.read(1)
  89. kw2 = self.ser.read(1)
  90. if len(kw1) == 0 or len(kw2) == 0:
  91. raise KLineException("Failed to read keyword bytes.")
  92. logger.debug(f"Received keywords: 0x{kw1[0]:02X}, 0x{kw2[0]:02X}")
  93. # Send complement of kw2
  94. kw2_comp = (~kw2[0]) & 0xFF
  95. logger.debug(f"Sending complement of KW2: 0x{kw2_comp:02X}")
  96. # Give ECU a little time
  97. time.sleep(0.025)
  98. self.ser.write(bytes([kw2_comp]))
  99. # Read our own echo from the K-Line
  100. echo = self.ser.read(1)
  101. if not echo or echo[0] != kw2_comp:
  102. logger.warning("Did not receive correct echo for KW2 complement.")
  103. # Read back ECU's complement of the address
  104. addr_comp = self.ser.read(1)
  105. expected_addr_comp = (~address) & 0xFF
  106. if not addr_comp:
  107. # Some ECUs might not send this, but we'll log it
  108. logger.warning("Did not receive address complement from ECU.")
  109. elif addr_comp[0] != expected_addr_comp:
  110. logger.warning(f"Expected address comp 0x{expected_addr_comp:02X}, got 0x{addr_comp[0]:02X}")
  111. logger.info("Init sequence complete.")
  112. return True
  113. def read_byte(self, complement: bool = True) -> int:
  114. """Reads a byte and optionally sends its complement back."""
  115. b = self.ser.read(1)
  116. if not b:
  117. raise KLineException("Timeout reading byte from K-Line.")
  118. val = b[0]
  119. if complement:
  120. comp = (~val) & 0xFF
  121. time.sleep(self.INTER_BYTE_DELAY)
  122. self.ser.write(bytes([comp]))
  123. # Consume our own echo
  124. echo = self.ser.read(1)
  125. if not echo or echo[0] != comp:
  126. raise KLineException(f"Echo mismatch in read_byte. Sent {comp:02X}, got {echo[0] if echo else 'None'}")
  127. return val
  128. def write_byte(self, val: int, verify_complement: bool = True):
  129. """Writes a byte and optionally reads back ECU's complement."""
  130. time.sleep(self.INTER_BYTE_DELAY)
  131. self.ser.write(bytes([val]))
  132. # Consume our own echo from the K-Line
  133. echo = self.ser.read(1)
  134. if not echo or echo[0] != val:
  135. raise KLineException(f"Echo mismatch in write_byte. Sent {val:02X}, got {echo[0] if echo else 'None'}")
  136. if verify_complement:
  137. b = self.ser.read(1)
  138. if not b:
  139. raise KLineException(f"Timeout waiting for complement of 0x{val:02X}")
  140. comp = b[0]
  141. expected = (~val) & 0xFF
  142. if comp != expected:
  143. logger.warning(f"Byte write complement mismatch. Sent 0x{val:02X}, expected 0x{expected:02X}, got 0x{comp:02X}")
  144. def close(self):
  145. if self.ser and self.ser.is_open:
  146. self.ser.close()