Push New Project

This commit is contained in:
naab
2025-06-04 14:55:43 +07:00
commit 31fc6c5833
20 changed files with 18625 additions and 0 deletions

575
main.py Normal file
View File

@ -0,0 +1,575 @@
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()