app.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. # app.py
  2. from flask import Flask, render_template, jsonify, request, Response
  3. import threading
  4. import time
  5. import json
  6. import logging
  7. from port_scanner import scan_ports, auto_select
  8. from kwp1281 import KWP1281, KWP1281Exception
  9. from dtc_db import decode_dtc
  10. from meas_decoder import decode_measurement
  11. from ecu_defs import HVAC_GROUPS, HVAC_ADDRESS
  12. logging.basicConfig(level=logging.INFO)
  13. logger = logging.getLogger("WebAPI")
  14. app = Flask(__name__)
  15. # Global state for KWP1281 connection
  16. protocol = None
  17. connection_lock = threading.Lock()
  18. is_connected = False
  19. ecu_info = {}
  20. live_mode = False
  21. live_group = 1
  22. def keep_alive_thread():
  23. global is_connected, protocol, live_mode
  24. while True:
  25. time.sleep(0.5)
  26. with connection_lock:
  27. if is_connected and protocol and not live_mode:
  28. try:
  29. # Send ACK periodically to keep connection alive if we aren't doing live polling
  30. protocol.send_ack()
  31. # Wait for ECU to respond (usually with an ACK)
  32. protocol.receive_block()
  33. except Exception as e:
  34. logger.error(f"Keep-alive failed: {e}")
  35. is_connected = False
  36. threading.Thread(target=keep_alive_thread, daemon=True).start()
  37. @app.route('/')
  38. def index():
  39. return render_template('index.html')
  40. @app.route('/api/ports')
  41. def get_ports():
  42. ports = scan_ports()
  43. return jsonify({
  44. "ports": ports,
  45. "auto": auto_select()
  46. })
  47. @app.route('/api/connect', methods=['POST'])
  48. def connect():
  49. global protocol, is_connected, ecu_info
  50. data = request.json
  51. port = data.get('port')
  52. if not port:
  53. return jsonify({"success": False, "error": "No port provided"}), 400
  54. with connection_lock:
  55. if is_connected and protocol:
  56. try:
  57. protocol.disconnect()
  58. except:
  59. pass
  60. protocol = KWP1281(port)
  61. try:
  62. ecu_info = protocol.connect(HVAC_ADDRESS)
  63. is_connected = True
  64. return jsonify({
  65. "success": True,
  66. "ecu_info": ecu_info
  67. })
  68. except KWP1281Exception as e:
  69. is_connected = False
  70. return jsonify({"success": False, "error": str(e)}), 500
  71. @app.route('/api/disconnect', methods=['POST'])
  72. def disconnect():
  73. global protocol, is_connected
  74. with connection_lock:
  75. if protocol and is_connected:
  76. try:
  77. protocol.disconnect()
  78. except:
  79. pass
  80. is_connected = False
  81. return jsonify({"success": True})
  82. @app.route('/api/status')
  83. def status():
  84. return jsonify({
  85. "connected": is_connected,
  86. "ecu_info": ecu_info if is_connected else None
  87. })
  88. @app.route('/api/dtcs')
  89. def get_dtcs():
  90. global protocol, is_connected
  91. with connection_lock:
  92. if not is_connected or not protocol:
  93. return jsonify({"success": False, "error": "Not connected"}), 400
  94. try:
  95. raw_dtcs = protocol.read_dtcs()
  96. decoded = [decode_dtc(d['hi'], d['lo'], d['elaboration']) for d in raw_dtcs]
  97. return jsonify({
  98. "success": True,
  99. "dtcs": decoded
  100. })
  101. except Exception as e:
  102. return jsonify({"success": False, "error": str(e)}), 500
  103. @app.route('/api/dtcs/clear', methods=['POST'])
  104. def clear_dtcs():
  105. global protocol, is_connected
  106. with connection_lock:
  107. if not is_connected or not protocol:
  108. return jsonify({"success": False, "error": "Not connected"}), 400
  109. try:
  110. protocol.clear_dtcs()
  111. return jsonify({"success": True})
  112. except Exception as e:
  113. return jsonify({"success": False, "error": str(e)}), 500
  114. @app.route('/api/meas/<int:group>')
  115. def get_meas(group):
  116. global protocol, is_connected
  117. if group < 1 or group > 255:
  118. return jsonify({"success": False, "error": "Invalid group"}), 400
  119. with connection_lock:
  120. if not is_connected or not protocol:
  121. return jsonify({"success": False, "error": "Not connected"}), 400
  122. try:
  123. raw_meas = protocol.read_meas_block(group)
  124. decoded = [decode_measurement(m['formula'], m['a'], m['b']) for m in raw_meas]
  125. # Add labels if available
  126. group_info = HVAC_GROUPS.get(group, {"name": f"Group {group}", "fields": []})
  127. for i, meas in enumerate(decoded):
  128. if i < len(group_info["fields"]):
  129. meas["label"] = group_info["fields"][i]
  130. else:
  131. meas["label"] = f"Field {i+1}"
  132. return jsonify({
  133. "success": True,
  134. "group": group,
  135. "name": group_info["name"],
  136. "measurements": decoded
  137. })
  138. except Exception as e:
  139. return jsonify({"success": False, "error": str(e)}), 500
  140. @app.route('/api/meas/live/<int:group>')
  141. def live_meas(group):
  142. global protocol, is_connected, live_mode, live_group
  143. live_mode = True
  144. live_group = group
  145. def generate():
  146. global is_connected, protocol, live_mode
  147. try:
  148. while live_mode and is_connected:
  149. with connection_lock:
  150. if not is_connected or not protocol:
  151. break
  152. try:
  153. raw_meas = protocol.read_meas_block(group)
  154. decoded = [decode_measurement(m['formula'], m['a'], m['b']) for m in raw_meas]
  155. group_info = HVAC_GROUPS.get(group, {"name": f"Group {group}", "fields": []})
  156. for i, meas in enumerate(decoded):
  157. if i < len(group_info["fields"]):
  158. meas["label"] = group_info["fields"][i]
  159. else:
  160. meas["label"] = f"Field {i+1}"
  161. data = {
  162. "group": group,
  163. "measurements": decoded
  164. }
  165. yield f"data: {json.dumps(data)}\n\n"
  166. except Exception as e:
  167. logger.error(f"Live polling error: {e}")
  168. is_connected = False
  169. break
  170. time.sleep(0.5) # Poll interval
  171. finally:
  172. live_mode = False
  173. return Response(generate(), mimetype='text/event-stream')
  174. @app.route('/api/meas/live/stop', methods=['POST'])
  175. def stop_live():
  176. global live_mode
  177. live_mode = False
  178. return jsonify({"success": True})
  179. if __name__ == '__main__':
  180. app.run(host='127.0.0.1', port=5000, debug=True, threaded=True)