import threading
import serial
import serial.tools.list_ports
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog
import time
import json
import re
import os
import sys
import tkinter.font as tkfont
import subprocess
CONFIG_FILE = 'config.json'
class SerialGUI(tk.Tk):
def __init__(self):
super().__init__()
# --- Phóng to font hệ thống thêm 20% ---
for name in ("TkDefaultFont", "TkTextFont", "TkFixedFont", "TkMenuFont",
"TkHeadingFont"):
try:
f = tkfont.nametofont(name)
f.configure(size=int(f.cget("size") * 1.2))
except tk.TclError:
pass
self.title("E-well Gateway")
self.state('zoomed')
self.protocol("WM_DELETE_WINDOW", self.on_close)
# Load config
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
saved = json.load(f)
except:
saved = {}
else:
saved = {}
# Variables
self.wifi_ssid = tk.StringVar(value=saved.get('ssid','CTY VTE'))
self.wifi_pass = tk.StringVar(value=saved.get('wifi_pass','01011984'))
self.username = tk.StringVar(value=saved.get('username','adminmqtt'))
self.user_pass = tk.StringVar(value=saved.get('user_pass','Sys123456'))
self.mqtt_broker =
tk.StringVar(value=saved.get('mqtt_broker','160.30.136.224'))
self.mqtt_port = tk.IntVar(value=saved.get('mqtt_port',1883))
self.gateway_sn = tk.StringVar(value=saved.get('gateway_sn',''))
self.dev_count = tk.IntVar(value=saved.get('devices',1))
self.sn_vars = []
# Command map for auto replies
self.cmd_map = {
'tên wifi': lambda: self.wifi_ssid.get(),
'mật khẩu wifi': lambda: self.wifi_pass.get(),
'sever mqtt': lambda: self.mqtt_broker.get(),
'port': lambda: str(self.mqtt_port.get()),
'username': lambda: self.username.get(),
'password': lambda: self.user_pass.get(),
'nhập seri của gateway': lambda: self.gateway_sn.get(),
'nhập số công tơ kết nối (1-255)': lambda: str(self.dev_count.get()),
'seri': lambda: ",".join(v.get() for v in self.sn_vars)
}
# Serial
self.ser = None
self._stop_read = threading.Event()
# Layout
pw = ttk.Panedwindow(self, orient='horizontal')
pw.pack(fill='both', expand=True)
# Left pane: config
left = ttk.Labelframe(pw, text="Cấu hình & Thiết bị")
pw.add(left, weight=1)
canvas = tk.Canvas(left)
vsb = ttk.Scrollbar(left, orient='vertical', command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)
vsb.pack(side='right', fill='y')
canvas.pack(side='left', fill='both', expand=True)
frame_cfg = ttk.Frame(canvas)
frame_cfg.bind('<Configure>', lambda e:
canvas.configure(scrollregion=canvas.bbox('all')))
canvas.create_window((0,0), window=frame_cfg, anchor='nw')
fields = [
("WiFi SSID:", self.wifi_ssid),
("WiFi Pass:", self.wifi_pass),
("MQTT Broker:", self.mqtt_broker),
("MQTT Port:", self.mqtt_port),
("Username:", self.username),
("Password:", self.user_pass)
]
for i,(lbl,var) in enumerate(fields):
ttk.Label(frame_cfg, text=lbl).grid(row=i, column=0, padx=8, pady=4,
sticky='e')
width = 30 if isinstance(var,tk.StringVar) else 10
ttk.Entry(frame_cfg, textvariable=var, width=width).grid(row=i,
column=1, padx=8, pady=4, sticky='w')
row0 = len(fields)
ttk.Label(frame_cfg, text="SN Gateway:").grid(row=row0, column=0, padx=8,
pady=4, sticky='e')
ttk.Entry(frame_cfg, textvariable=self.gateway_sn, width=30).grid(row=row0,
column=1, padx=8, pady=4, sticky='w')
ttk.Label(frame_cfg, text="Số thiết bị:").grid(row=row0+1, column=0,
padx=8, pady=4, sticky='e')
spin = tk.Spinbox(frame_cfg, from_=1, to=30, textvariable=self.dev_count,
width=6, command=self.update_sn_entries)
spin.grid(row=row0+1, column=1, padx=8, pady=4, sticky='w')
self.sn_frame = ttk.Frame(frame_cfg)
self.sn_frame.grid(row=row0+2, column=0, columnspan=2, pady=8)
self.update_sn_entries()
# Right pane: serial + log + actions
right = ttk.Frame(pw)
pw.add(right, weight=2)
conn = ttk.Labelframe(right, text="Kết nối Gateway")
conn.pack(fill='x', padx=12, pady=(12,6))
ttk.Label(conn, text="Cổng COM:").grid(row=0, column=0, padx=6)
self.cmb_port = ttk.Combobox(conn, values=self.get_ports(), width=12)
self.cmb_port.grid(row=0, column=1, padx=6)
ttk.Button(conn, text="Làm mới", command=self.refresh_ports).grid(row=0,
column=2, padx=6)
ttk.Label(conn, text="Baud rate:").grid(row=0, column=3, padx=6)
self.cmb_baud = ttk.Combobox(conn,
values=[300,1200,2400,4800,9600,19200,38400,57600,115200], width=10)
self.cmb_baud.set(9600)
self.cmb_baud.grid(row=0, column=4, padx=6)
self.btn_connect = ttk.Button(conn, text="Kết nối", command=self.connect)
self.btn_connect.grid(row=0, column=5, padx=6)
self.btn_disconnect = ttk.Button(conn, text="Ngắt",
command=self.disconnect, state='disabled')
self.btn_disconnect.grid(row=0, column=6, padx=6)
logf = ttk.Labelframe(right, text="Dữ liệu nhận về")
logf.pack(fill='both', expand=True, padx=12, pady=6)
self.txt_log = scrolledtext.ScrolledText(logf, wrap='none',
font=('Consolas',12))
self.txt_log.pack(fill='both', expand=True, padx=6, pady=6)
act = ttk.Frame(right)
act.pack(fill='x', padx=12, pady=6)
self.btn_flash = ttk.Button(act, text="Nạp Firmware",
command=self.flash_firmware, state='disabled')
self.btn_flash.pack(side='left', padx=(0,12))
self.btn_clear_data = ttk.Button(act, text="Xóa dữ liệu",
command=self.clear_data, state='disabled')
self.btn_clear_data.pack(side='left', padx=(0,12))
self.btn_save = ttk.Button(act, text="Lưu config",
command=self.save_config, state='disabled')
self.btn_save.pack(side='left', padx=(0,12))
self.btn_clear_log = ttk.Button(act, text="Xóa log",
command=self.clear_log, state='disabled')
self.btn_clear_log.pack(side='left')
def update_sn_entries(self):
for w in self.sn_frame.winfo_children(): w.destroy()
self.sn_vars.clear()
for i in range(self.dev_count.get()):
var = tk.StringVar()
self.sn_vars.append(var)
ttk.Label(self.sn_frame, text=f"SN thiết bị {i+1}:").grid(row=i,
column=0, padx=6, pady=4, sticky='e')
ttk.Entry(self.sn_frame, textvariable=var, width=30).grid(row=i,
column=1, padx=6, pady=4, sticky='w')
def get_ports(self): return [p.device for p in
serial.tools.list_ports.comports()]
def refresh_ports(self): self.cmb_port['values'] = self.get_ports()
def connect(self):
port = self.cmb_port.get(); baud = int(self.cmb_baud.get() or 0)
if not port or not baud:
messagebox.showwarning("Thiếu thông tin","Chọn COM và baudrate!")
return
try:
self.ser = serial.Serial(port, baud, timeout=0.1)
self.ser.dtr=False; time.sleep(0.05); self.ser.dtr=True;
self.ser.reset_input_buffer()
except Exception as e:
messagebox.showerror("Kết nối lỗi",str(e)); return
for b in
(self.btn_flash,self.btn_clear_data,self.btn_save,self.btn_clear_log,self.btn_disco
nnect): b.config(state='normal')
self.btn_connect.config(state='disabled'); self._stop_read.clear()
threading.Thread(target=self._read_thread,daemon=True).start()
self.log(f"✔ Đã kết nối {port} @ {baud}")
def disconnect(self):
if self.ser and self.ser.is_open: self._stop_read.set(); self.ser.close()
for b in
(self.btn_flash,self.btn_clear_data,self.btn_save,self.btn_clear_log,self.btn_disco
nnect): b.config(state='disabled')
self.btn_connect.config(state='normal'); self.log("✖ Đã ngắt kết nối")
def clear_data(self):
if self.ser: self.ser.write(("Đang xóa dữ liệu trước đó vui lòng không thao
tác thêm ...\n").encode()); self.log("Đang xóa dữ liệu trước đó vui lòng không thao
tác thêm ...")
def save_config(self):
if self.ser and self.ser.is_open: self.ser.write(("y\n").encode());
self.log("➡ Gửi y"); self.btn_save.config(state='disabled')
cfg = {'ssid':self.wifi_ssid.get(), 'wifi_pass':self.wifi_pass.get(),
'username':self.username.get(), 'user_pass':self.user_pass.get(),
'mqtt_broker':self.mqtt_broker.get(), 'mqtt_port':self.mqtt_port.get(),
'gateway_sn':self.gateway_sn.get(), 'devices':self.dev_count.get(), 'serials':
[v.get() for v in self.sn_vars]}
with open(CONFIG_FILE,'w',encoding='utf-8') as f:
json.dump(cfg,f,ensure_ascii=False,indent=2)
def clear_log(self): self.txt_log.delete('1.0',tk.END)
def flash_firmware(self):
if self.ser and self.ser.is_open: self._stop_read.set(); self.ser.close();
self.log("✖ Đã đóng COM để nạp firmware")
choice = messagebox.askquestion("Firmware","Chọn thư mục chứa các file .bin
(Yes) hoặc file .bin đơn (No)?")
if choice=='yes':
folder = filedialog.askdirectory(title="Chọn thư mục chứa các
file .bin")
if not folder: return
bl=os.path.join(folder,'bootloader.bin');
part=os.path.join(folder,'partitions.bin')
apps=[f for f in os.listdir(folder) if f.endswith('.bin') and f not
in('bootloader.bin','partitions.bin')]
if not apps: messagebox.showerror("Lỗi nạp","Không tìm thấy firmware
ứng dụng");return
appf=os.path.join(folder,apps[0]);
offsets=['0x1000',bl,'0x8000',part,'0x10000',appf]
else:
path=filedialog.askopenfilename(title="Chọn file
firmware",filetypes=[("Firmware files",("*.ino.bin","*.bin","*.bin")),("All
files","*.*")],defaultextension=".bin")
if not path:return
offsets=['0x10000',path]
port=self.cmb_port.get()
if not port: messagebox.showwarning("Thiếu COM","Chọn cổng COM trước khi
nạp firmware!");return
def _run_flash():
self.log("Bắt đầu nạp firmware với python -m esptool...")
cmd=[sys.executable,'-m','esptool','--chip','esp32','--port',port,'--
baud','921600','write_flash','-z']+offsets
try:
proc=subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,universal
_newlines=True)
except FileNotFoundError: messagebox.showerror("Lỗi nạp","Không tìm
thấy esptool. Hãy pip install esptool");return
for ln in proc.stdout: self.log(ln.rstrip())
rc=proc.wait()
if rc==0: self.log("✔ Nạp firmware thành
công");messagebox.showinfo("Hoàn thành","Nạp firmware thành công!")
else: self.log(f"✖ Nạp firmware thất bại (code
{rc})");messagebox.showerror("Lỗi nạp",f"Mã trả về {rc}")
threading.Thread(target=_run_flash,daemon=True).start()
def _read_thread(self):
while not self._stop_read.is_set():
try:
raw=self.ser.readline();
if not raw: continue
line=raw.decode('utf-8',errors='ignore').strip(); self.log(line)
key=line.lower().rstrip(':').strip()
if key=='xác nhận lưu cấu hình':
self.btn_save.config(state='normal'); continue
if key=='seri gateway': resp=self.gateway_sn.get()
elif m:=re.match(r'số seri của công tơ\s*(\d+)',key):
idx=int(m.group(1))-1; resp=self.sn_vars[idx].get() if 0<=idx<len(self.sn_vars)
else ''
elif key in self.cmd_map: resp=self.cmd_map[key]()
elif key=='getconfig': self.send_config();continue
else: continue
self.ser.write((resp+"\n").encode()); self.log(f"➡ {resp}")
except: break
def send_config(self):
cfg={'ssid':self.wifi_ssid.get(),'wifi_pass':self.wifi_pass.get(),'username':self.u
sername.get(),'user_pass':self.user_pass.get(),'mqtt_broker':self.mqtt_broker.get()
,'mqtt_port':self.mqtt_port.get(),'gateway_sn':self.gateway_sn.get(),'devices':self
.dev_count.get(),'serials':[v.get() for v in self.sn_vars]}
msg=json.dumps(cfg)
self.ser.write((msg+"\n").encode()); self.log(f"➡ Gửi config: {msg}")
def log(self,msg): self.txt_log.insert('end',msg+'\n'); self.txt_log.see('end')
def on_close(self): self.disconnect(); self.destroy()
if __name__=='__main__': app=SerialGUI(); app.mainloop()