TrackerApp
TrackerApp to zaawansowana, wielomodułowa aplikacja desktopowa klasy MES (Manufacturing Execution System) oraz WMS (Warehouse Management System), zaprojektowana do kompleksowego śledzenia procesów produkcyjnych, kontroli jakości (IQC, IPQC, OQC, FQA), pakowania, paletyzacji oraz serwisu posprzedażowego (RMA). System pozwala na pełną ewidencję i walidację cyklu życia produktu (traceability) w czasie rzeczywistym – od momentu przyjęcia surowych komponentów, przez kolejne etapy montażu i testów technologicznych (FCT/ME), aż po pakowanie jednostkowe, paletyzację, wysyłkę do klienta końcowego oraz obsługę gwarancyjną.
🛠️ Stos Technologiczny (Tech Stack)
Wszystkie warstwy aplikacji zostały zaprojektowane z myślą o wydajności, precyzji sprzętowej i odporności na błędy produkcyjne:
- Desktop & GUI:
Python 3.12+•PyQt6(wielowątkowość, asynchroniczny Event Loop) •Pillow - Baza Danych & ORM:
SQLAlchemy 2.0(mechanizmy SSoT i zdarzenia) •MySQL / MariaDB - Integracja Sprzętowa:
ezPL (Godex)(surowe komendy TCP/IP dla drukarek) •win32print(low-level Windows API) - Raportowanie:
ReportLab PDF Library(generowanie certyfikatów w tle, dynamiczne tabele) - Testowanie & Jakość:
pytest•pytest-qt•pytest-cov(pokrycie testami kluczowych ścieżek) - Dystrybucja:
PyInstaller(pakowanie do standalone.exe)
📈 Korzyści Biznesowe (Business Impact)
- Traceability < 5s (Pełna identyfikowalność wsteczna): System umożliwia natychmiastowe odtworzenie pełnej historii każdego wyprodukowanego urządzenia (sparowane podzespoły, wyniki testów FCT/ME, operatorzy), co skraca czas weryfikacji reklamacji o 90%.
- Poka-Yoke 0% (Brak pominiętych etapów): Logika biznesowa systemu weryfikuje poprawność sekwencji produkcyjnej – produkt nie może przejść do kolejnego etapu (np. pakowania) bez zaliczenia wszystkich wymaganych testów (PASS).
- Automatyzacja 75% (Logistyka i pakowanie): Integracja z przemysłowymi drukarkami etykiet wyeliminowała manualne projektowanie fiszek, automatyzując generowanie unikalnych kodów kreskowych (Code128) i kodów EAN.
- Dokumentacja 80% szybciej: Asynchroniczny generator raportów w tle (ReportLab) tworzy certyfikaty testowe w kilka sekund, pobierając tabele pomiarowe bezpośrednio z bazy danych.
💡 Wyzwania Inżynieryjne i Rozwiązania (The "How")
TrackerApp został zaprojektowany w architekturze modularnej, z wyraźnym oddzieleniem warstwy prezentacji od logiki biznesowej, aby sprostać wymaganiom pracy w trybie ciągłym (24/7).
1. Wielowątkowość i responsywność GUI
- Wyzwanie: Standardowy Event Loop PyQt6 jest jednowątkowy. Ciężkie operacje (generowanie PDF, zapytania do DB, komunikacja sieciowa z drukarkami) blokowałyby interfejs, powodując frustrację operatorów.
- Rozwiązanie: Wdrożono architekturę opartą na klasach dziedziczących po
QThread(np.ReportsGeneratorWorker). Ciężka logika została przeniesiona do wątków robotniczych, a komunikacja z GUI odbywa się wyłącznie poprzez bezpiecznepyqtSignal. Dzięki temu interfejs jest w 100% responsywny nawet podczas generowania złożonych raportów.
2. Automatyczny Audit Trail (SSoT) na poziomie ORM
- Wyzwanie: Zgodność z normami jakości (ISO 9001) wymaga niezaprzeczalności historii zmian. Tradycyjne logowanie w widokach jest podatne na błędy (pominięcia).
- Rozwiązanie: Zaimplementowano scentralizowany mechanizm audytu w warstwie ORM SQLAlchemy przy użyciu zdarzeń (
before_flush,after_flush). System automatycznie przechwytuje każdą zmianę w modelu, porównuje stanold_valuesvsnew_valuesi serializuje je do JSON w jednej transakcji. To jedyne źródło prawdy (SSoT) o historii każdej zmiany w systemie.
3. Integracja sprzętowa (Niezawodność drukarek)
- Wyzwanie: Windowsowy bufor wydruku (Spooler) w warunkach produkcyjnych często zawodził, generując opóźnienia i błędy synchronizacji.
- Rozwiązanie: Całkowicie pominięto systemowe sterowniki drukarek. Komunikacja z drukarkami Godex realizowana jest bezpośrednio przez surowe komendy języka
ezPLprzesyłane do gniazda TCP/IP (port 9100) lub bezpośrednio do portu USB przezwin32print. Czcionki i szablony są wgrywane do pamięci flash drukarki, co czyni proces druku błyskawicznym.
4. Implementacja Poka-Yoke (Maszyna Stanów)
- Wyzwanie: Ryzyko przejścia wadliwego produktu do kolejnego etapu produkcji.
- Rozwiązanie: Wprowadzono maszynę stanów zdefiniowaną w bazie danych. ORM weryfikuje sekwencję (
ProjectFlow) przy każdej próbie zmiany statusu (np. przed pakowaniem). Jeśli testy pomiarowe (FCT/ME) nie zwrócą statusuPASS, transakcja jest blokowana na poziomie bazy danych, uniemożliwiając przesunięcie produktu w systemie.
💻 Przykłady Kodu (Code Snippets)
1. Automatyczny Audit Trail za pomocą SQLAlchemy Events (wycinek z src/db.py)
Prezentacja scentralizowanego nasłuchiwania sesji ORM. Przechwytuje każdą zmianę w modelach przed zatwierdzeniem transakcji (`before_flush`) i zapisuje stary oraz nowy stan w formie JSON, przypisując wygenerowane RecordID po zapisie (`after_flush`) w ramach jednej transakcji:# src/db.py
from sqlalchemy import event, inspect
from sqlalchemy.orm import Session
import json
from src.models.system import AuditLog
def log_change(session, action, target):
"""Przygotowuje wpis audytu. Dla INSERT ID zostanie uzupełnione w after_flush."""
if isinstance(target, AuditLog):
return
table_name = target.__tablename__
current_user = get_current_user()
state = inspect(target)
old_values = {}
new_values = {}
if action == 'UPDATE':
for attr in state.mapper.column_attrs:
hist = state.get_history(attr.key, True)
if hist.has_changes():
if hist.deleted:
old_values[attr.key] = str(hist.deleted[0]) if hist.deleted[0] is not None else None
new_values[attr.key] = str(getattr(target, attr.key))
elif action == 'INSERT':
new_values = {k: str(v) if v is not None else None for k, v in object_as_dict(target).items()}
elif action == 'DELETE':
old_values = {k: str(v) if v is not None else None for k, v in object_as_dict(target).items()}
def truncate_large_data(data_dict):
"""Skraca zbyt długie wartości w słowniku logów."""
if not data_dict:
return None
truncated = {}
for k, v in data_dict.items():
if v and len(v) > 1000:
truncated[k] = f"{v[:100]}... <TRUNCATED {len(v)} chars> ...{v[-20:]}"
else:
truncated[k] = v
return json.dumps(truncated)
audit_entry = AuditLog(
TableName=table_name,
RecordID=getattr(target, 'ID', None),
Action=action,
OldValues=truncate_large_data(old_values),
NewValues=truncate_large_data(new_values),
User=current_user
)
audit_entry._target_obj = target
if not hasattr(session, '_pending_audit_logs'):
session._pending_audit_logs = []
session._pending_audit_logs.append(audit_entry)
@event.listens_for(Session, 'before_flush')
def receive_before_flush(session, flush_context, instances):
for obj in session.new:
log_change(session, 'INSERT', obj)
for obj in session.dirty:
if session.is_modified(obj):
log_change(session, 'UPDATE', obj)
for obj in session.deleted:
log_change(session, 'DELETE', obj)
@event.listens_for(Session, 'after_flush')
def audit_after_flush(session, flush_context):
"""Uzupełnia RecordID dla nowych obiektów i zapisuje logi audytu."""
if not hasattr(session, '_pending_audit_logs') or not session._pending_audit_logs:
return
logs = session._pending_audit_logs
session._pending_audit_logs = []
for log in logs:
if log.Action == 'INSERT' and log.RecordID is None:
log.RecordID = getattr(log._target_obj, 'ID', None)
if log.NewValues and log.RecordID is not None:
try:
if log.NewValues.strip().startswith("{") and "<TRUNCATED" not in log.NewValues:
nv = json.loads(log.NewValues)
if 'ID' in nv and (nv['ID'] is None or nv['ID'] == 'None'):
nv['ID'] = str(log.RecordID)
log.NewValues = json.dumps(nv)
except: pass
if hasattr(log, '_target_obj'):
del log._target_obj
session.add(log)
2. Niskopoziomowa integracja sprzętowa z drukarkami Godex (wycinek z src/printer_godex.py)
Przykłady bezpośredniej wysyłki komend ezPL przez gniazda TCP/IP (LAN) oraz poprzez niskopoziomowe systemowe API bufora drukowania Windows (USB) za pomocą `win32print`:# src/printer_godex.py
import socket
import win32print
# Komunikacja sieciowa przez surowe gniazdo TCP/IP
class Ge300LAN(PrinterFlash):
def connect_to_printer(self):
self.__check_connection_detail()
try:
self.__printer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.__printer.settimeout(2)
self.__printer.connect((self.__printer_ip, self.__tcp_port))
except Exception as e:
self.logs.add_log("w", f"Błąd połączenia TCP/IP z drukarką: {e}")
def send_to_printer(self, label, **kwargs):
self.connect_to_printer()
if not self.__printer:
return
raw_data = bytes(label, "utf-8")
try:
self.__printer.send(raw_data)
except Exception as e:
self.logs.add_log("w", f"Błąd wysyłania komend ezPL: {e}")
finally:
self.__printer.close()
# Komunikacja USB z pominięciem sterowników przez low-level Windows API
class Ge300USB(PrinterFlash):
def connect_to_printer(self):
self.__check_connection_detail()
try:
self.__printer = win32print.OpenPrinter(self.__printer_name)
# Sprawdzenie statusu online/offline drukarki
attributes = win32print.GetPrinter(self.__printer)[13]
if (attributes & 0x00000400) >> 10:
self.__printer = None
self.logs.add_log("w", "Drukarka USB jest offline!")
except Exception as e:
self.logs.add_log("w", f"Błąd otwierania portu drukarki USB: {e}")
def send_to_printer(self, label, **kwargs):
self.connect_to_printer()
if not self.__printer:
return
raw_data = bytes(label, "utf-8")
try:
hJob = win32print.StartDocPrinter(self.__printer, 1, ("TrackerApp - label", None, "RAW"))
try:
win32print.StartPagePrinter(self.__printer)
win32print.WritePrinter(self.__printer, raw_data)
win32print.EndPagePrinter(self.__printer)
finally:
win32print.EndDocPrinter(self.__printer)
finally:
win32print.ClosePrinter(self.__printer)
3. Asynchroniczny worker GUI oparty na wątkach PyQt6 (wycinek z src/reports_generator/worker.py)
Implementacja wielowątkowości w PyQt6 przy użyciu klasy `QThread` oraz bezpiecznej komunikacji z wątkiem głównym interfejsu GUI za pomocą sygnałów `pyqtSignal` w celu unikania zawieszania interfejsu (GUI freeze):# src/reports_generator/worker.py
from PyQt6.QtCore import QThread, pyqtSignal
class ReportsGeneratorWorker(QThread):
finished = pyqtSignal(str) # Sygnał po sukcesie (SN urządzenia)
error = pyqtSignal(str) # Sygnał błędu
progress = pyqtSignal(str) # Sygnał ze statusem postępu (tekst)
def __init__(self, sn: str, destination_path: str, product_report_builder):
super().__init__()
self.sn = sn
self.destination_path = destination_path
self.builder = product_report_builder # Obiekt generatora PDF (ProductReport)
def run(self):
try:
self.progress.emit("Zbieranie danych z baz danych...")
data = self.builder._collect_product_data_internal(self.sn, self.destination_path)
if not data:
self.error.emit("Nie udało się zebrać danych dla podanego numeru SN.")
return
self.progress.emit("Budowanie dokumentu PDF...")
self.builder._setup_document(data)
self.builder._build_report_content(data)
self.progress.emit("Finalizacja i zapisywanie...")
self.builder._finalize_report_internal(data['product'])
self.finished.emit(self.sn)
except Exception as e:
self.error.emit(f"Błąd krytyczny podczas generowania raportu: {str(e)}")