SPI Line Monitor
SPI Line Monitor to lekka desktopowa aplikacja służąca do ciągłego śledzenia oraz statystycznej kontroli procesu (SPC - Statistical Process Control) nanoszenia pasty lutowniczej na płytki PCB. Narzędzie automatycznie nawiązuje połączenie z bazą MySQL maszyn inspekcyjnych SPI (np. marki PARMI) i wizualizuje pomiary wysokości pasty dla kluczowych komponentów i ich wyprowadzeń (padów). Aplikacja automatycznie rysuje karty kontrolne Xbar-R, generuje histogramy rozkładu normalnego (Cp/Cpk, Pp/Ppk) oraz pozwala na tworzenie zaawansowanych raportów PDF. Dodatkowo program integruje się z maszyną za pomocą pliku konfiguracyjnego linii, wykrywając w czasie rzeczywistym aktualnie produkowany wyrób.
🛠️ Stos Technologiczny (Tech Stack)
Aplikacja została zaprojektowana z naciskiem na niezawodność, wydajność przetwarzania dużych zbiorów danych pomiarowych i zerową latencję interfejsu GUI:
- Język i GUI:
**Python 3.8+**•**PyQt5**(obsługa wielu okien ustawień, dynamiczne przełączanie widoków, kontrolki pomiarowe) - Silnik Wykresów i SPC:
**Matplotlib**(rysowanie w czasie rzeczywistym kart kontrolnych Xbar-R oraz histogramów procesu w oparciu o backend Qt5Agg) - Obliczenia Statystyczne:
**NumPy**•**SciPy**(generowanie krzywej rozkładu normalnego, obliczenia odchylenia standardowego, estymacji sigma oraz wskaźników zdolności procesu Cp, Cpk, Pp, Ppk) - Baza Danych:
**mysql-connector-python**(wydajna, bezpośrednia komunikacja z relacyjną bazą danych SQL maszyn inspekcyjnych) - Zarządzanie Stanem:
**QSettings**(trwałe przechowywanie parametrów połączenia z bazą danych MySQL, geometrii okien oraz ustawień wykresów po zamknięciu programu) - Generowanie Raportów:
**matplotlib.backends.backend_pdf.PdfPages**(generowanie wielostronicowych, eleganckich raportów PDF z wykresami i tabelami z kolorowaniem warunkowym) - Asynchroniczność:
**QThread**(wykonywanie ciężkich zapytań SQL w osobnym wątku roboczym bez blokowania interfejsu graficznego oraz monitorowanie aktywności oprogramowania maszyny)
📈 Po co to w ogóle powstało? (Zastosowanie i korzyści dla twórcy)
- Eliminacja przestojów i wad SMT: Ponad 60-70% wad montażowych w procesie SMT wynika z nieprawidłowego nadruku pasty lutowniczej. Aplikacja pozwala inżynierom procesu na natychmiastowe wykrycie trendów przesunięcia średniej wysokości pasty lub wzrostu rozrzutu przed powstaniem wad typu mostki czy zwarcia.
- Integracja w czasie rzeczywistym (Real-Time Tracking): Program automatycznie odpytuje bazę danych maszyn inspekcyjnych w zdefiniowanych interwałach czasu, zapewniając ciągły monitoring bez udziału operatora.
- Automatyczne dopasowanie do linii produkcyjnej: Aplikacja nasłuchuje zmian w pliku stanu maszyny (np.
SPIworksLIM.datdla PARMI). W momencie, gdy operator maszyny przełącza program produkcyjny, monitor automatycznie wykrywa nową nazwę projektu i aktualizuje mapowanie baz danych. - Badanie stabilności maszyny (Golden Sample): Specjalny moduł referencyjny pozwala badać powtarzalność maszyny SPI przy użyciu płytki wzorcowej, porównując pomiary z limitami ostrzegawczymi (UCL/LCL) i generując raporty stabilności.
- Wygodna archiwizacja i PDF: Automatycznie generowany raport PDF zawiera pełne karty kontrolne, histogramy zdolności procesu oraz tabele wyników, gdzie wadliwe pomiary są wyróżnione kolorem czerwonym.
💡 Architektura i Wyzwania Inżynieryjne (Czyli jak to działa pod maską)
Podczas budowania aplikacji rozwiązano szereg wyzwań związanych z przetwarzaniem strumieniowym danych produkcyjnych oraz integracją systemów przemysłowych.
1. Asynchroniczne wątki robocze (QThread) i komunikacja z bazą danych
- Wyzwanie: Pobieranie pomiarów z bazy danych dla setek punktów pomiarowych PCB może trwać od kilkunastu do kilkudziesięciu sekund (szczególnie przy dużym limicie próbek). Wykonywanie tego synchronicznie w głównym wątku powodowałoby zamrażanie GUI.
- Rozwiązanie: Przeniesienie logiki odpytywania bazy SQL do klasy
DataCheckerdziedziczącej poQThread. Wątek ten działa w nieskończonej pętli z określonym interwałem uśpienia (time.sleep(AUTO_CHECK)). Po pobraniu i ustrukturyzowaniu danych przesyła je z powrotem do głównego wątku za pomocą sygnałunew_data.emit(...), co gwarantuje płynne działanie interfejsu PyQt5.
2. Dynamiczne partycjonowanie tabel danych inspekcyjnych maszyn SPI
- Wyzwanie: Bazy danych maszyn SPI przechowują gigantyczne ilości danych. Aby zachować wydajność, tabele pomiarowe są automatycznie partycjonowane (nowa tabela tworzona jest co 1000 paneli, np.
pad_insp_result_1,pad_insp_result_1001itd.). Program musi dynamicznie wnioskować, z których tabel odczytać pomiary dla danego zakresu paneli. - Rozwiązanie: Zaimplementowano algorytm
get_table_name, który na podstawie tablicy indeksów paneli oblicza nazwę docelowej tabeli przy użyciu funkcji sufitu matematycznego (math.ceil). Dane są następnie grupowane według tabel, dzięki czemu program wysyła jedno zoptymalizowane zapytanie zbiorcze per tabela zamiast odpytywać bazę tysiące razy pojedynczo.
3. Generowanie tabelarycznych raportów graficznych o niskiej alokacji pamięci
- Wyzwanie: Wygenerowanie raportu PDF zawierającego setki wierszy tabelarycznych z kolorowaniem komórek i wieloma wykresami bywa kosztowne pamięciowo i trudne do ostylowania przy użyciu tradycyjnych bibliotek raportowych.
- Rozwiązanie: Wykorzystano bezpośredni backend
PdfPagesz bibliotekiMatplotlib. Raport jest budowany strona po stronie. Tabele są tworzone obiektowo za pomocą funkcjipylab.table, a aplikacja iteruje po jej komórkach (tab.get_celld().items()), modyfikując dynamicznie ich tła za pomocą.set_facecolor()w zależności od przekroczenia granic limitów UCL/LCL i specyfikacji (USL/LSL). Po narysowaniu każdej strony pamięć wykresu jest natychmiast czyszczona (pylab.close()), zapobiegając wyciekom pamięci RAM.
💻 Przykłady Kodu (Code Snippets)
1. Dynamiczne mapowanie partycjonowanych tabel pomiarowych
Poniższy fragment modułu bazy danych (`database/SPI_mysql.py`) pokazuje, w jaki sposób aplikacja dynamicznie mapuje indeksy paneli na odpowiednie fizyczne tabele bazy danych, minimalizując obciążenie serwera SQL:# database/SPI_mysql.py
import math
from typing import Union
def get_table_name(panels_index: Union[tuple, list]) -> dict:
"""
panels_index - tuple, list indeksów paneli dla których chcemy poznać nazwy tabel, w których znajdują się dane.
Zwraca słownik: table_names[table_name] = [lista_paneli]
"""
table_names = {}
for panel in panels_index:
podzielnik = math.ceil(int(panel) / 1000)
if podzielnik <= 1:
table_name = "pad_insp_result_1"
else:
table_name = f"pad_insp_result_{(podzielnik * 1000) - 999}"
if table_names.get(table_name) is None:
table_names[table_name] = [panel]
else:
table_names[table_name].append(panel)
return table_names
2. Generowanie raportów PDF z kolorowaniem warunkowym komórek
Poniższy fragment kodu z generatora raportów (`apps/report_pdf/main.py`) przedstawia pętlę generującą strony tabelaryczne w pliku PDF z automatyczną walidacją i zmianą kolorów tła komórek w zależności od statusu pomiarów:# apps/report_pdf/main.py
import pylab
from matplotlib.backends.backend_pdf import PdfPages
# Fragment metody w klasie GeneratorPDF(QThread)
def generate_pdf_tables(report: PdfPages, lista_danych: list, wiersze: list, kolumny: list, parameters: dict, licznik: int):
for x in range(licznik):
pylab.clf()
fig = pylab.figure(figsize=(12, 8))
pylab.subplots_adjust(left=0.03, bottom=0.1, right=0.90, top=0.94)
pylab.title(f'Tabela Pomiarowa - Strona {x + 1}/{licznik}', fontsize=12)
# Tworzenie tabeli graficznej Matplotlib
tab = pylab.table(
cellText=lista_danych[x * 40:(x + 1) * 40],
rowLabels=wiersze[x * 40:(x + 1) * 40],
rowLoc='center',
colLabels=kolumny,
colWidths=[0.09, 0.08, 0.14, 0.06, 0.08, 0.07, 0.07, 0.07, 0.07, 0.07, 0.1, 0.07, 0.09],
cellLoc='center',
loc='upper left'
)
# Warunkowe kolorowanie komórek (Walidacja UCL/LCL i Statusu)
for key, cell in tab.get_celld().items():
cell.set_linewidth(0.1)
cell.set_alpha(0.5)
row, col = key
if row > 0:
# Kolumna 8: Average (średnia próbki)
if col == 8:
wartosc = float(tab.get_celld()[(row, col)].get_text().get_text())
if wartosc > parameters['xbar_UCL'] or wartosc < parameters['xbar_LCL']:
cell.set_facecolor("red")
else:
cell.set_facecolor("green")
# Kolumna 9: Range (rozrzut)
elif col == 9:
wartosc = float(tab.get_celld()[(row, col)].get_text().get_text())
if wartosc > parameters['r_UCL'] or wartosc < parameters['r_LCL']:
cell.set_facecolor("red")
else:
cell.set_facecolor("green")
# Kolumna 11: Status płytki
elif col == 11:
wartosc = tab.get_celld()[(row, col)].get_text().get_text()
if wartosc == "GOOD":
cell.set_facecolor("green")
elif wartosc == "PASS":
cell.set_facecolor("orange")
else:
cell.set_facecolor("red")
tab.auto_set_font_size(False)
tab.set_fontsize(8)
pylab.axis('off')
# Zapis do pliku PDF
nr_strony = report.get_pagecount() + 1
pylab.text(0.93, 0.04, f"Page {nr_strony}", transform=fig.transFigure, size=7)
report.savefig()
pylab.close()