RESTRICTED
#!/usr/bin/env python3
import os, time, datetime, tkinter as tk
from tkinter import ttk, messagebox
from PIL import Image, ImageTk
import [Link] as GPIO
import openpyxl
# Matplotlib for the trend chart
import matplotlib
[Link]("Agg")
from [Link] import Figure
from [Link].backend_tkagg import FigureCanvasTkAgg
# =========================
# GPIO Pin Assignments
# =========================
CYCLE_START_PIN = 18 # FALLING edge = cycle START
CYCLE_COMPLETE_PIN = 24 # LOW = normal CYCLE COMPLETE
EMERGENCY_STOP_PIN = 25 # LOW = EMERGENCY STOP
MACHINE_FAULT_PIN = 26 # LOW = MACHINE FAULT
EXCEL_REPORT = "oee_daily_report.xlsx"
# =========================
# TPM Loss Categories (16 Major Losses)
# =========================
LOSS_CATEGORIES = [
"Machine Breakdown",
"Minor Stoppages",
"Setup & Adjustment",
"Cutting Tool Replacement",
"Startup Loss",
"Speed Reduction",
"Defects & Rework",
"Planned Shutdown Loss",
"Management Loss",
"Operating Motion Loss",
"Line Organization Loss",
"Logistics Loss",
"Measurement & Adjustment Loss",
"Energy Loss",
"Consumable Loss",
"Yield Loss"
]
RESTRICTED
# =========================
# Excel Initialization & Logging
# =========================
def initialize_excel():
if not [Link](EXCEL_REPORT):
wb = [Link]()
ws1 = [Link]
[Link] = "OEE Data"
[Link]([
"Date/Time","OEE (%)","Availability (%)","Performance (%)","Quality (%)",
"Cycle Day","Actual Cycle Time (min)",
*LOSS_CATEGORIES
])
ws2 = wb.create_sheet(title="Stoppage Log")
[Link](["Date/Time","Cycle Day","Loss Category","Duration (min)","Remarks"])
[Link](EXCEL_REPORT)
def log_oee_summary(row):
initialize_excel()
wb = openpyxl.load_workbook(EXCEL_REPORT)
ws = wb["OEE Data"]
[Link](row)
[Link](EXCEL_REPORT)
def log_stoppage_entries(entries):
initialize_excel()
wb = openpyxl.load_workbook(EXCEL_REPORT)
ws = wb["Stoppage Log"]
for e in entries:
[Link]([
e["timestamp"].strftime("%Y-%m-%d %H:%M:%S"),
e["timestamp"].day,
e["category"],
e["duration"],
e["remarks"]
])
[Link](EXCEL_REPORT)
# =========================
# OEE Calculation Functions
# =========================
def calc_availability(planned, losses):
return max(0.0, (planned - losses)/planned) if planned>0 else 0.0
RESTRICTED
def calc_performance(ideal, actual):
return ideal/actual if actual>0 else 0.0
def calc_quality(good, total):
return good/total if total>0 else 0.0
def calc_oee(a,p,q):
return a*p*q*100
# =========================
# Main App
# =========================
class OEEApp:
def _init_(self, root):
[Link] = root
[Link]("Rotary Oven OEE Monitor")
[Link]("1200x850")
# GPIO init
[Link]([Link])
for pin in (CYCLE_START_PIN, CYCLE_COMPLETE_PIN, EMERGENCY_STOP_PIN,
MACHINE_FAULT_PIN):
[Link](pin, [Link], pull_up_down=GPIO.PUD_UP)
# State
self.prev_start = [Link](CYCLE_START_PIN)==[Link]
self.prev_break = False
self.cycle_in_progress = False
self.cycle_start_time = None
self.actual_cycle_time = 0.0
# Stoppages & losses
self.stoppage_log = []
self.loss_totals = {cat:0.0 for cat in LOSS_CATEGORIES}
self.loss_vars = {cat:[Link](0.0) for cat in LOSS_CATEGORIES}
# OEE history
self.oee_history = [] # list of (timestamp, oee)
# Operator inputs
self.planned_time = [Link](480.0) # minutes
self.ideal_cycle = [Link](0.5) # minutes
self.total_parts = [Link](0)
RESTRICTED
self.good_parts = [Link](0)
self.build_widgets()
self.poll_gpio()
def build_widgets(self):
frm = [Link]([Link], padding=10)
[Link](sticky="nsew")
# Title & OEE summary
[Link](frm, text="Rotary Oven OEE Monitor", font=("Arial",16,"bold"))\
.grid(row=0,column=0,columnspan=4,pady=5)
self.lbl_oee = [Link](frm, text="OEE: --%", font=("Arial",24,"bold"))
self.lbl_oee.grid(row=1,column=0,columnspan=4,pady=5)
self.lbl_avail = [Link](frm, text="Availability: --%"); self.lbl_avail.grid(row=2,column=0,sticky="w")
self.lbl_perf = [Link](frm, text="Performance: --%"); self.lbl_perf .grid(row=3,column=0,sticky="w")
self.lbl_qual = [Link](frm, text="Quality: --%"); self.lbl_qual .grid(row=4,column=0,sticky="w")
self.lbl_act = [Link](frm, text="Actual Cycle Time: -- min")
self.lbl_act.grid(row=5,column=0,sticky="w")
# Tower lamp
lamp = [Link](frm, text="Status")
[Link](row=2,column=3,rowspan=4,padx=10,sticky="ne")
self.l_red = [Link](lamp,text="EMER STOP", width=12, bg="gray",fg="white")
self.l_amber = [Link](lamp,text="MACH FAULT", width=12, bg="gray")
self.l_green = [Link](lamp,text="RUNNING", width=12, bg="gray",fg="white")
self.l_blue = [Link](lamp,text="COMPLETE", width=12, bg="gray",fg="white")
for w in (self.l_red,self.l_amber,self.l_green,self.l_blue): [Link](pady=2)
# Inputs & Losses
inp = [Link](frm, text="Inputs & TPM Losses")
[Link](row=6,column=0,columnspan=4,sticky="ew",pady=10)
# operator inputs
labels = ["Planned Time (min):","Ideal Cycle (min):","Total Parts:","Good Parts:"]
vars = [self.planned_time,self.ideal_cycle,self.total_parts,self.good_parts]
for i,(lbl,var) in enumerate(zip(labels,vars)):
[Link](inp,text=lbl).grid(row=i,column=0,sticky="w")
[Link](inp,textvariable=var,width=10).grid(row=i,column=1,padx=5)
# loss totals
for idx,cat in enumerate(LOSS_CATEGORIES):
r = idx//2; c = (idx%2)*3+2
[Link](inp,text=cat+":").grid(row=r,column=c,sticky="e",padx=(20,2))
[Link](inp,textvariable=self.loss_vars[cat],width=8,state="readonly")\
.grid(row=r,column=c+1)
RESTRICTED
[Link](inp, text="Log Stoppage ", command=self.open_log_dialog)\
.grid(row=8,column=0,columnspan=2,pady=5)
# Stoppage Log
logf = [Link](frm,text="Stoppage Log")
[Link](row=7,column=0,columnspan=4,sticky="nsew",padx=5,pady=5)
cols=("Time","Day","Category","Duration","Remarks")
[Link] = [Link](logf,columns=cols,show="headings",height=6)
for c in cols:
[Link](c,text=c); [Link](c,width=100,anchor="center")
[Link](sticky="nsew")
sb = [Link](logf,orient="vertical",command=[Link])
[Link](row=0,column=1,sticky="ns")
[Link](yscrollcommand=[Link])
# Buttons
btns = [Link](frm); [Link](row=8,column=0,columnspan=4,pady=5)
[Link](btns,text="Calculate OEE", command=self.compute_oee).grid(row=0,column=0,padx=5)
[Link](btns,text="Generate Report",command=self.generate_report).grid(row=0,column=1,padx=5)
# Trend Chart
chartf = [Link](frm,text="OEE Trend (Last 30)")
[Link](row=9,column=0,columnspan=4,sticky="nsew",padx=5,pady=5)
[Link](9,weight=1); [Link](0,weight=1); [Link](0,weight=1)
[Link] = Figure(figsize=(7,2),dpi=100)
[Link] = [Link].add_subplot(111)
[Link].set_ylim(0,100)
[Link].set_xlabel("Day of Month")
[Link].set_ylabel("OEE (%)")
[Link].set_title("OEE Trend")
[Link] = FigureCanvasTkAgg([Link], master=chartf)
[Link].get_tk_widget().grid(sticky="nsew")
def open_log_dialog(self):
dlg = [Link]([Link]); [Link]("Log a Stoppage")
[Link](dlg,text="Loss Category:").grid(row=0,column=0,padx=5,pady=5)
cat = [Link](value=LOSS_CATEGORIES[0])
[Link](dlg,cat,*LOSS_CATEGORIES).grid(row=0,column=1)
[Link](dlg,text="Duration (min):").grid(row=1,column=0,padx=5,pady=5)
dur = [Link](0.0); [Link](dlg,textvariable=dur).grid(row=1,column=1)
[Link](dlg,text="Remarks:").grid(row=2,column=0,padx=5,pady=5)
rem = [Link](); [Link](dlg,textvariable=rem,width=30).grid(row=2,column=1)
def add():
RESTRICTED
ts = [Link]()
e = {"timestamp":ts,"category":[Link](),
"duration":[Link](),"remarks":[Link]().strip()}
self.stoppage_log.append(e)
self.loss_totals[e["category"]] += e["duration"]
self.loss_vars[e["category"]].set(round(self.loss_totals[e["category"]],2))
[Link]("",[Link],values=(
[Link]("%H:%M:%S"),[Link],e["category"],
f"{e['duration']:.2f}",e["remarks"]
))
[Link]()
[Link](dlg,text="Add",command=add).grid(row=3,column=0,pady=10)
[Link](dlg,text="Cancel",command=[Link]).grid(row=3,column=1,pady=10)
def open_breakdown_dialog(self, category, duration):
dlg = [Link]([Link]); [Link](f"{category} Details")
[Link](dlg,text=f"{category} Duration: {duration:.2f} min").grid(row=0,column=0,columnspan=2,pady=5)
[Link](dlg,text="Remarks:").grid(row=1,column=0,padx=5,pady=5)
rem = [Link]()
[Link](dlg,textvariable=rem,width=30).grid(row=1,column=1,padx=5,pady=5)
def add():
ts = [Link]()
e = {"timestamp":ts,"category":category,
"duration":duration,"remarks":[Link]().strip()}
self.stoppage_log.append(e)
self.loss_totals[category] += duration
self.loss_vars[category].set(round(self.loss_totals[category],2))
[Link]("",[Link],values=(
[Link]("%H:%M:%S"),[Link],category,
f"{duration:.2f}",e["remarks"]
))
# recalc & trend
self.compute_oee()
self.oee_history.append((ts,[Link]))
if len(self.oee_history)>30: self.oee_history.pop(0)
self.draw_trend()
[Link]()
[Link](dlg,text="Add Remark",command=add).grid(row=2,column=0,pady=10)
[Link](dlg,text="Cancel", command=[Link]).grid(row=2,column=1,pady=10)
def open_post_cycle_dialog(self):
dlg = [Link]([Link]); [Link]("Enter Parts Count")
[Link](dlg,text="Total Parts Produced:").grid(row=0,column=0,padx=5,pady=5)
tot = [Link](0); [Link](dlg,textvariable=tot).grid(row=0,column=1)
RESTRICTED
[Link](dlg,text="Good Parts Count:").grid(row=1,column=0,padx=5,pady=5)
good= [Link](0); [Link](dlg,textvariable=good).grid(row=1,column=1)
def add():
self.total_parts .set([Link]())
self.good_parts .set([Link]())
# now that counts are in, compute & trend
ts = [Link]()
self.compute_oee()
self.oee_history.append((ts,[Link]))
if len(self.oee_history)>30: self.oee_history.pop(0)
self.draw_trend()
[Link]()
[Link](dlg,text="Submit", command=add).grid(row=2,column=0,pady=10)
[Link](dlg,text="Cancel", command=[Link]).grid(row=2,column=1,pady=10)
def poll_gpio(self):
start = [Link](CYCLE_START_PIN)==[Link]
complete= [Link](CYCLE_COMPLETE_PIN)==[Link]
emer = [Link](EMERGENCY_STOP_PIN)==[Link]
fault = [Link](MACHINE_FAULT_PIN)==[Link]
breakdown = emer or fault
# START detection (falling edge)
if not self.cycle_in_progress and start and self.prev_start:
self.cycle_in_progress = True
self.cycle_start_time = [Link]()
self.prev_start = start
# BREAKDOWN detection
if self.cycle_in_progress and breakdown and not self.prev_break:
self.cycle_in_progress = False
dur = ([Link]() - self.cycle_start_time)/60.0
cat = "Emergency Stop" if emer else "Machine Breakdown"
self.open_breakdown_dialog(cat, dur)
self.prev_break = breakdown
# NORMAL complete
if self.cycle_in_progress and complete:
self.cycle_in_progress = False
self.actual_cycle_time = ([Link]() - self.cycle_start_time)/60.0
# update UI display of cycle time
self.lbl_act.config(text=f"Actual Cycle Time: {self.actual_cycle_time:.2f} min")
# then ask parts count
self.open_post_cycle_dialog()
RESTRICTED
# Update tower lamp
self.update_lamp(emer, fault, self.cycle_in_progress, complete)
# repeat
[Link](200, self.poll_gpio)
def update_lamp(self, emer, fault, running, complete):
self.l_red .config(bg="red" if emer else "gray")
self.l_amber.config(bg="orange" if fault else "gray")
self.l_green.config(bg="green" if running else "gray")
self.l_blue .config(bg="blue" if complete and not running else "gray")
def compute_oee(self):
total_losses = sum(self.loss_totals.values())
A = calc_availability(self.planned_time.get(), total_losses)
P = calc_performance(self.ideal_cycle.get(), self.actual_cycle_time)
Q = calc_quality(self.good_parts.get(), self.total_parts.get())
[Link] = calc_oee(A,P,Q)
# update labels
self.lbl_oee .config(text=f"OEE: {[Link]:.1f}%")
self.lbl_avail.config(text=f"Availability: {A*100:.1f}%")
self.lbl_perf .config(text=f"Performance: {P*100:.1f}%")
self.lbl_qual .config(text=f"Quality: {Q*100:.1f}%")
def draw_trend(self):
days = [[Link] for ts,_ in self.oee_history]
vals = [v for _,v in self.oee_history]
[Link]()
[Link].set_ylim(0,100)
[Link].set_xlabel("Day of Month")
[Link].set_ylabel("OEE (%)")
[Link].set_title("OEE Trend")
[Link](days, vals, marker="o", color="blue")
[Link].set_xticks(list(range(1,32)))
[Link]()
def generate_report(self):
now = [Link]()
row = [
[Link]("%Y-%m-%d %H:%M:%S"),
round([Link],1),
round(calc_availability(self.planned_time.get(), sum(self.loss_totals.values()))*100,1),
round(calc_performance(self.ideal_cycle.get(),self.actual_cycle_time)*100,1),
RESTRICTED
round(calc_quality(self.good_parts.get(), self.total_parts.get())*100,1),
[Link],
round(self.actual_cycle_time,2),
*[round(self.loss_totals[cat],2) for cat in LOSS_CATEGORIES]
]
log_oee_summary(row)
log_stoppage_entries(self.stoppage_log)
[Link]("Report","Excel report updated.")
def cleanup(self):
[Link]()
# =========================
# Run
# =========================
if _name=="main_":
root = [Link]()
app = OEEApp(root)
def on_close():
if [Link]("Quit","Exit?"):
[Link]()
[Link]()
[Link]("WM_DELETE_WINDOW", on_close)
[Link]()