import tkinter as tk from tkinter import ttk UPDATE_MS = 1000 # 1 second TRAY_SIZE = 27 MAX_INFEED_TRAYS = 50 MAX_OUTFEED_TRAYS = 50 START_CARTONS = 1000 START_IFUS = 2000 START_CASES = 100 PALLET_CAPACITY = 60 class RunoutSimulator: def __init__(self, root): self.root = root self.root.title("Runout Simulator") self.running = False self.base_line_speed = 60 # devices/min self.speed_factor = 1.0 self.device_credit = 0.0 self.mode = tk.StringVar(value="4-Pack") self.reset_values() self.build_ui() self.update_display() def reset_values(self): self.infeed_trays = [TRAY_SIZE] * MAX_INFEED_TRAYS self.outfeed_trays = 0 self.cartons = START_CARTONS self.ifus = START_IFUS self.cases = START_CASES self.current_carton_devices = 0 self.current_case_cartons = 0 self.pallet_cases = 0 self.device_credit = 0.0 self.status = "Ready" def devices_per_carton(self): if self.mode.get() == "4-Pack": return 4 return 1 def cartons_per_case(self): if self.mode.get() == "4-Pack": return 18 return 40 def build_ui(self): controls = tk.Frame(self.root) controls.pack(fill="x", padx=10, pady=5) tk.Button( controls, text="Start", command=self.start ).pack(side="left") tk.Button( controls, text="Pause", command=self.pause ).pack(side="left", padx=5) tk.Button( controls, text="Reset", command=self.reset ).pack(side="left") tk.Button( controls, text="Speed +10%", command=self.speed_up ).pack(side="left", padx=20) tk.Button( controls, text="Speed -10%", command=self.speed_down ).pack(side="left") self.speed_label = tk.Label(controls) self.speed_label.pack(side="left", padx=20) mode_frame = tk.Frame(self.root) mode_frame.pack(fill="x", padx=10, pady=5) tk.Label( mode_frame, text="Mode:" ).pack(side="left") tk.Radiobutton( mode_frame, text="4-Pack", variable=self.mode, value="4-Pack", command=self.change_mode ).pack(side="left") tk.Radiobutton( mode_frame, text="1-Pack", variable=self.mode, value="1-Pack", command=self.change_mode ).pack(side="left") # Infeed self.infeed_frame = tk.LabelFrame( self.root, text="Device Infeed", padx=5, pady=5 ) self.infeed_frame.pack(fill="x", padx=10, pady=5) self.infeed_bar = ttk.Progressbar( self.infeed_frame, maximum=MAX_INFEED_TRAYS * TRAY_SIZE ) self.infeed_bar.pack(fill="x", expand=True) self.infeed_label = tk.Label(self.infeed_frame) self.infeed_label.pack(anchor="w") tk.Button( self.infeed_frame, text="+5 Trays", command=self.add_infeed_trays ).pack(anchor="e") # Outfeed self.outfeed_frame = tk.LabelFrame( self.root, text="Tray Outfeed", padx=5, pady=5 ) self.outfeed_frame.pack(fill="x", padx=10, pady=5) self.outfeed_bar = ttk.Progressbar( self.outfeed_frame, maximum=MAX_OUTFEED_TRAYS ) self.outfeed_bar.pack(fill="x", expand=True) self.outfeed_label = tk.Label(self.outfeed_frame) self.outfeed_label.pack(anchor="w") tk.Button( self.outfeed_frame, text="Clear Outfeed", command=self.clear_outfeed ).pack(anchor="e") # Cartons self.carton_frame = tk.LabelFrame( self.root, text="Carton Magazine", padx=5, pady=5 ) self.carton_frame.pack(fill="x", padx=10, pady=5) self.carton_bar = ttk.Progressbar( self.carton_frame, maximum=START_CARTONS ) self.carton_bar.pack(fill="x", expand=True) self.carton_label = tk.Label(self.carton_frame) self.carton_label.pack(anchor="w") tk.Button( self.carton_frame, text="+20 Cartons", command=self.add_cartons ).pack(anchor="e") # IFUs self.ifu_frame = tk.LabelFrame( self.root, text="IFU Loader", padx=5, pady=5 ) self.ifu_frame.pack(fill="x", padx=10, pady=5) self.ifu_bar = ttk.Progressbar( self.ifu_frame, maximum=START_IFUS ) self.ifu_bar.pack(fill="x", expand=True) self.ifu_label = tk.Label(self.ifu_frame) self.ifu_label.pack(anchor="w") tk.Button( self.ifu_frame, text="+100 IFUs", command=self.add_ifus ).pack(anchor="e") # Case Supply self.case_supply_frame = tk.LabelFrame( self.root, text="Case Magazine", padx=5, pady=5 ) self.case_supply_frame.pack(fill="x", padx=10, pady=5) self.case_supply_bar = ttk.Progressbar( self.case_supply_frame, maximum=START_CASES ) self.case_supply_bar.pack(fill="x", expand = True) self.case_supply_label = tk.Label(self.case_supply_frame) self.case_supply_label.pack(anchor="w") tk.Button( self.case_supply_frame, text=f"10 Cases", command=self.add_cases ).pack(anchor="e") # Current carton self.current_carton_frame = tk.LabelFrame( self.root, text="Current Carton", padx=5, pady=5 ) self.current_carton_frame.pack(fill="x", padx=10, pady=5) self.current_carton_bar = ttk.Progressbar( self.current_carton_frame ) self.current_carton_bar.pack(fill="x", expand=True) self.current_carton_label = tk.Label(self.current_carton_frame) self.current_carton_label.pack(anchor="w") # Current case self.current_case_frame = tk.LabelFrame( self.root, text="Current Case", padx=5, pady=5 ) self.current_case_frame.pack(fill="x", padx=10, pady=5) self.current_case_bar = ttk.Progressbar( self.current_case_frame ) self.current_case_bar.pack(fill="x", expand=True) self.current_case_label = tk.Label(self.current_case_frame) self.current_case_label.pack(anchor="w") # Pallet self.pallet_frame = tk.LabelFrame( self.root, text="Finished Pallet", padx=5, pady=5 ) self.pallet_frame.pack(fill="x", padx=10, pady=5) self.pallet_bar = ttk.Progressbar( self.pallet_frame, maximum=PALLET_CAPACITY ) self.pallet_bar.pack(fill="x", expand=True) self.pallet_label = tk.Label(self.pallet_frame) self.pallet_label.pack(anchor="w") self.pallet_button = tk.Button( self.pallet_frame, text="Replace Full Pallet", command=self.replace_pallet ) self.pallet_button.pack(anchor="e") # Status self.status_label = tk.Label( self.root, font=("Arial", 11, "bold") ) self.status_label.pack(anchor="w", padx=10, pady=10) def start(self): if not self.running: self.running = True self.status = "Running" self.tick() def pause(self): self.running = False self.status = "Paused" self.update_display() def reset(self): self.running = False self.speed_factor = 1.0 self.reset_values() self.update_display() def change_mode(self): self.running = False self.speed_factor = 1.0 self.reset_values() self.update_display() def speed_up(self): self.speed_factor *= 1.10 self.update_display() def speed_down(self): self.speed_factor *= 0.90 self.update_display() def add_infeed_trays(self): space_available = MAX_INFEED_TRAYS - len(self.infeed_trays) trays_to_add = min(5, space_available) for _ in range(trays_to_add): self.infeed_trays.append(TRAY_SIZE) self.update_display() def clear_outfeed(self): self.outfeed_trays = 0 self.update_display() def add_cartons(self): self.cartons = min(START_CARTONS, self.cartons + 20) self.update_display() def add_ifus(self): self.ifus = min(START_IFUS, self.ifus + 100) self.update_display() def add_cases(self): self.cases = min(START_CASES, self.cases + 10) self.update_display() def replace_pallet(self): if self.pallet_cases >= PALLET_CAPACITY: self.pallet_cases = 0 self.status = "Ready" self.update_display() def tick(self): if not self.running: return current_speed = self.base_line_speed * self.speed_factor self.device_credit += current_speed / 60 while self.device_credit >= 1 and self.running: success = self.process_one_device() if success: self.device_credit -= 1 else: self.running = False self.device_credit = 0 self.update_display() if self.running: self.root.after(UPDATE_MS, self.tick) def process_one_device(self): if len(self.infeed_trays) == 0: self.status = "Stopped - No infeed trays" return False if self.pallet_cases >= PALLET_CAPACITY: self.status = "Stopped - Pallet full" return False active_tray_devices = self.infeed_trays[0] if active_tray_devices == 1 and self.outfeed_trays >= MAX_OUTFEED_TRAYS: self.status = "Stopped - Tray outfeed full" return False will_complete_carton = ( self.current_carton_devices + 1 >= self.devices_per_carton() ) if will_complete_carton: if self.cartons <= 0: self.status = "Stopped - No cartons" return False if self.ifus <= 0: self.status = "Stopped - No IFUs" return False # Remove one device from the current tray self.infeed_trays[0] -= 1 # Add device to current carton self.current_carton_devices += 1 # If tray is empty, move it to outfeed if self.infeed_trays[0] == 0: self.infeed_trays.pop(0) self.outfeed_trays += 1 # If carton is complete, consume 1 carton and 1 IFU if self.current_carton_devices >= self.devices_per_carton(): self.current_carton_devices = 0 self.cartons -= 1 self.ifus -= 1 self.current_case_cartons += 1 # If case is complete, move 1 case to pallet if self.current_case_cartons >= self.cartons_per_case(): if self.cases <= 0: self.status = "Stopped - No cases" return False self.current_case_cartons = 0 self.cases -= 1 self.pallet_cases += 1 if self.pallet_cases >= PALLET_CAPACITY: self.pallet_cases = PALLET_CAPACITY self.status = "Stopped - Pallet full" return False self.status = "Running" return True def update_display(self): current_speed = self.base_line_speed * self.speed_factor self.speed_label.config( text=f"Line Speed: {current_speed:.0f} devices/min " f"({self.speed_factor * 100:.0f}%)" ) total_infeed_devices = sum(self.infeed_trays) self.infeed_bar["value"] = total_infeed_devices if len(self.infeed_trays) > 0: first_tray_text = f"Current tray: {self.infeed_trays[0]} / {TRAY_SIZE} devices" else: first_tray_text = "Current tray: none" self.infeed_label.config( text=f"{len(self.infeed_trays)} trays, " f"{total_infeed_devices} devices remaining\n" f"{first_tray_text}" ) self.outfeed_bar["value"] = self.outfeed_trays self.outfeed_label.config( text=f"{self.outfeed_trays} / {MAX_OUTFEED_TRAYS} empty trays" ) self.carton_bar["value"] = self.cartons self.carton_label.config( text=f"{self.cartons} / {START_CARTONS} cartons" ) self.ifu_bar["value"] = self.ifus self.ifu_label.config( text=f"{self.ifus} / {START_IFUS} IFUs" ) self.case_supply_bar["value"] = self.cases self.case_supply_label.config( text=f"{self.cases} / {START_CASES} cases available" ) self.current_carton_bar["maximum"] = self.devices_per_carton() self.current_carton_bar["value"] = self.current_carton_devices self.current_carton_label.config( text=f"{self.current_carton_devices} / " f"{self.devices_per_carton()} devices" ) self.current_case_bar["maximum"] = self.cartons_per_case() self.current_case_bar["value"] = self.current_case_cartons self.current_case_label.config( text=f"{self.current_case_cartons} / " f"{self.cartons_per_case()} cartons" ) self.pallet_bar["value"] = self.pallet_cases self.pallet_label.config( text=f"{self.pallet_cases} / {PALLET_CAPACITY} cases" ) if self.pallet_cases >= PALLET_CAPACITY: self.pallet_button.config(state="normal") else: self.pallet_button.config(state="disabled") self.status_label.config( text=f"Status: {self.status}" ) root = tk.Tk() app = RunoutSimulator(root) root.mainloop()