kwp1281.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. # kwp1281.py
  2. import time
  3. import logging
  4. from kline import KLineTransport, KLineException
  5. logger = logging.getLogger("KWP1281")
  6. class KWP1281Exception(Exception):
  7. pass
  8. class KWP1281:
  9. def __init__(self, port: str):
  10. self.transport = KLineTransport(port)
  11. self.block_counter = 0
  12. self.connected = False
  13. def connect(self, address: int = 0x08) -> dict:
  14. """Connect to ECU and read identification."""
  15. try:
  16. self.transport.slow_init(address)
  17. self.connected = True
  18. # The ECU will now send identification blocks.
  19. # We read them until we get a block that doesn't say "more info follows"
  20. ecu_info = {
  21. "part_number": "",
  22. "coding": "",
  23. "workshop_code": "",
  24. "extra": []
  25. }
  26. # Usually ECUs send 0xF6 (ASCII string) blocks
  27. while True:
  28. length, counter, title, data = self.receive_block()
  29. if title == 0xF6:
  30. text = "".join(chr(c) for c in data if 32 <= c <= 126)
  31. if not ecu_info["part_number"]:
  32. ecu_info["part_number"] = text
  33. else:
  34. ecu_info["extra"].append(text)
  35. # Check if this is the last block of the connection sequence
  36. if title == 0x09: # ACK, shouldn't really happen here but just in case
  37. break
  38. # To get next block, we must send an ACK (0x09)
  39. self.send_ack()
  40. # In real KWP1281, 0x03 as title means end of communication, but we are just looking for the end of the ID phase.
  41. # Actually, some ECUs just send all info, then wait for command.
  42. # A common pattern is: ECU sends 0xF6 (info), Tester sends 0x09 (ACK), ECU sends next 0xF6, Tester sends 0x09...
  43. # Finally ECU sends 0x09 to say "I'm ready for commands". Let's assume we break when ECU sends 0x09.
  44. if title == 0x09:
  45. break
  46. return ecu_info
  47. except KLineException as e:
  48. self.connected = False
  49. self.transport.close()
  50. raise KWP1281Exception(f"Connection failed: {e}")
  51. def disconnect(self):
  52. """End communication gracefully."""
  53. if self.connected:
  54. try:
  55. self.send_block(0x06, []) # 0x06 is often End Communication
  56. except Exception:
  57. pass
  58. self.connected = False
  59. self.transport.close()
  60. def receive_block(self):
  61. """Receives a KWP1281 block."""
  62. try:
  63. length = self.transport.read_byte()
  64. if length == 0:
  65. raise KWP1281Exception("Received block length 0")
  66. counter = self.transport.read_byte()
  67. self.block_counter = (counter + 1) % 256
  68. title = self.transport.read_byte()
  69. data = []
  70. for _ in range(length - 3):
  71. data.append(self.transport.read_byte())
  72. end_byte = self.transport.read_byte(complement=False) # 0x03 has no complement
  73. if end_byte != 0x03:
  74. logger.warning(f"Block did not end with 0x03, got 0x{end_byte:02X}")
  75. logger.debug(f"Received block: len={length}, ctr={counter}, title=0x{title:02X}, data={[hex(d) for d in data]}")
  76. return length, counter, title, data
  77. except KLineException as e:
  78. self.connected = False
  79. raise KWP1281Exception(f"Failed to receive block: {e}")
  80. def send_block(self, title: int, data: list):
  81. """Sends a KWP1281 block."""
  82. length = len(data) + 3
  83. logger.debug(f"Sending block: len={length}, ctr={self.block_counter}, title=0x{title:02X}, data={[hex(d) for d in data]}")
  84. try:
  85. self.transport.write_byte(length)
  86. self.transport.write_byte(self.block_counter)
  87. self.transport.write_byte(title)
  88. for d in data:
  89. self.transport.write_byte(d)
  90. self.transport.write_byte(0x03, verify_complement=False) # End byte has no complement
  91. self.block_counter = (self.block_counter + 1) % 256
  92. except KLineException as e:
  93. self.connected = False
  94. raise KWP1281Exception(f"Failed to send block: {e}")
  95. def send_ack(self):
  96. """Sends an ACK block."""
  97. self.send_block(0x09, [])
  98. def read_dtcs(self) -> list:
  99. """Reads Diagnostic Trouble Codes."""
  100. if not self.connected:
  101. raise KWP1281Exception("Not connected")
  102. self.send_block(0x07, [])
  103. dtcs_raw = []
  104. while True:
  105. length, counter, title, data = self.receive_block()
  106. if title == 0x09:
  107. break
  108. if title == 0xFC: # DTC response
  109. # Data comes in triplets usually: Code_High, Code_Low, Elaboration/Status
  110. for i in range(0, len(data), 3):
  111. if i + 2 < len(data):
  112. hi = data[i]
  113. lo = data[i+1]
  114. elab = data[i+2]
  115. if hi == 0xFF and lo == 0xFF:
  116. # 0xFFFF means end of faults
  117. pass
  118. else:
  119. dtcs_raw.append({"hi": hi, "lo": lo, "elaboration": elab})
  120. self.send_ack()
  121. return dtcs_raw
  122. def clear_dtcs(self):
  123. """Clears stored DTCs."""
  124. if not self.connected:
  125. raise KWP1281Exception("Not connected")
  126. self.send_block(0x05, [])
  127. while True:
  128. length, counter, title, data = self.receive_block()
  129. if title == 0x09:
  130. break
  131. self.send_ack()
  132. def read_meas_block(self, group: int) -> list:
  133. """Reads a specific measurement group."""
  134. if not self.connected:
  135. raise KWP1281Exception("Not connected")
  136. self.send_block(0x29, [group])
  137. raw_measurements = []
  138. while True:
  139. length, counter, title, data = self.receive_block()
  140. if title == 0x09:
  141. break
  142. if title == 0xE7:
  143. # Data comes in triplets: Formula, A, B
  144. for i in range(0, len(data), 3):
  145. if i + 2 < len(data):
  146. raw_measurements.append({
  147. "formula": data[i],
  148. "a": data[i+1],
  149. "b": data[i+2]
  150. })
  151. self.send_ack()
  152. return raw_measurements