Composite Debug Tool: GUI test tổng hợp cho STM32 USB
GUI Python gộp CDC log, vendor command và RAM dump vào một màn hình: pattern queue-poll thống nhất trong tkinter, và vì sao viết GUI mới không đụng vào phần USB đã có.
Bài 8/8 trong series USB Device trên STM32.
Trước khi đọc bài này
Bài 7 đã có CLI tool (vendor_test.py) test được 4 vendor command. Bài này
thêm GUI gộp CDC log realtime và toàn bộ vendor panel vào một cửa sổ.
Milestone: lab-14-composite-debug-tool-gui.
Ý chính bài này
- GUI viết mới ở lab-14, nhưng không đụng vào code giao tiếp USB - tận dụng
vendor_usb.pyđã test xong từ lab-13. - Một queue, một vòng poll duy nhất xử lý cả CDC log lẫn dump progress - không phải hai cơ chế khác nhau.
- CDC và USB vendor là hai kết nối độc lập, mất một vẫn dùng được cái kia.
tkinterkhông thread-safe: mọi cập nhật UI phải quay về main thread qua queue.
Thiết kế tổng quan
CdcLogger thread
Đọc dòng log liên tục, đẩy vào _result_queue.
Dump worker thread
Đọc Bulk IN, đẩy progress vào cùng _result_queue.
_result_queue
Điểm gặp duy nhất giữa các thread và main thread.
self.after(_poll)
main thread, chạy định kỳ, lấy hết item ra khỏi queue.
Widget cập nhật
Text log, progress bar - chỉ sửa từ đây.
Hai nguồn dữ liệu khác hẳn nhau (log CDC liên tục, tiến độ dump rời rạc) nhưng dùng chung đúng một cơ chế đưa dữ liệu về main thread - không phải hai pattern riêng cho hai việc.
Source map
tools/
├─ vendor_usb.py // protocol layer, dùng chung CLI và GUI (lab-13)
├─ vendor_test.py // CLI smoke test (lab-13)
├─ requirements.txt // + pyserial (GUI cần đọc CDC qua COM port)
└─ composite_debug_tool/
├─ main.py // entry point, trỏ về vendor_usb.py ở thư mục cha
├─ gui.py // layout, _result_queue, _poll (lab-14)
├─ cdc_logger.py // thread đọc COM port, đẩy vào queue
└─ hex_view.py // hex preview cho RAM dump main.py dùng sys.path.insert trỏ về thư mục tools/ cha thay vì giữ bản
copy vendor_usb.py riêng - một bản duy nhất, không lo hai bản lệch nhau.
Vì sao viết GUI mới không đụng vào phần USB
gui.py là code mới hoàn toàn, viết ở lab-14. Nhưng viết mới không có nghĩa
viết lại từ đầu: vendor_usb.py (protocol layer) đã tồn tại và được kiểm
chứng từ lab-13, qua vendor_test.py chạy CLI thành công cả 4 lệnh trước khi
GUI tồn tại.
# gui.py - chỉ gọi lại API đã có, không viết thêm logic giao tiếpinfo = self._usb.get_firmware_info()ok = self._usb.set_led_mode(mode)result = self._usb.start_ram_dump(progress_cb=progress_cb)GUI không viết thêm ctrl_transfer nào, không tự parse lại FirmwareInfo_t
theo cách khác. Toàn bộ phần khó (định dạng request, đọc response, đọc Bulk IN
theo acceptedLength) đã giải quyết xong ở lab-13. Việc của lab-14 chỉ là lớp
trình bày: layout, thread, queue, progress bar, hex view - không chạm vào code
giao tiếp USB.
Quyết định 1: một queue cho mọi thứ cần về main thread
tkinter không thread-safe - mọi thao tác lên widget phải chạy trên main
thread. Thay vì có nhiều pattern khác nhau cho từng loại dữ liệu, GUI dùng
đúng một _result_queue, gắn tag ở phần tử đầu tiên để _poll biết xử lý ra sao:
# Từ thread đọc CDC (cdc_logger.py) và thread dump (gui.py) - khác nhau về# nguồn nhưng cùng đẩy vào một queue duy nhấtself._result_queue.put(("progress", received, total))self._result_queue.put(("dump_done", result))# _poll - main thread, gọi lại chính nó qua self.after mỗi POLL_MSdef _poll(self): while not self._result_queue.empty(): item = self._result_queue.get_nowait() # ... rẽ nhánh theo item[0] ... self.after(self.POLL_MS, self._poll)CDC log dùng queue riêng của nó (cdc_logger.py tự có queue.Queue), nhưng
theo đúng nguyên lý giống hệt: thread nền chỉ đẩy dữ liệu thô vào queue, không
bao giờ chạm widget trực tiếp - main thread mới là nơi duy nhất được sửa UI.
Quyết định 2: CdcLogger tự quản lý vòng đời thread
class CdcLogger: def connect(self, port: str, baudrate: int = 115200) -> None: self._stop_event.clear() self._port = serial.Serial(port, baudrate, timeout=0.1) self._thread = threading.Thread(target=self._reader, daemon=True) self._thread.start() def disconnect(self) -> None: self._stop_event.set() self._thread.join(timeout=2.0) self._port.close()timeout=0.1 cho Serial() là lý do vòng đọc dừng được: không có timeout,
readline() block vô hạn khi chưa có dòng mới, thread không bao giờ quay lại
kiểm tra _stop_event. daemon=True nghĩa là Python tự kill thread này khi
chương trình chính thoát - không cần chờ đúng quy trình nếu người dùng đóng
cửa sổ giữa chừng.
Hai kết nối độc lập
GUI có hai nút Connect riêng: một cho CDC (qua COM port, pyserial), một cho
USB vendor (qua pyusb/libusbK). Mất kết nối này không ảnh hưởng kết nối kia
- chỉ cần xem log thì không cần cài Zadig, chỉ cần test vendor command thì không cần mở CDC.
RAM dump: worker thread, progress qua queue
def _cmd_start_dump(self): def progress_cb(received, total): self._result_queue.put(("progress", received, total)) def worker(): result = self._usb.start_ram_dump(progress_cb=progress_cb) self._result_queue.put(("dump_done", result)) threading.Thread(target=worker, daemon=True).start()progress_cb chạy trong worker thread (được vendor_usb.py gọi lại mỗi khi
nhận thêm dữ liệu), chỉ làm đúng một việc: đẩy vào queue. _poll ở main
thread đọc queue và cập nhật thanh tiến độ - đúng pattern đã nói ở Quyết định 1,
không phải cơ chế riêng cho dump.
Sau khi dump xong, hex_view.py hiển thị preview đầu file dạng hex + ASCII để
xác nhận có nội dung thật. Nút Save lưu toàn bộ 147456 byte ra file .bin.
Video demo
Video demo
Thứ tự demo: Connect → Get Info → Repeat Toggle → LED Control → RAM Dump.
Tóm tắt series
Bài 8 là điểm kết của series, nhưng không phải điểm kết của repo. lab-15
(USB lifecycle) merge sau đó, xử lý reset/suspend/resume cho dump dở dang và
TX flag kẹt - chi tiết ở mục “USB lifecycle” trong
trang project Composite Device.
Nhìn lại toàn bộ series, hai điều đáng nhớ nhất không phải chi tiết giao thức USB:
- Build OK không đủ. Phần lớn bug composite class (lab-09) không gây lỗi build. USBView và Wireshark mới là nơi phát hiện ra.
- Tách layer trả giá trị thật, không phải nguyên tắc suông. GUI viết mới
ở lab-14 gần như không chạm vào code USB - vì
vendor_usb.pyđã tách riêng và test xong từ lab-13.
Tài liệu tham khảo
- tkinter documentation - Python GUI framework
- pyserial documentation - đọc CDC qua COM port
- threading.Queue - thread-safe queue cho pattern producer/consumer giữa các thread
Bài viết này hữu ích với bạn?
Chia sẻ, góp ý, hoặc ủng hộ nếu bạn thấy nội dung này có giá trị.