import tkinter as tk from tkinter import ttk, filedialog import modbus_tk import modbus_tk.defines as cst from modbus_tk import modbus_tcp import time import threading import sys def validate_and_write(): if not is_connected: print_log("Not connected to Modbus server.") return try: pattern_idx = PATTERN_OPTIONS.index(pattern_var.get()) engine_speed = int(engine_speed_var.get()) knock_amp = float(knock_amp_var.get()) knock_freq = float(knock_freq_var.get()) alpha = float(alpha_var.get()) delta1 = float(delta1_var.get()) delta2 = float(delta2_var.get()) vibration_amp = float(vibration_amp_var.get()) vibration_freq = float(vibration_freq_var.get()) mb_client.execute(0, cst.WRITE_MULTIPLE_REGISTERS, 100, output_value=[pattern_idx,engine_speed,knock_amp,knock_freq,alpha,delta1,delta2,vibration_amp,vibration_freq]) print_log("Successfully wrote to Modbus registers.") except ValueError as ve: print_log(f"Input error: All fields must be numeric. {str(ve)}") except Exception as e: print_log(f"Modbus write error: {str(e)}") PATTERN_OPTIONS = [ "Dizzy 4-cylinder (2 evenly spaced teeth)", # DIZZY_FOUR_CYLINDER "Dizzy 6-cylinder (3 evenly spaced teeth)", # DIZZY_SIX_CYLINDER "Dizzy 8-cylinder (4 evenly spaced teeth)", # DIZZY_EIGHT_CYLINDER "60-2 crank only", # SIXTY_MINUS_TWO "60-2 with cam", # SIXTY_MINUS_TWO_WITH_CAM "60-2 with halfmoon cam", # SIXTY_MINUS_TWO_WITH_HALFMOON_CAM "36-1 crank only", # THIRTY_SIX_MINUS_ONE "24-1 crank only", # TWENTY_FOUR_MINUS_ONE "4-1 crank with cam", # FOUR_MINUS_ONE_WITH_CAM "8-1 crank only", # EIGHT_MINUS_ONE "6-1 crank with cam", # SIX_MINUS_ONE_WITH_CAM "12-1 crank with cam", # TWELVE_MINUS_ONE_WITH_CAM "Ford V-10 40-1 crank only", # FOURTY_MINUS_ONE "Dizzy 4-cylinder trigger return (40deg on, 50deg off)", # DIZZY_FOUR_TRIGGER_RETURN "Oddfire VR (V-twin)", # ODDFIRE_VR "Optispark LT1 (360 and 8)", # OPTISPARK_LT1 "12-3 crank", # TWELVE_MINUS_THREE "36-2-2-2 crank only (H4)", # THIRTY_SIX_MINUS_TWO_TWO_TWO "36-2-2-2 crank only (H6)", # THIRTY_SIX_MINUS_TWO_TWO_TWO_H6 "36-2-2-2 with cam", # THIRTY_SIX_MINUS_TWO_TWO_TWO_WITH_CAM "4200 wheel", # FOURTY_TWO_HUNDRED_WHEEL "Mazda FE3 36-1 with cam", # THIRTY_SIX_MINUS_ONE_WITH_CAM_FE3 "Mitsubishi 6G72 with cam", # SIX_G_SEVENTY_TWO_WITH_CAM "Buell oddfire cam (45 deg)", # BUELL_ODDFIRE_CAM "GM LS1 crank and cam (24 tooth)", # GM_LS1_CRANK_AND_CAM "GM 58x LS crank with 4x cam", # GM_58x_LS_CRANK_4X_CAM "Lotus 36-1-1-1-1 crank", # LOTUS_THIRTY_SIX_MINUS_ONE_ONE_ONE_ONE "Honda RC51 with cam (90 deg V-twin)", # HONDA_RC51_WITH_CAM "36-1 with second trigger (JimStim)", # THIRTY_SIX_MINUS_ONE_WITH_SECOND_TRIGGER "Chrysler NGC 36+2-2 with NGC 4-cylinder cam", # CHRYSLER_NGC_THIRTY_SIX_PLUS_TWO_MINUS_TWO_WITH_NGC4_CAM "Chrysler NGC 36-2+2 with NGC 6-cylinder cam", # CHRYSLER_NGC_THIRTY_SIX_MINUS_TWO_PLUS_TWO_WITH_NGC6_CAM "Chrysler NGC 36-2+2 with NGC 8-cylinder cam", # CHRYSLER_NGC_THIRTY_SIX_MINUS_TWO_PLUS_TWO_WITH_NGC8_CAM "Weber IAW with cam (JimStim)", # WEBER_IAW_WITH_CAM "Fiat 1.8 16V crank and cam", # FIAT_ONE_POINT_EIGHT_SIXTEEN_VALVE_WITH_CAM "Nissan 360 tooth CAS with 6 slots (JimStim)", # THREE_SIXTY_NISSAN_CAS "Mazda CAS 24-2 with single pulse outer ring", # TWENTY_FOUR_MINUS_TWO_WITH_SECOND_TRIGGER "Yamaha 2002-03 R1 8 even-tooth crank with 1 tooth cam", # YAMAHA_EIGHT_TOOTH_WITH_CAM "GM 4 even-tooth crank with 1 tooth cam", # GM_FOUR_TOOTH_WITH_CAM "GM 6 even-tooth crank with 1 tooth cam", # GM_SIX_TOOTH_WITH_CAM "GM 8 even-tooth crank with 1 tooth cam", # GM_EIGHT_TOOTH_WITH_CAM "Volvo d12[acd] crank with 7 tooth cam", # VOLVO_D12ACD_WITH_CAM "Mazda 36-2-2-2 with 6 tooth cam", # MAZDA_THIRTY_SIX_MINUS_TWO_TWO_TWO_WITH_SIX_TOOTH_CAM "Mitsubishi 4G63 4/2 crank and cam", # MITSUBISHI_4G63_4_2 "Audi 135 tooth crank and cam", # AUDI_135_WITH_CAM (Corrected "1351" to "135") "Honda D17 Crank (12+1)", # HONDA_D17_NO_CAM "Mazda 323 AU version", # MAZDA_323_AU "Daihatsu 3SI distributor (3 cylinders)", # DAIHATSU_3CYL "Miata 99-05", # MIATA_9905 "12/1 (12 crank with cam)", # TWELVE_WITH_CAM "24/1 (24 crank with cam)", # TWENTY_FOUR_WITH_CAM "Subaru 6 crank, 7 cam", # SUBARU_SIX_SEVEN "GM 7X (6 even teeth with 1 uneven tooth)", # GM_7X "DSM 420A", # FOUR_TWENTY_A "Ford ST170", # FORD_ST170 "Mitsubishi 3A92 (3-cylinder)", # MITSUBISHI_3A92 "Toyota 4AGE CAS (4 teeth with 1 cam tooth)", # TOYOTA_4AGE_CAS "Toyota 4AGZE (24 teeth with 1 cam tooth)", # TOYOTA_4AGZE "Suzuki DRZ400 (6 coil teeth, 2 uneven crank teeth)", # SUZUKI_DRZ400 "Jeep 2000 (4.0 6-cylinder)", # JEEP2000 "BMW N20 (58x with custom cam)", # BMW_N20 "Dodge Viper 1996-2002", # VIPER_96_02 "36-2 with 1 tooth cam (2JZ-GTE VVTi crank pulley + non-VVTi cam)", # THIRTY_SIX_MINUS_TWO_WITH_ONE_CAM ] HOST_IP = "172.31.255.252" PORT = 502 mb_client = modbus_tcp.TcpMaster(host=HOST_IP, port=PORT) mb_client.set_timeout(2.0) is_connected = False root = tk.Tk() root.title("Crank, Cam, Knock, Vibration Config") root.geometry("900x600") sc_wdt = root.winfo_screenwidth() sc_ht = root.winfo_screenheight() win_wdh, win_ht = 900, 600 x_pos = (sc_wdt - win_wdh) // 2 y_pos = (sc_ht - win_ht) // 2 root.geometry(f"{win_wdh}x{win_ht}+{x_pos}+{y_pos}") root.resizable(False, False) status_frame = ttk.Frame(root) status_frame.pack(side="bottom", fill="x") status_txt = tk.Text(status_frame, height=5, wrap="word", state="disabled") status_txt.pack(side="left", fill="x", expand=True, padx=5, pady=5) status_scrollbar = ttk.Scrollbar(status_frame, orient="vertical", command=status_txt.yview) status_scrollbar.pack(side="right", fill="y") status_txt.configure(yscrollcommand=status_scrollbar.set) clear_bt = ttk.Button(status_frame, text="Clear Logs", command=lambda: clear_logs()) clear_bt.pack(side="right", padx=5, pady=5) original_stdout = sys.stdout def clear_logs(): status_txt.configure(state="normal") status_txt.delete(1.0, "end") status_txt.configure(state="disabled") def print_log(*args, **kwargs): timestamp = time.strftime("%H:%M:%S") message = f"[{timestamp}] " + " ".join(map(str, args)) + "\n" original_stdout.write(message) original_stdout.flush() status_txt.configure(state="normal") status_txt.insert("end", message) status_txt.see("end") status_txt.configure(state="disabled") def connect_modbus(): global is_connected def try_connect(): global is_connected try: mb_client.open() is_connected = True connect_bt.config(text="Connected", state="disabled") print_log("Connected to Modbus server.") except Exception as e: is_connected = False connect_bt.config(text="Connect (Failed - Retry)", state="normal") print_log(f"Connection failed: {str(e)}") connect_bt.config(text="Connecting...", state="disabled") threading.Thread(target=try_connect, daemon=True).start() canvas = tk.Canvas(root) canvas.pack(side="left", fill="both", expand=True) v_scrollbar = ttk.Scrollbar(root, orient="vertical", command=canvas.yview) v_scrollbar.pack(side="right", fill="y") canvas.configure(yscrollcommand=v_scrollbar.set) main_frame = ttk.Frame(canvas) canvas.create_window((0, 0), window=main_frame, anchor="nw") def on_frame_configure(event): canvas.configure(scrollregion=canvas.bbox("all")) main_frame.bind("", on_frame_configure) def on_mousewheel(event): if event.delta: canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") canvas.bind_all("", on_mousewheel) pattern_var = tk.StringVar(value=PATTERN_OPTIONS[0]) engine_speed_var = tk.StringVar(value="7000") knock_amp_var = tk.StringVar(value="10") knock_freq_var = tk.StringVar(value="10") alpha_var = tk.StringVar(value="80") delta1_var = tk.StringVar(value="8") delta2_var = tk.StringVar(value="105") vibration_amp_var = tk.StringVar(value="0.1") vibration_freq_var = tk.StringVar(value="500") HOST_IP = "192.168.20.22" def validate_and_write(): if not is_connected: print_log("Not connected to Modbus server.") return try: pattern_idx = PATTERN_OPTIONS.index(pattern_var.get()) engine_speed = int(engine_speed_var.get()) knock_amp = int(float(knock_amp_var.get()) / 400 * 1000) knock_freq = int(float(knock_freq_var.get()) / 5 * 1000) alpha = int(float(alpha_var.get()) * 16) delta1 = int(float(delta1_var.get())) delta2 = int(float(delta2_var.get())) vibration_amp = int(float(vibration_amp_var.get()) * 10) vibration_freq = int(float(vibration_freq_var.get())) mb_client.execute(1, cst.WRITE_MULTIPLE_REGISTERS, 0, output_value=[ pattern_idx, engine_speed, knock_amp, knock_freq, alpha, delta1, delta2, vibration_amp, vibration_freq ]) print_log("Successfully wrote to Modbus registers.") except ValueError as ve: print_log(f"Input error: All fields must be numeric. {str(ve)}") except Exception as e: print_log(f"Modbus write error: {str(e)}") def load_xls_file(): file_path = filedialog.askopenfilename(filetypes=[("Excel files", "*.xls *.xlsx")]) if file_path: print_log(f"Selected file: {file_path}") # TODO main_frame.grid_columnconfigure(0, weight=1) main_frame.grid_columnconfigure(1, weight=0) main_frame.grid_columnconfigure(2, weight=1) connect_bt = ttk.Button(main_frame, text="Connect to Modbus", command=connect_modbus) connect_bt.grid(row=0, column=2, padx=10, pady=10, sticky="ew") crank_frame = ttk.LabelFrame(main_frame, text="Crank, Cam", padding=10) crank_frame.grid(row=1, column=0, columnspan=5, padx=10, pady=5, sticky="ew") ttk.Label(crank_frame, text="Crank Cam Pattern").grid(row=0, column=0, padx=5, pady=5, sticky="w") pattern_menu = ttk.Combobox(crank_frame, textvariable=pattern_var, values=PATTERN_OPTIONS, width=50) pattern_menu.grid(row=0, column=1, padx=5, pady=5) ttk.Button(crank_frame, text="*New Pattern from .xls file", command=load_xls_file).grid(row=1, column=0, padx=5, pady=5, sticky="w") ttk.Label(crank_frame, text="Engine Speed (RPM)").grid(row=0, column=2, padx=5, pady=5) ttk.Entry(crank_frame, textvariable=engine_speed_var, width=15).grid(row=0, column=3, padx=5, pady=5) knock_frame = ttk.LabelFrame(main_frame, text="Knock", padding=10) knock_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky="ew") ttk.Label(knock_frame, text="Amplitude (V)").grid(row=0, column=0, padx=5, pady=5, sticky="w") ttk.Entry(knock_frame, textvariable=knock_amp_var).grid(row=0, column=1, padx=5, pady=5) ttk.Label(knock_frame, text="f (kHz)").grid(row=1, column=0, padx=5, pady=5, sticky="w") ttk.Entry(knock_frame, textvariable=knock_freq_var).grid(row=1, column=1, padx=5, pady=5) ttk.Label(knock_frame, text="α (º)").grid(row=2, column=0, padx=5, pady=5, sticky="w") ttk.Entry(knock_frame, textvariable=alpha_var).grid(row=2, column=1, padx=5, pady=5) ttk.Label(knock_frame, text="Δ₁ (º)").grid(row=3, column=0, padx=5, pady=5, sticky="w") ttk.Entry(knock_frame, textvariable=delta1_var).grid(row=3, column=1, padx=5, pady=5) ttk.Label(knock_frame, text="Δ₂ (º)").grid(row=4, column=0, padx=5, pady=5, sticky="w") ttk.Entry(knock_frame, textvariable=delta2_var).grid(row=4, column=1, padx=5, pady=5) vibration_frame = ttk.LabelFrame(main_frame, text="Engine Base Vibration", padding=10) vibration_frame.grid(row=2, column=3, columnspan=2, padx=10, pady=5, sticky="ew") ttk.Label(vibration_frame, text="Amplitude (V)").grid(row=0, column=0, padx=5, pady=5, sticky="w") ttk.Entry(vibration_frame, textvariable=vibration_amp_var).grid(row=0, column=1, padx=5, pady=5) ttk.Label(vibration_frame, text="f (Hz)").grid(row=1, column=0, padx=5, pady=5, sticky="w") ttk.Entry(vibration_frame, textvariable=vibration_freq_var).grid(row=1, column=1, padx=5, pady=5) ttk.Button(main_frame, text="Write to Modbus", command=validate_and_write).grid(row=3, column=0, columnspan=5, pady=20) root.mainloop()