import tkinter as tk from tkinter import ttk, messagebox import serial import serial.tools.list_ports import threading import time import datetime import os import re class DualScaleRS232Reader: def __init__(self, root): self.root = root self.root.title("Scale RS232 Reader - Hiển thị 2 cân đồng thời") self.root.geometry("1600x900") # Tăng kích thước self.root.minsize(1400, 800) # Màu sắc self.colors = { 'white': '#FFFFFF', 'light_gray': '#E8E8E8', 'light_blue': '#90CAF9', 'light_green': '#A5D6A7', 'text_dark': '#2E2E2E', 'text_light': '#666666', 'border': '#CCCCCC' } self.root.configure(bg=self.colors['white']) # Thiết lập styles self.setup_styles() # Biến lưu trữ kết nối và dữ liệu self.serial_connections = {} self.running = {} self.data_widgets = {} # Tạo thư mục lịch sử self.history_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "history") os.makedirs(self.history_dir, exist_ok=True) # Tạo giao diện self.create_main_interface() # Cập nhật danh sách cổng COM self.update_com_ports() def setup_styles(self): """Thiết lập styles cho giao diện""" style = ttk.Style() style.theme_use('clam') # Frame styles style.configure("Main.TFrame", background=self.colors['white']) style.configure("Header.TFrame", background=self.colors['light_blue']) style.configure("Control.TFrame", background=self.colors['light_gray']) # Label styles style.configure("Title.TLabel", background=self.colors['light_blue'], foreground=self.colors['text_dark'], font=("Segoe UI", 18, "bold")) style.configure("Header.TLabel", background=self.colors['light_gray'], foreground=self.colors['text_dark'], font=("Segoe UI", 12, "bold")) style.configure("Info.TLabel", background=self.colors['white'], foreground=self.colors['text_light'], font=("Segoe UI", 11)) # Button styles style.configure("Action.TButton", font=("Segoe UI", 9, "bold"), padding=(8, 4)) style.configure("Connect.TButton", font=("Segoe UI", 10, "bold"), padding=(10, 6)) # Combobox style style.configure("Compact.TCombobox", font=("Segoe UI", 9)) def create_main_interface(self): """Tạo giao diện chính""" # Container chính main_container = ttk.Frame(self.root, style="Main.TFrame", padding="15") main_container.pack(fill=tk.BOTH, expand=True) # Header self.create_header(main_container) # Control panel compact self.create_control_panel(main_container) # Display area cho 2 cân (chiếm phần lớn màn hình) self.create_dual_display_area(main_container) # Status bar self.create_status_bar(main_container) def create_header(self, parent): """Tạo header""" header_frame = tk.Frame(parent, bg=self.colors['light_blue'], height=50) header_frame.pack(fill=tk.X, pady=(0, 10)) header_frame.pack_propagate(False) title_label = tk.Label(header_frame, text="Scale RS232 Reader - Hiển thị đồng thời 2 cân", font=("Segoe UI", 18, "bold"), fg=self.colors['text_dark'], bg=self.colors['light_blue']) title_label.pack(side=tk.LEFT, padx=15, pady=10) version_label = tk.Label(header_frame, text="v2.2 - Ultra Large Display", font=("Segoe UI", 11), fg=self.colors['text_light'], bg=self.colors['light_blue']) version_label.pack(side=tk.RIGHT, padx=15, pady=12) def create_control_panel(self, parent): """Tạo panel điều khiển compact""" control_frame = tk.Frame(parent, bg=self.colors['light_gray'], relief="solid", bd=1, height=130) # Đủ cao cho 2 cân control_frame.pack(fill=tk.X, pady=(0, 10)) control_frame.pack_propagate(False) # Cố định chiều cao # Main container với grid layout main_container = tk.Frame(control_frame, bg=self.colors['light_gray']) main_container.pack(fill=tk.BOTH, padx=10, pady=6) # Configure grid weights main_container.grid_columnconfigure(0, weight=3) # Controls area main_container.grid_columnconfigure(1, weight=1) # Actions area # Left side - Scale controls controls_frame = tk.Frame(main_container, bg=self.colors['light_gray']) controls_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 15)) # Title compact title_label = tk.Label(controls_frame, text="QUẢN LÝ KẾT NỐI", font=("Segoe UI", 10, "bold"), fg=self.colors['text_dark'], bg=self.colors['light_gray']) title_label.pack(pady=(0, 5)) # Controls container controls_container = tk.Frame(controls_frame, bg=self.colors['light_gray']) controls_container.pack(fill=tk.X) # Cân 1 self.create_scale_controls("Scale_1", "CÂN 1", controls_container, 0) # Cân 2 self.create_scale_controls("Scale_2", "CÂN 2", controls_container, 1) # Right side - Action buttons actions_frame = tk.Frame(main_container, bg=self.colors['light_gray']) actions_frame.grid(row=0, column=1, sticky="nsew") # Actions title actions_title = tk.Label(actions_frame, text="THAO TÁC", font=("Segoe UI", 10, "bold"), fg=self.colors['text_dark'], bg=self.colors['light_gray']) actions_title.pack(pady=(0, 5)) # Action buttons - vertical layout ttk.Button(actions_frame, text="Làm mới cổng COM", command=self.update_com_ports, style="Action.TButton").pack(fill=tk.X, pady=1) ttk.Button(actions_frame, text="Xem lịch sử", command=self.view_history, style="Action.TButton").pack(fill=tk.X, pady=1) ttk.Button(actions_frame, text="Ngắt tất cả", command=self.disconnect_all, style="Action.TButton").pack(fill=tk.X, pady=1) def create_scale_controls(self, scale_id, scale_name, parent, row): """Tạo controls compact cho một cân""" # Scale name name_label = tk.Label(parent, text=f"{scale_name}:", font=("Segoe UI", 10, "bold"), fg=self.colors['text_dark'], bg=self.colors['light_gray']) name_label.grid(row=row, column=0, sticky="w", padx=(0, 10), pady=3) # COM Port com_label = tk.Label(parent, text="COM:", font=("Segoe UI", 9), fg=self.colors['text_light'], bg=self.colors['light_gray']) com_label.grid(row=row, column=1, sticky="w", padx=(0, 5), pady=3) com_var = tk.StringVar() com_combo = ttk.Combobox(parent, textvariable=com_var, width=12, style="Compact.TCombobox", state="readonly") com_combo.grid(row=row, column=2, padx=(0, 10), pady=3) # Baudrate baud_label = tk.Label(parent, text="Baud:", font=("Segoe UI", 9), fg=self.colors['text_light'], bg=self.colors['light_gray']) baud_label.grid(row=row, column=3, sticky="w", padx=(0, 5), pady=3) baud_var = tk.StringVar(value="9600") baud_combo = ttk.Combobox(parent, textvariable=baud_var, values=["1200", "2400", "4800", "9600", "19200", "38400"], width=8, style="Compact.TCombobox", state="readonly") baud_combo.grid(row=row, column=4, padx=(0, 10), pady=3) # Connect button connect_btn = ttk.Button(parent, text="Kết nối", command=lambda: self.toggle_connection(scale_id), style="Connect.TButton") connect_btn.grid(row=row, column=5, pady=3) # Lưu controls if not hasattr(self, 'scale_controls'): self.scale_controls = {} self.scale_controls[scale_id] = { 'scale_name': scale_name, 'com_var': com_var, 'com_combo': com_combo, 'baud_var': baud_var, 'connect_btn': connect_btn } def create_dual_display_area(self, parent): """Tạo khu vực hiển thị 2 cân đồng thời - tối đa hóa""" display_frame = tk.Frame(parent, bg=self.colors['white']) display_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) # Configure grid weights display_frame.grid_columnconfigure(0, weight=1) display_frame.grid_columnconfigure(1, weight=1) display_frame.grid_rowconfigure(0, weight=1) # Cân 1 self.create_scale_display("Scale_1", "CÂN 1", display_frame, 0, 0) # Cân 2 self.create_scale_display("Scale_2", "CÂN 2", display_frame, 0, 1) def create_scale_display(self, scale_id, scale_name, parent, row, col): """Tạo màn hình hiển thị cho một cân - tối đa hóa số""" # Main container cho cân scale_frame = tk.Frame(parent, bg=self.colors['white'], relief="solid", bd=1, highlightbackground=self.colors['border']) scale_frame.grid(row=row, column=col, sticky="nsew", padx=3, pady=3) # Header compact header_frame = tk.Frame(scale_frame, bg=self.colors['light_green'], height=40) header_frame.pack(fill=tk.X) header_frame.pack_propagate(False) header_label = tk.Label(header_frame, text=scale_name, font=("Segoe UI", 14, "bold"), fg=self.colors['text_dark'], bg=self.colors['light_green']) header_label.pack(expand=True, pady=8) # Status compact status_frame = tk.Frame(scale_frame, bg=self.colors['white'], height=30) status_frame.pack(fill=tk.X) status_frame.pack_propagate(False) status_var = tk.StringVar(value="Chưa kết nối") status_label = tk.Label(status_frame, textvariable=status_var, font=("Segoe UI", 10), fg=self.colors['text_light'], bg=self.colors['white']) status_label.pack(pady=5) # Weight display - SIÊU LỚN (tối đa hóa) weight_frame = tk.Frame(scale_frame, bg=self.colors['white']) weight_frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=15) weight_var = tk.StringVar(value="0.000") weight_label = tk.Label(weight_frame, textvariable=weight_var, font=("Arial", 150, "bold"), # Font siêu lớn để nhìn từ xa fg=self.colors['text_dark'], bg=self.colors['white']) weight_label.pack(expand=True) # Unit unit_label = tk.Label(weight_frame, text="kg", font=("Segoe UI", 32, "bold"), # Tăng size đơn vị fg=self.colors['text_light'], bg=self.colors['white']) unit_label.pack() # Statistics compact stats_frame = tk.Frame(scale_frame, bg=self.colors['light_gray'], height=60) stats_frame.pack(fill=tk.X, padx=8, pady=(0, 8)) stats_frame.pack_propagate(False) # Stats grid stats_grid = tk.Frame(stats_frame, bg=self.colors['light_gray']) stats_grid.pack(expand=True, pady=8) stats_vars = {} stat_items = [ ("Đọc:", "count", 0, 0), ("Max:", "max", 0, 2), ("Min:", "min", 1, 0), ("Avg:", "avg", 1, 2) ] for label_text, key, row, col in stat_items: tk.Label(stats_grid, text=label_text, font=("Segoe UI", 8, "bold"), fg=self.colors['text_dark'], bg=self.colors['light_gray']).grid( row=row, column=col, sticky="w", padx=(0, 3), pady=1) stats_vars[key] = tk.StringVar(value="0") tk.Label(stats_grid, textvariable=stats_vars[key], font=("Segoe UI", 8), fg=self.colors['text_light'], bg=self.colors['light_gray']).grid( row=row, column=col+1, sticky="w", padx=(0, 10), pady=1) # Lưu widgets self.data_widgets[scale_id] = { 'status_var': status_var, 'weight_var': weight_var, 'weight_label': weight_label, 'stats_vars': stats_vars, 'values': [], 'scale_name': scale_name } def create_status_bar(self, parent): """Tạo status bar""" status_frame = tk.Frame(parent, bg=self.colors['light_gray'], height=30) status_frame.pack(fill=tk.X) status_frame.pack_propagate(False) self.status_var = tk.StringVar(value="Sẵn sàng - Chọn cổng COM và kết nối") self.status_label = tk.Label(status_frame, textvariable=self.status_var, font=("Segoe UI", 9), fg=self.colors['text_dark'], bg=self.colors['light_gray']) self.status_label.pack(side=tk.LEFT, padx=10, pady=6) # Time display self.time_var = tk.StringVar() self.time_label = tk.Label(status_frame, textvariable=self.time_var, font=("Segoe UI", 9), fg=self.colors['text_light'], bg=self.colors['light_gray']) self.time_label.pack(side=tk.RIGHT, padx=10, pady=6) self.update_time() def update_time(self): """Cập nhật thời gian""" current_time = datetime.datetime.now().strftime("%H:%M:%S - %d/%m/%Y") self.time_var.set(current_time) self.root.after(1000, self.update_time) def update_com_ports(self): """Cập nhật danh sách cổng COM""" ports = [port.device for port in serial.tools.list_ports.comports()] for scale_id, controls in self.scale_controls.items(): current_value = controls['com_var'].get() controls['com_combo']['values'] = ports if current_value in ports: controls['com_var'].set(current_value) elif ports: controls['com_combo'].current(0) self.status_var.set(f"Đã tìm thấy {len(ports)} cổng COM") def toggle_connection(self, scale_id): """Kết nối/ngắt kết nối cân""" controls = self.scale_controls[scale_id] port = controls['com_var'].get() if not port: messagebox.showerror("Lỗi", f"Vui lòng chọn cổng COM cho {controls['scale_name']}") return if scale_id in self.serial_connections: self.disconnect_scale(scale_id) else: self.connect_scale(scale_id) def connect_scale(self, scale_id): """Kết nối cân""" controls = self.scale_controls[scale_id] widgets = self.data_widgets[scale_id] port = controls['com_var'].get() try: controls['connect_btn'].config(text="Đang kết nối...", state="disabled") widgets['status_var'].set(f"Đang kết nối với {port}...") self.root.update() # Tạo kết nối serial baudrate = int(controls['baud_var'].get()) ser = serial.Serial(port, baudrate, timeout=1) self.serial_connections[scale_id] = ser # Tạo file lịch sử timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") history_file = os.path.join(self.history_dir, f"{scale_id}_{port.replace('/', '_')}_{timestamp}.csv") with open(history_file, 'w', encoding='utf-8') as f: f.write("Timestamp,Raw Data,Weight Value (kg)\n") widgets['history_file'] = history_file # Bắt đầu thread đọc dữ liệu self.running[scale_id] = True thread = threading.Thread(target=self.read_data, args=(scale_id,), daemon=True) thread.start() # Cập nhật UI controls['connect_btn'].config(text="Ngắt kết nối", state="normal") widgets['status_var'].set(f"Đã kết nối với {port}") self.status_var.set(f"{controls['scale_name']} đã kết nối với {port}") except Exception as e: messagebox.showerror("Lỗi kết nối", f"Không thể kết nối {controls['scale_name']} với {port}:\n{str(e)}") controls['connect_btn'].config(text="Kết nối", state="normal") widgets['status_var'].set("Lỗi kết nối") def disconnect_scale(self, scale_id): """Ngắt kết nối cân""" controls = self.scale_controls[scale_id] widgets = self.data_widgets[scale_id] # Dừng thread if scale_id in self.running: self.running[scale_id] = False time.sleep(0.3) # Đóng serial connection try: if scale_id in self.serial_connections: self.serial_connections[scale_id].close() del self.serial_connections[scale_id] except: pass # Reset UI controls['connect_btn'].config(text="Kết nối") widgets['status_var'].set("Chưa kết nối") widgets['weight_var'].set("0.000") widgets['weight_label'].config(fg=self.colors['text_dark']) # Reset stats for key in widgets['stats_vars']: widgets['stats_vars'][key].set("0") widgets['values'] = [] self.status_var.set(f"Đã ngắt kết nối {controls['scale_name']}") def read_data(self, scale_id): """Thread đọc dữ liệu từ cân""" ser = self.serial_connections[scale_id] while self.running.get(scale_id, False): try: if ser.in_waiting > 0: data = ser.readline().decode('utf-8', errors='ignore').strip() if data: try: value = self.parse_weight(data) self.update_display(scale_id, value, data) except Exception as e: print(f"Lỗi xử lý dữ liệu {scale_id}: {str(e)}") time.sleep(0.1) except Exception as e: print(f"Lỗi đọc dữ liệu {scale_id}: {str(e)}") self.running[scale_id] = False break def parse_weight(self, data): """Parse giá trị trọng lượng từ dữ liệu thô""" weight_match = re.search(r'\d+\s+(\d+\.\d+)', data) if weight_match: return float(weight_match.group(1)) else: wt_kg_match = re.search(r'WT/kg\s+\+?(\d+\.\d+)', data) if wt_kg_match: return float(wt_kg_match.group(1)) else: all_numbers = re.findall(r'[-+]?\d*\.\d+|\d+', data) if len(all_numbers) >= 2: return float(all_numbers[1]) else: number_match = re.search(r'[-+]?\d*\.\d+|\d+', data) if number_match: return float(number_match.group()) else: return 0.0 def update_display(self, scale_id, value, raw_data): """Cập nhật hiển thị cho cân""" widgets = self.data_widgets[scale_id] # Cập nhật giá trị trọng lượng widgets['weight_var'].set(f"{value:.3f}") # Đổi màu theo giá trị if value > 0: widgets['weight_label'].config(fg='#2E7D32') # Xanh elif value < 0: widgets['weight_label'].config(fg='#C62828') # Đỏ else: widgets['weight_label'].config(fg='#F57C00') # Cam # Cập nhật thống kê values = widgets['values'] values.append(value) if len(values) > 1000: values = values[-1000:] stats = widgets['stats_vars'] stats['count'].set(str(len(values))) stats['max'].set(f"{max(values):.3f}") stats['min'].set(f"{min(values):.3f}") stats['avg'].set(f"{sum(values)/len(values):.3f}") # Lưu vào file try: timestamp = datetime.datetime.now().strftime("%H:%M:%S") with open(widgets['history_file'], 'a', encoding='utf-8') as f: f.write(f"{timestamp},{raw_data},{value:.3f}\n") except Exception as e: print(f"Lỗi ghi file: {str(e)}") # Cập nhật status scale_name = widgets['scale_name'] self.status_var.set(f"{scale_name}: {value:.3f} kg | Tổng: {len(values)} lần đọc") def view_history(self): """Mở thư mục lịch sử""" try: if os.name == 'nt': os.startfile(self.history_dir) else: os.system(f'xdg-open "{self.history_dir}"') self.status_var.set(f"Đã mở thư mục lịch sử: {self.history_dir}") except Exception as e: messagebox.showerror("Lỗi", f"Không thể mở thư mục lịch sử:\n{str(e)}") def disconnect_all(self): """Ngắt tất cả kết nối""" if messagebox.askyesno("Xác nhận", "Bạn có chắc muốn ngắt tất cả kết nối?"): for scale_id in list(self.serial_connections.keys()): self.disconnect_scale(scale_id) self.status_var.set("Đã ngắt tất cả kết nối") if __name__ == "__main__": root = tk.Tk() app = DualScaleRS232Reader(root) root.mainloop()