Last active 1 month ago

Revision c9b143b43e846124c67c8e4809d512ff25025e3f

RunoutTime.py Raw
1import tkinter as tk
2from tkinter import ttk
3
4UPDATE_MS = 1000 # 1 second
5
6
7class Station:
8 def __init__(self, name, max_qty, rate_per_min, refill_amount):
9 self.name = name
10 self.max_qty = max_qty
11 self.current_qty = max_qty
12 self.rate_per_min = rate_per_min
13 self.refill_amount = refill_amount
14
15 def reset(self):
16 self.current_qty = self.max_qty
17
18 def add(self):
19 self.current_qty = min(
20 self.max_qty,
21 self.current_qty + self.refill_amount
22 )
23
24 @property
25 def minutes_remaining(self):
26 if self.rate_per_min <= 0:
27 return 9999
28 return self.current_qty / self.rate_per_min
29
30
31class RunoutSimulator:
32
33 def __init__(self, root):
34 self.root = root
35 self.root.title("Runout Simulator")
36
37 self.running = False
38 self.speed_factor = 1.0
39 self.base_line_speed = 60 # devices/min
40
41 self.stations = [
42 Station("Tray Infeed", 135, 11, 27),
43 Station("Tray Outfeed", 135, 11, 27),
44 Station("IFU Loader", 2000, 60, 100),
45 Station("Carton Magazine", 1000, 60, 20),
46 Station("Case Magazine", 100, 3, 10),
47 ]
48
49 self.pallet_capacity = 60
50 self.pallet_cases = 0
51 self.case_rate = 3
52
53 self.station_widgets = []
54
55 self.build_ui()
56 self.update_display()
57
58 def build_ui(self):
59
60 controls = tk.Frame(self.root)
61 controls.pack(fill="x", padx=10, pady=5)
62
63 tk.Button(
64 controls,
65 text="Start",
66 command=self.start
67 ).pack(side="left")
68
69 tk.Button(
70 controls,
71 text="Pause",
72 command=self.pause
73 ).pack(side="left", padx=5)
74
75 tk.Button(
76 controls,
77 text="Reset",
78 command=self.reset
79 ).pack(side="left")
80
81 tk.Button(
82 controls,
83 text="Speed +10%",
84 command=self.speed_up
85 ).pack(side="left", padx=20)
86
87 tk.Button(
88 controls,
89 text="Speed -10%",
90 command=self.speed_down
91 ).pack(side="left")
92
93 self.speed_label = tk.Label(
94 controls,
95 text="Line Speed: 100%"
96 )
97 self.speed_label.pack(side="left", padx=20)
98
99 for station in self.stations:
100
101 frame = tk.LabelFrame(
102 self.root,
103 text=station.name,
104 padx=5,
105 pady=5
106 )
107 frame.pack(
108 fill="x",
109 padx=10,
110 pady=5
111 )
112
113 bar = ttk.Progressbar(
114 frame,
115 maximum=station.max_qty,
116 )
117 bar.pack(fill="x", expand=True)
118
119 qty_label = tk.Label(frame)
120 qty_label.pack(anchor="w")
121
122 time_label = tk.Label(frame)
123 time_label.pack(anchor="w")
124
125 refill_btn = tk.Button(
126 frame,
127 text=f"+{station.refill_amount}",
128 command=station.add
129 )
130 refill_btn.pack(anchor="e")
131
132 self.station_widgets.append(
133 (bar, qty_label, time_label)
134 )
135
136 pallet_frame = tk.LabelFrame(
137 self.root,
138 text="Finished Pallet",
139 padx=5,
140 pady=5
141 )
142 pallet_frame.pack(fill="x", padx=10, pady=5)
143
144 self.pallet_bar = ttk.Progressbar(
145 pallet_frame,
146 maximum=self.pallet_capacity,
147 )
148 self.pallet_bar.pack(fill="x", expand=True)
149
150 self.pallet_label = tk.Label(pallet_frame)
151 self.pallet_label.pack(anchor="w")
152
153 self.pallet_button = tk.Button(
154 pallet_frame,
155 text="Replace Full Pallet",
156 command=self.replace_pallet
157 )
158 self.pallet_button.pack(anchor="e")
159
160 def start(self):
161 if not self.running:
162 self.running = True
163 self.tick()
164
165 def pause(self):
166 self.running = False
167
168 def reset(self):
169 self.running = False
170
171 for station in self.stations:
172 station.reset()
173
174 self.pallet_cases = 0
175
176 self.speed_factor = 1.0
177 self.update_display()
178
179 def speed_up(self):
180 self.speed_factor *= 1.10
181 self.update_display()
182
183 def speed_down(self):
184 self.speed_factor *= 0.90
185 self.update_display()
186
187 def replace_pallet(self):
188 if self.pallet_cases >= self.pallet_capacity:
189 self.pallet_cases = 0
190
191 def tick(self):
192
193 if not self.running:
194 return
195
196 for station in self.stations:
197
198 usage_per_second = (
199 station.rate_per_min
200 * self.speed_factor
201 / 60
202 )
203
204 station.current_qty = max(
205 0,
206 station.current_qty - usage_per_second
207 )
208
209 self.pallet_cases += (self.case_rate * self.speed_factor / 60)
210
211 if self.pallet_cases > self.pallet_capacity:
212 self.pallet_cases = self.pallet_capacity
213
214 self.update_display()
215
216 self.root.after(
217 UPDATE_MS,
218 self.tick
219 )
220
221
222 def update_display(self):
223
224 for station, widgets in zip(
225 self.stations,
226 self.station_widgets):
227
228 bar, qty_label, time_label = widgets
229
230 bar["value"] = station.current_qty
231
232 qty_label.config(
233 text=f"Qty: {station.current_qty:.1f}/{station.max_qty}"
234 )
235
236 time_label.config(
237 text=f"Remaining: {station.minutes_remaining:.1f} min"
238 )
239
240 current_speed = self.base_line_speed * self.speed_factor
241
242 self.speed_label.config(
243 text=f"Line Speed: {current_speed:.0f} devices/min "
244 f"({self.speed_factor * 100:.0f}%)"
245 )
246
247 self.pallet_bar["value"] = self.pallet_cases
248
249 self.pallet_label.config(
250 text=f"{self.pallet_cases:.1f} / "
251 f"{self.pallet_capacity} cases"
252 )
253
254 if self.pallet_cases >= self.pallet_capacity:
255 self.pallet_button.config(state="normal")
256 else:
257 self.pallet_button.config(state="disabled")
258
259
260root = tk.Tk()
261app = RunoutSimulator(root)
262root.mainloop()
RunoutTime2.py Raw
1import tkinter as tk
2from tkinter import ttk
3
4UPDATE_MS = 1000 # 1 second
5
6TRAY_SIZE = 27
7MAX_INFEED_TRAYS = 50
8MAX_OUTFEED_TRAYS = 50
9
10START_CARTONS = 1000
11START_IFUS = 2000
12
13PALLET_CAPACITY = 60
14
15
16class RunoutSimulator:
17
18 def __init__(self, root):
19 self.root = root
20 self.root.title("Runout Simulator")
21
22 self.running = False
23
24 self.base_line_speed = 60 # devices/min
25 self.speed_factor = 1.0
26 self.device_credit = 0.0
27
28 self.mode = tk.StringVar(value="4-Pack")
29
30 self.reset_values()
31
32 self.build_ui()
33 self.update_display()
34
35 def reset_values(self):
36 self.infeed_trays = [TRAY_SIZE] * MAX_INFEED_TRAYS
37 self.outfeed_trays = 0
38
39 self.cartons = START_CARTONS
40 self.ifus = START_IFUS
41
42 self.current_carton_devices = 0
43 self.current_case_cartons = 0
44 self.pallet_cases = 0
45
46 self.device_credit = 0.0
47 self.status = "Ready"
48
49 def devices_per_carton(self):
50 if self.mode.get() == "4-Pack":
51 return 4
52 return 1
53
54 def cartons_per_case(self):
55 if self.mode.get() == "4-Pack":
56 return 18
57 return 40
58
59 def build_ui(self):
60
61 controls = tk.Frame(self.root)
62 controls.pack(fill="x", padx=10, pady=5)
63
64 tk.Button(
65 controls,
66 text="Start",
67 command=self.start
68 ).pack(side="left")
69
70 tk.Button(
71 controls,
72 text="Pause",
73 command=self.pause
74 ).pack(side="left", padx=5)
75
76 tk.Button(
77 controls,
78 text="Reset",
79 command=self.reset
80 ).pack(side="left")
81
82 tk.Button(
83 controls,
84 text="Speed +10%",
85 command=self.speed_up
86 ).pack(side="left", padx=20)
87
88 tk.Button(
89 controls,
90 text="Speed -10%",
91 command=self.speed_down
92 ).pack(side="left")
93
94 self.speed_label = tk.Label(controls)
95 self.speed_label.pack(side="left", padx=20)
96
97 mode_frame = tk.Frame(self.root)
98 mode_frame.pack(fill="x", padx=10, pady=5)
99
100 tk.Label(
101 mode_frame,
102 text="Mode:"
103 ).pack(side="left")
104
105 tk.Radiobutton(
106 mode_frame,
107 text="4-Pack",
108 variable=self.mode,
109 value="4-Pack",
110 command=self.change_mode
111 ).pack(side="left")
112
113 tk.Radiobutton(
114 mode_frame,
115 text="1-Pack",
116 variable=self.mode,
117 value="1-Pack",
118 command=self.change_mode
119 ).pack(side="left")
120
121 # Infeed
122 self.infeed_frame = tk.LabelFrame(
123 self.root,
124 text="Device Infeed",
125 padx=5,
126 pady=5
127 )
128 self.infeed_frame.pack(fill="x", padx=10, pady=5)
129
130 self.infeed_bar = ttk.Progressbar(
131 self.infeed_frame,
132 maximum=MAX_INFEED_TRAYS * TRAY_SIZE
133 )
134 self.infeed_bar.pack(fill="x", expand=True)
135
136 self.infeed_label = tk.Label(self.infeed_frame)
137 self.infeed_label.pack(anchor="w")
138
139 tk.Button(
140 self.infeed_frame,
141 text="+5 Trays",
142 command=self.add_infeed_trays
143 ).pack(anchor="e")
144
145 # Outfeed
146 self.outfeed_frame = tk.LabelFrame(
147 self.root,
148 text="Tray Outfeed",
149 padx=5,
150 pady=5
151 )
152 self.outfeed_frame.pack(fill="x", padx=10, pady=5)
153
154 self.outfeed_bar = ttk.Progressbar(
155 self.outfeed_frame,
156 maximum=MAX_OUTFEED_TRAYS
157 )
158 self.outfeed_bar.pack(fill="x", expand=True)
159
160 self.outfeed_label = tk.Label(self.outfeed_frame)
161 self.outfeed_label.pack(anchor="w")
162
163 tk.Button(
164 self.outfeed_frame,
165 text="Clear Outfeed",
166 command=self.clear_outfeed
167 ).pack(anchor="e")
168
169 # Cartons
170 self.carton_frame = tk.LabelFrame(
171 self.root,
172 text="Carton Magazine",
173 padx=5,
174 pady=5
175 )
176 self.carton_frame.pack(fill="x", padx=10, pady=5)
177
178 self.carton_bar = ttk.Progressbar(
179 self.carton_frame,
180 maximum=START_CARTONS
181 )
182 self.carton_bar.pack(fill="x", expand=True)
183
184 self.carton_label = tk.Label(self.carton_frame)
185 self.carton_label.pack(anchor="w")
186
187 tk.Button(
188 self.carton_frame,
189 text="+20 Cartons",
190 command=self.add_cartons
191 ).pack(anchor="e")
192
193 # IFUs
194 self.ifu_frame = tk.LabelFrame(
195 self.root,
196 text="IFU Loader",
197 padx=5,
198 pady=5
199 )
200 self.ifu_frame.pack(fill="x", padx=10, pady=5)
201
202 self.ifu_bar = ttk.Progressbar(
203 self.ifu_frame,
204 maximum=START_IFUS
205 )
206 self.ifu_bar.pack(fill="x", expand=True)
207
208 self.ifu_label = tk.Label(self.ifu_frame)
209 self.ifu_label.pack(anchor="w")
210
211 tk.Button(
212 self.ifu_frame,
213 text="+100 IFUs",
214 command=self.add_ifus
215 ).pack(anchor="e")
216
217 # Current carton
218 self.current_carton_frame = tk.LabelFrame(
219 self.root,
220 text="Current Carton",
221 padx=5,
222 pady=5
223 )
224 self.current_carton_frame.pack(fill="x", padx=10, pady=5)
225
226 self.current_carton_bar = ttk.Progressbar(
227 self.current_carton_frame
228 )
229 self.current_carton_bar.pack(fill="x", expand=True)
230
231 self.current_carton_label = tk.Label(self.current_carton_frame)
232 self.current_carton_label.pack(anchor="w")
233
234 # Current case
235 self.current_case_frame = tk.LabelFrame(
236 self.root,
237 text="Current Case",
238 padx=5,
239 pady=5
240 )
241 self.current_case_frame.pack(fill="x", padx=10, pady=5)
242
243 self.current_case_bar = ttk.Progressbar(
244 self.current_case_frame
245 )
246 self.current_case_bar.pack(fill="x", expand=True)
247
248 self.current_case_label = tk.Label(self.current_case_frame)
249 self.current_case_label.pack(anchor="w")
250
251 # Pallet
252 self.pallet_frame = tk.LabelFrame(
253 self.root,
254 text="Finished Pallet",
255 padx=5,
256 pady=5
257 )
258 self.pallet_frame.pack(fill="x", padx=10, pady=5)
259
260 self.pallet_bar = ttk.Progressbar(
261 self.pallet_frame,
262 maximum=PALLET_CAPACITY
263 )
264 self.pallet_bar.pack(fill="x", expand=True)
265
266 self.pallet_label = tk.Label(self.pallet_frame)
267 self.pallet_label.pack(anchor="w")
268
269 self.pallet_button = tk.Button(
270 self.pallet_frame,
271 text="Replace Full Pallet",
272 command=self.replace_pallet
273 )
274 self.pallet_button.pack(anchor="e")
275
276 # Status
277 self.status_label = tk.Label(
278 self.root,
279 font=("Arial", 11, "bold")
280 )
281 self.status_label.pack(anchor="w", padx=10, pady=10)
282
283 def start(self):
284 if not self.running:
285 self.running = True
286 self.status = "Running"
287 self.tick()
288
289 def pause(self):
290 self.running = False
291 self.status = "Paused"
292 self.update_display()
293
294 def reset(self):
295 self.running = False
296 self.speed_factor = 1.0
297 self.reset_values()
298 self.update_display()
299
300 def change_mode(self):
301 self.running = False
302 self.speed_factor = 1.0
303 self.reset_values()
304 self.update_display()
305
306 def speed_up(self):
307 self.speed_factor *= 1.10
308 self.update_display()
309
310 def speed_down(self):
311 self.speed_factor *= 0.90
312 self.update_display()
313
314 def add_infeed_trays(self):
315 space_available = MAX_INFEED_TRAYS - len(self.infeed_trays)
316 trays_to_add = min(5, space_available)
317
318 for _ in range(trays_to_add):
319 self.infeed_trays.append(TRAY_SIZE)
320
321 self.update_display()
322
323 def clear_outfeed(self):
324 self.outfeed_trays = 0
325 self.update_display()
326
327 def add_cartons(self):
328 self.cartons = min(START_CARTONS, self.cartons + 20)
329 self.update_display()
330
331 def add_ifus(self):
332 self.ifus = min(START_IFUS, self.ifus + 100)
333 self.update_display()
334
335 def replace_pallet(self):
336 if self.pallet_cases >= PALLET_CAPACITY:
337 self.pallet_cases = 0
338 self.status = "Ready"
339 self.update_display()
340
341 def tick(self):
342
343 if not self.running:
344 return
345
346 current_speed = self.base_line_speed * self.speed_factor
347 self.device_credit += current_speed / 60
348
349 while self.device_credit >= 1 and self.running:
350 success = self.process_one_device()
351
352 if success:
353 self.device_credit -= 1
354 else:
355 self.running = False
356 self.device_credit = 0
357
358 self.update_display()
359
360 if self.running:
361 self.root.after(UPDATE_MS, self.tick)
362
363 def process_one_device(self):
364
365 if len(self.infeed_trays) == 0:
366 self.status = "Stopped - No infeed trays"
367 return False
368
369 if self.pallet_cases >= PALLET_CAPACITY:
370 self.status = "Stopped - Pallet full"
371 return False
372
373 active_tray_devices = self.infeed_trays[0]
374
375 if active_tray_devices == 1 and self.outfeed_trays >= MAX_OUTFEED_TRAYS:
376 self.status = "Stopped - Tray outfeed full"
377 return False
378
379 will_complete_carton = (
380 self.current_carton_devices + 1 >= self.devices_per_carton()
381 )
382
383 if will_complete_carton:
384 if self.cartons <= 0:
385 self.status = "Stopped - No cartons"
386 return False
387
388 if self.ifus <= 0:
389 self.status = "Stopped - No IFUs"
390 return False
391
392 # Remove one device from the current tray
393 self.infeed_trays[0] -= 1
394
395 # Add device to current carton
396 self.current_carton_devices += 1
397
398 # If tray is empty, move it to outfeed
399 if self.infeed_trays[0] == 0:
400 self.infeed_trays.pop(0)
401 self.outfeed_trays += 1
402
403 # If carton is complete, consume 1 carton and 1 IFU
404 if self.current_carton_devices >= self.devices_per_carton():
405 self.current_carton_devices = 0
406 self.cartons -= 1
407 self.ifus -= 1
408
409 self.current_case_cartons += 1
410
411 # If case is complete, move 1 case to pallet
412 if self.current_case_cartons >= self.cartons_per_case():
413 self.current_case_cartons = 0
414 self.pallet_cases += 1
415
416 if self.pallet_cases >= PALLET_CAPACITY:
417 self.pallet_cases = PALLET_CAPACITY
418 self.status = "Stopped - Pallet full"
419 return False
420
421 self.status = "Running"
422 return True
423
424 def update_display(self):
425
426 current_speed = self.base_line_speed * self.speed_factor
427
428 self.speed_label.config(
429 text=f"Line Speed: {current_speed:.0f} devices/min "
430 f"({self.speed_factor * 100:.0f}%)"
431 )
432
433 total_infeed_devices = sum(self.infeed_trays)
434 self.infeed_bar["value"] = total_infeed_devices
435
436 if len(self.infeed_trays) > 0:
437 first_tray_text = f"Current tray: {self.infeed_trays[0]} / {TRAY_SIZE} devices"
438 else:
439 first_tray_text = "Current tray: none"
440
441 self.infeed_label.config(
442 text=f"{len(self.infeed_trays)} trays, "
443 f"{total_infeed_devices} devices remaining\n"
444 f"{first_tray_text}"
445 )
446
447 self.outfeed_bar["value"] = self.outfeed_trays
448 self.outfeed_label.config(
449 text=f"{self.outfeed_trays} / {MAX_OUTFEED_TRAYS} empty trays"
450 )
451
452 self.carton_bar["value"] = self.cartons
453 self.carton_label.config(
454 text=f"{self.cartons} / {START_CARTONS} cartons"
455 )
456
457 self.ifu_bar["value"] = self.ifus
458 self.ifu_label.config(
459 text=f"{self.ifus} / {START_IFUS} IFUs"
460 )
461
462 self.current_carton_bar["maximum"] = self.devices_per_carton()
463 self.current_carton_bar["value"] = self.current_carton_devices
464 self.current_carton_label.config(
465 text=f"{self.current_carton_devices} / "
466 f"{self.devices_per_carton()} devices"
467 )
468
469 self.current_case_bar["maximum"] = self.cartons_per_case()
470 self.current_case_bar["value"] = self.current_case_cartons
471 self.current_case_label.config(
472 text=f"{self.current_case_cartons} / "
473 f"{self.cartons_per_case()} cartons"
474 )
475
476 self.pallet_bar["value"] = self.pallet_cases
477 self.pallet_label.config(
478 text=f"{self.pallet_cases} / {PALLET_CAPACITY} cases"
479 )
480
481 if self.pallet_cases >= PALLET_CAPACITY:
482 self.pallet_button.config(state="normal")
483 else:
484 self.pallet_button.config(state="disabled")
485
486 self.status_label.config(
487 text=f"Status: {self.status}"
488 )
489
490
491root = tk.Tk()
492app = RunoutSimulator(root)
493root.mainloop()