Kaynağa Gözat

Changed factory layout, changed dso pickup interrupts, prepped RTsim for anomaly detection

anfeny 2 ay önce
ebeveyn
işleme
a246e66548

+ 26 - 0
simulator/devs_models/collector.py

@@ -1,5 +1,7 @@
+import time
 from pypdevs.DEVS import AtomicDEVS
 from loguru import logger
+from data_models.workpiece import Workpiece
 
 class Collector(AtomicDEVS):
     """A simple collector which holds every received item in a list in its state"""
@@ -21,4 +23,28 @@ class Collector(AtomicDEVS):
             new_item = inputs[self.inp]
             self.state["items"].append(new_item)
             logger.trace(f"{type(self).__name__} '{self.name}' received: {new_item}")
+        return self.state
+    
+
+    
+class WorkpieceCollector(AtomicDEVS):
+    """A Workpiece collector which collects workpieces, and keeps track of their arrival time (wall_time)"""
+    def __init__(self, name):
+        super(WorkpieceCollector, self).__init__(name)
+        self.inp = self.addInPort("inp")
+
+        # keep track of items received
+        self.state = {
+            "workpieces": [], # (wall_time, workpiece)
+        }
+
+    def get_workpieces(self) -> list[float, Workpiece]:
+        """ Get the generated workpieces """
+        return self.state["workpieces"]
+
+    def extTransition(self, inputs):
+        if self.inp in inputs:
+            new_item = inputs[self.inp]
+            self.state["workpieces"].append((time.time(), new_item))
+            logger.trace(f"{type(self).__name__} '{self.name}' received: {new_item}")
         return self.state

+ 7 - 4
simulator/devs_models/dso.py

@@ -36,6 +36,8 @@ class DSO(AtomicDEVS):
         self.mqtt_in = self.addInPort("mqtt_in") # to receive 'pickup' simulation message to clear output
         self.mqtt_out = self.addOutPort("mqtt_out")
 
+        self.req_pickup = self.addInPort("req_pickup") # to receive 'pickup' message to clear output
+
         self.state = DsoState()
 
     def change_phase(self, new_phase: DsoPhase):
@@ -64,7 +66,7 @@ class DSO(AtomicDEVS):
         # active = 1 if (self.state.workpiece is not None) else 0
         # code = 1 if (phase == DsoPhase.IDLE) else 0 # TODO: figure out this logic
         active = 1 if (phase == DsoPhase.WP_RECEIVED) else 0 # 1 if active
-        code = 0 if (self.state.workpiece) else 1 # 0 if workpiece
+        code = 1 if (phase == DsoPhase.IDLE) else 0 # 0 if workpiece
 
         message = MqttMessage()
         message.topic = "f/i/state/dso"
@@ -91,12 +93,13 @@ class DSO(AtomicDEVS):
 
         if self.mqtt_in in inputs:
             msg: MqttMessage = inputs[self.mqtt_in][0]
-            if msg.topic == "simulation/ctrl/dso" and msg.payload["action"] == "clear":
-                self.change_phase(DsoPhase.PICKUP)
-            elif msg.topic == "simulation/ctrl/all" and msg.payload.get("action") == "refresh":
+            if msg.topic == "simulation/ctrl/all" and msg.payload.get("action") == "refresh":
                 self.state.status_requested = True
                 self.state.visual_update_pending = True
                 return self.state
+        
+        if self.req_pickup in inputs:
+            self.change_phase(DsoPhase.PICKUP)
 
         return self.state # important, return state
 

+ 10 - 8
simulator/devs_models/fischertechnik_factory.py

@@ -1,6 +1,6 @@
 from pypdevs.DEVS import CoupledDEVS
 
-from devs_models.collector import Collector
+from devs_models.collector import WorkpieceCollector
 from devs_models.crate_transporter import CrateTransporter
 from devs_models.dsi import DSI
 from devs_models.dso import DSO
@@ -16,12 +16,14 @@ from devs_models.vacuum_gripper import VacuumGripper
 class FischertechnikFactory(CoupledDEVS):
     def __init__(self, name: str):
         super(FischertechnikFactory, self).__init__(name)
-        # Input port for workpieces
+        # In ports
         self.inp = self.addInPort('inp')
-        # Input for MQTT messages
-        self.mqtt_in = self.addInPort('mqtt_in')
-        # Add output port for MQTT
-        self.REALTIME_OBSERVED = self.addOutPort('REALTIME_OBSERVED')
+        self.mqtt_in = self.addInPort('mqtt_in') # Input for MQTT messages
+        self.req_pickup = self.addInPort('req_pickup') # Input port for pickup requests (clearing output)
+
+        # Out ports
+        self.out = self.addOutPort('out')  # Output port for picked up workpieces
+        self.REALTIME_OBSERVED = self.addOutPort('REALTIME_OBSERVED') # MQTT output for real-time observations
 
         # Create models
         self.dsi: DSI = self.addSubModel(DSI("DSI"))
@@ -35,10 +37,10 @@ class FischertechnikFactory(CoupledDEVS):
         self.sld: SLD = self.addSubModel(SLD("SLD"))
         self.dso: DSO = self.addSubModel(DSO("DSO"))
         self.mqtt_control: MQTTControlUnit = self.addSubModel(MQTTControlUnit("MQTTControlUnit"))
-        self.collector: Collector = self.addSubModel(Collector("Collector"))
 
         # Connect ports
         self.connectPorts(self.inp, self.dsi.inp)
+        self.connectPorts(self.req_pickup, self.dso.req_pickup)
         self.connectPorts(self.dsi.out, self.vgr.dsi_in)
         self.connectPorts(self.vgr.dsi_out, self.dsi.vgr_in)
         self.connectPorts(self.vgr.mpo_out, self.mpo.vgr_in)
@@ -60,7 +62,7 @@ class FischertechnikFactory(CoupledDEVS):
         self.connectPorts(self.vgr.sld_out["BLUE"], self.sld.blue_in)
         self.connectPorts(self.vgr.sld_out["WHITE"], self.sld.white_in)
         self.connectPorts(self.vgr.dso_out, self.dso.inp)
-        self.connectPorts(self.dso.out, self.collector.inp)
+        self.connectPorts(self.dso.out, self.out)
 
         # Connect MQTT ports
         self.connectPorts(self.mqtt_in, self.mqtt_control.REALTIME_INTERRUPT)

+ 5 - 1
simulator/devs_models/generator.py

@@ -10,7 +10,7 @@ from pypdevs.infinity import INFINITY
 class GeneratorInput:
     """ Class representing an item that the generator needs to generate."""
     time: float # generation time in seconds
-    item : Workpiece | MqttMessage
+    item : Workpiece | MqttMessage | list # list denotes pickup request
 
 
 @dataclass
@@ -25,6 +25,7 @@ class Generator(AtomicDEVS):
         # name needs to be unique to refer to it
         super(Generator, self).__init__(name)
         self.out = self.addOutPort("out") # out port for workpieces
+        self.req_pickup_out = self.addOutPort("req_pickup_out") # out port for pickup requests (empty list)
         self.mqtt_out = self.addOutPort("mqtt_out") # out port for MQTT messages
 
         self.state = GeneratorState(generation_queue=items_to_generate)
@@ -42,6 +43,9 @@ class Generator(AtomicDEVS):
         if isinstance(item.item, Workpiece):
             logger.trace(f"{type(self).__name__} '{self.name}' outputs workpiece: {item.item}")
             return {self.out: [item.item]}
+        elif isinstance(item.item, list):
+            logger.trace(f"{type(self).__name__} '{self.name}' outputs pickup request: {item.item}")
+            return {self.req_pickup_out: [item.item]}
         else: # MQTT
             logger.trace(f"{type(self).__name__} '{self.name}' outputs MQTT message: {item.item}")
             return {self.mqtt_out: [item.item]}

+ 6 - 3
simulator/fast_simulation.py

@@ -8,6 +8,7 @@ from pypdevs.simulator import Simulator
 from data_models.workpiece import Workpiece, WorkpieceColor
 from devs_models.fischertechnik_factory import FischertechnikFactory
 from devs_models.generator import Generator, GeneratorInput
+from devs_models.collector import WorkpieceCollector
 from utils.flowchart_generator import FlowchartGenerator
 
 
@@ -18,10 +19,12 @@ class FastSimModel(CoupledDEVS):
         super(FastSimModel, self).__init__(name)
         self.generator: Generator = self.addSubModel(Generator("Generator", items_to_generate))
         self.factory: FischertechnikFactory = self.addSubModel(FischertechnikFactory("FischertechnikFactory"))
+        self.collector: WorkpieceCollector = self.addSubModel(WorkpieceCollector("WorkpieceCollector"))
 
         self.connectPorts(self.generator.out, self.factory.inp)
+        self.connectPorts(self.generator.req_pickup_out, self.factory.req_pickup)
         self.connectPorts(self.generator.mqtt_out, self.factory.mqtt_in)
-
+        self.connectPorts(self.factory.out, self.collector.inp)
 
 if __name__ == "__main__":
     # Enable logging traces:
@@ -43,7 +46,7 @@ if __name__ == "__main__":
         GeneratorInput(time=0, item=Workpiece(color=WorkpieceColor.RED, id='1')),
         GeneratorInput(time=100, item=Workpiece(color=WorkpieceColor.BLUE, id='2')),
         GeneratorInput(time=150, item=order_string),  # TODO: change MQTT passthrough
-        GeneratorInput(time=330, item=clear_dso_string),
+        GeneratorInput(time=330, item=[]), # empty list denotes pickup request
         GeneratorInput(time=335, item=Workpiece(color=WorkpieceColor.WHITE, id='3')),
         GeneratorInput(time=420, item=Workpiece(color=WorkpieceColor.BLUE, id='4')),
         GeneratorInput(time=450, item=Workpiece(color=WorkpieceColor.WHITE, id='5')),
@@ -62,4 +65,4 @@ if __name__ == "__main__":
     logger.info("HBW inventory:")
     logger.info(sim.model.factory.get_inventory())
     logger.info("Collector contents:")
-    logger.info(sim.model.factory.collector.get_contents())
+    logger.info(sim.model.collector.get_workpieces())

+ 266 - 252
simulator/flowchart.md

@@ -1,266 +1,280 @@
 ```mermaid
 ---
-title: FischertechnikFactory
+title: RealTimeModel
 ---
 flowchart LR
-	subgraph DSI
-		DSI_out("out")
-		DSI_mqtt_in("mqtt_in")
-		DSI_mqtt_out("mqtt_out")
-		DSI_vgr_in("vgr_in")
-		DSI_inp("inp")
-		DSI_inp ~~~ DSI_out
-		DSI_mqtt_in ~~~ DSI_mqtt_out
-	end
-	DSI_out --> VacuumGripper_dsi_in
-	DSI_mqtt_out --> MQTTControlUnit_mqtt_in
-	subgraph ReaderStation
-		ReaderStation_color_out("color_out")
-		ReaderStation_nfc_out("nfc_out")
-		ReaderStation_mqtt_out("mqtt_out")
-		ReaderStation_color_in("color_in")
-		ReaderStation_nfc_in("nfc_in")
-		ReaderStation_mqtt_in("mqtt_in")
-		ReaderStation_nfc_in ~~~ ReaderStation_nfc_out
-		ReaderStation_color_in ~~~ ReaderStation_color_out
-		ReaderStation_mqtt_in ~~~ ReaderStation_mqtt_out
-	end
-	ReaderStation_nfc_out --> VacuumGripper_nfc_in
-	ReaderStation_color_out --> VacuumGripper_color_sensor_in
-	ReaderStation_mqtt_out --> MQTTControlUnit_mqtt_in
-	subgraph VacuumGripper
-		VacuumGripper_ct_out("ct_out")
-		VacuumGripper_ct_in("ct_in")
-		VacuumGripper_nfc_out("nfc_out")
-		VacuumGripper_color_sensor_out("color_sensor_out")
-		VacuumGripper_dsi_in("dsi_in")
-		VacuumGripper_mpo_out("mpo_out")
-		VacuumGripper_nfc_in("nfc_in")
-		VacuumGripper_sld_red_out("sld_red_out")
-		VacuumGripper_color_sensor_in("color_sensor_in")
-		VacuumGripper_sld_blue_out("sld_blue_out")
-		VacuumGripper_sld_red_in("sld_red_in")
-		VacuumGripper_sld_white_out("sld_white_out")
-		VacuumGripper_mqtt_out("mqtt_out")
-		VacuumGripper_sld_blue_in("sld_blue_in")
-		VacuumGripper_dso_out("dso_out")
-		VacuumGripper_sld_white_in("sld_white_in")
-		VacuumGripper_mqtt_in("mqtt_in")
-		VacuumGripper_dsi_out("dsi_out")
-		VacuumGripper_dsi_in ~~~ VacuumGripper_dsi_out
-		VacuumGripper_ct_in ~~~ VacuumGripper_ct_out
-		VacuumGripper_nfc_in ~~~ VacuumGripper_nfc_out
-		VacuumGripper_color_sensor_in ~~~ VacuumGripper_color_sensor_out
-		VacuumGripper_sld_red_in ~~~ VacuumGripper_mpo_out
-		VacuumGripper_sld_blue_in ~~~ VacuumGripper_sld_red_out
-		VacuumGripper_sld_white_in ~~~ VacuumGripper_sld_blue_out
-		VacuumGripper_mqtt_in ~~~ VacuumGripper_sld_white_out
-	end
-	VacuumGripper_dsi_out --> DSI_vgr_in
-	VacuumGripper_ct_out --> Transporter_right_in
-	VacuumGripper_nfc_out --> ReaderStation_nfc_in
-	VacuumGripper_color_sensor_out --> ReaderStation_color_in
-	VacuumGripper_mpo_out --> MPO_vgr_in
-	VacuumGripper_sld_red_out --> SLD_red_in
-	VacuumGripper_sld_blue_out --> SLD_blue_in
-	VacuumGripper_sld_white_out --> SLD_white_in
-	VacuumGripper_dso_out --> DSO_inp
-	VacuumGripper_mqtt_out --> MQTTControlUnit_mqtt_in
-	subgraph Transporter
-		Transporter_right_out("right_out")
-		Transporter_left_in("left_in")
-		Transporter_mqtt_in("mqtt_in")
-		Transporter_left_out("left_out")
-		Transporter_mqtt_out("mqtt_out")
-		Transporter_right_in("right_in")
-		Transporter_left_in ~~~ Transporter_right_out
-		Transporter_right_in ~~~ Transporter_left_out
-		Transporter_mqtt_in ~~~ Transporter_mqtt_out
-	end
-	Transporter_right_out --> VacuumGripper_ct_in
-	Transporter_left_out --> HighBayWarehouse_inp
-	Transporter_mqtt_out --> MQTTControlUnit_mqtt_in
-	subgraph HighBayWarehouse
-		HighBayWarehouse_inventory_out("inventory_out")
-		HighBayWarehouse_mqtt_out("mqtt_out")
-		HighBayWarehouse_inp("inp")
-		HighBayWarehouse_out("out")
-		HighBayWarehouse_mqtt_in("mqtt_in")
-		HighBayWarehouse_inp ~~~ HighBayWarehouse_out
-		HighBayWarehouse_mqtt_in ~~~ HighBayWarehouse_mqtt_out
-	end
-	HighBayWarehouse_out --> Transporter_left_in
-	HighBayWarehouse_mqtt_out --> MQTTControlUnit_mqtt_in
-	HighBayWarehouse_inventory_out --> InventoryPublisher_inp
-	subgraph InventoryPublisher
-		InventoryPublisher_mqtt_out("mqtt_out")
-		InventoryPublisher_inp("inp")
-		InventoryPublisher_inp ~~~ InventoryPublisher_mqtt_out
-	end
-	InventoryPublisher_mqtt_out --> MQTTControlUnit_mqtt_in
-	subgraph MPO
-		MPO_mqtt_in("mqtt_in")
-		MPO_conveyor_out("conveyor_out")
-		MPO_vgr_in("vgr_in")
-		MPO_mqtt_out("mqtt_out")
-		MPO_vgr_in ~~~ MPO_mqtt_out
-		MPO_mqtt_in ~~~ MPO_conveyor_out
-		subgraph MPO_oven
-			MPO_oven_mqtt_in("mqtt_in")
-			MPO_oven_gripper_out("gripper_out")
-			MPO_oven_gripper_in("gripper_in")
-			MPO_oven_vgr_in("vgr_in")
-			MPO_oven_mqtt_out("mqtt_out")
-			MPO_oven_vgr_in ~~~ MPO_oven_gripper_out
-			MPO_oven_gripper_in ~~~ MPO_oven_mqtt_out
+	subgraph FischertechnikFactory
+		FischertechnikFactory_out("out")
+		FischertechnikFactory_req_pickup("req_pickup")
+		FischertechnikFactory_REALTIME_OBSERVED("REALTIME_OBSERVED")
+		FischertechnikFactory_mqtt_in("mqtt_in")
+		FischertechnikFactory_inp("inp")
+		FischertechnikFactory_inp ~~~ FischertechnikFactory_out
+		FischertechnikFactory_mqtt_in ~~~ FischertechnikFactory_REALTIME_OBSERVED
+		subgraph DSI
+			DSI_vgr_in("vgr_in")
+			DSI_out("out")
+			DSI_inp("inp")
+			DSI_mqtt_out("mqtt_out")
+			DSI_mqtt_in("mqtt_in")
+			DSI_inp ~~~ DSI_out
+			DSI_mqtt_in ~~~ DSI_mqtt_out
 		end
-		MPO_oven_gripper_out --> MPO_gripper_oven_in
-		MPO_oven_mqtt_out --> MPO_mqtt_out
-		subgraph MPO_gripper
-			MPO_gripper_mqtt_in("mqtt_in")
-			MPO_gripper_table_out("table_out")
-			MPO_gripper_oven_in("oven_in")
-			MPO_gripper_table_in("table_in")
-			MPO_gripper_oven_out("oven_out")
-			MPO_gripper_mqtt_out("mqtt_out")
-			MPO_gripper_oven_in ~~~ MPO_gripper_oven_out
-			MPO_gripper_table_in ~~~ MPO_gripper_table_out
-			MPO_gripper_mqtt_in ~~~ MPO_gripper_mqtt_out
+		DSI_out --> VacuumGripper_dsi_in
+		DSI_mqtt_out --> MQTTControlUnit_mqtt_in
+		subgraph ReaderStation
+			ReaderStation_nfc_out("nfc_out")
+			ReaderStation_color_out("color_out")
+			ReaderStation_mqtt_out("mqtt_out")
+			ReaderStation_nfc_in("nfc_in")
+			ReaderStation_mqtt_in("mqtt_in")
+			ReaderStation_color_in("color_in")
+			ReaderStation_nfc_in ~~~ ReaderStation_nfc_out
+			ReaderStation_color_in ~~~ ReaderStation_color_out
+			ReaderStation_mqtt_in ~~~ ReaderStation_mqtt_out
 		end
-		MPO_gripper_oven_out --> MPO_oven_gripper_in
-		MPO_gripper_table_out --> MPO_saw_gripper_in
-		MPO_gripper_mqtt_out --> MPO_mqtt_out
-		subgraph MPO_saw
-			MPO_saw_gripper_in("gripper_in")
-			MPO_saw_mqtt_out("mqtt_out")
-			MPO_saw_mqtt_in("mqtt_in")
-			MPO_saw_conveyor_out("conveyor_out")
-			MPO_saw_gripper_out("gripper_out")
-			MPO_saw_gripper_in ~~~ MPO_saw_gripper_out
-			MPO_saw_mqtt_in ~~~ MPO_saw_conveyor_out
+		ReaderStation_nfc_out --> VacuumGripper_nfc_in
+		ReaderStation_color_out --> VacuumGripper_color_sensor_in
+		ReaderStation_mqtt_out --> MQTTControlUnit_mqtt_in
+		subgraph VacuumGripper
+			VacuumGripper_sld_blue_out("sld_blue_out")
+			VacuumGripper_sld_white_in("sld_white_in")
+			VacuumGripper_sld_blue_in("sld_blue_in")
+			VacuumGripper_dsi_in("dsi_in")
+			VacuumGripper_dsi_out("dsi_out")
+			VacuumGripper_color_sensor_in("color_sensor_in")
+			VacuumGripper_mpo_out("mpo_out")
+			VacuumGripper_mqtt_in("mqtt_in")
+			VacuumGripper_dso_out("dso_out")
+			VacuumGripper_sld_red_out("sld_red_out")
+			VacuumGripper_nfc_out("nfc_out")
+			VacuumGripper_color_sensor_out("color_sensor_out")
+			VacuumGripper_sld_red_in("sld_red_in")
+			VacuumGripper_sld_white_out("sld_white_out")
+			VacuumGripper_ct_out("ct_out")
+			VacuumGripper_nfc_in("nfc_in")
+			VacuumGripper_mqtt_out("mqtt_out")
+			VacuumGripper_ct_in("ct_in")
+			VacuumGripper_dsi_in ~~~ VacuumGripper_dsi_out
+			VacuumGripper_ct_in ~~~ VacuumGripper_ct_out
+			VacuumGripper_nfc_in ~~~ VacuumGripper_nfc_out
+			VacuumGripper_color_sensor_in ~~~ VacuumGripper_color_sensor_out
+			VacuumGripper_sld_red_in ~~~ VacuumGripper_mpo_out
+			VacuumGripper_sld_blue_in ~~~ VacuumGripper_sld_red_out
+			VacuumGripper_sld_white_in ~~~ VacuumGripper_sld_blue_out
+			VacuumGripper_mqtt_in ~~~ VacuumGripper_sld_white_out
 		end
-		MPO_saw_gripper_out --> MPO_gripper_table_in
-		MPO_saw_conveyor_out --> MPO_conveyor_out
-		MPO_saw_mqtt_out --> MPO_mqtt_out
-	end
-	MPO_mqtt_out --> MQTTControlUnit_mqtt_in
-	MPO_conveyor_out --> Conveyor_inp
-	MPO_vgr_in --> MPO_oven_vgr_in
-	MPO_mqtt_in --> MPO_oven_mqtt_in
-	MPO_mqtt_in --> MPO_gripper_mqtt_in
-	MPO_mqtt_in --> MPO_saw_mqtt_in
-	subgraph Conveyor
-		Conveyor_mqtt_in("mqtt_in")
-		Conveyor_out("out")
-		Conveyor_mqtt_out("mqtt_out")
-		Conveyor_inp("inp")
-		Conveyor_inp ~~~ Conveyor_out
-		Conveyor_mqtt_in ~~~ Conveyor_mqtt_out
-	end
-	Conveyor_out --> SLD_inp
-	Conveyor_mqtt_out --> MQTTControlUnit_mqtt_in
-	subgraph SLD
-		SLD_blue_out("blue_out")
-		SLD_red_out("red_out")
-		SLD_mqtt_in("mqtt_in")
-		SLD_white_out("white_out")
-		SLD_inp("inp")
-		SLD_blue_in("blue_in")
-		SLD_red_in("red_in")
-		SLD_white_in("white_in")
-		SLD_mqtt_out("mqtt_out")
-		SLD_inp ~~~ SLD_white_out
-		SLD_mqtt_in ~~~ SLD_red_out
-		SLD_white_in ~~~ SLD_blue_out
-		SLD_red_in ~~~ SLD_mqtt_out
-		subgraph SLD_conveyor
-			SLD_conveyor_out_blue("out_blue")
-			SLD_conveyor_out_red("out_red")
-			SLD_conveyor_inp("inp")
-			SLD_conveyor_out_white("out_white")
-			SLD_conveyor_mqtt_in("mqtt_in")
-			SLD_conveyor_mqtt_out("mqtt_out")
-			SLD_conveyor_inp ~~~ SLD_conveyor_out_white
-			SLD_conveyor_mqtt_in ~~~ SLD_conveyor_out_red
+		VacuumGripper_dsi_out --> DSI_vgr_in
+		VacuumGripper_ct_out --> Transporter_right_in
+		VacuumGripper_nfc_out --> ReaderStation_nfc_in
+		VacuumGripper_color_sensor_out --> ReaderStation_color_in
+		VacuumGripper_mpo_out --> MPO_vgr_in
+		VacuumGripper_sld_red_out --> SLD_red_in
+		VacuumGripper_sld_blue_out --> SLD_blue_in
+		VacuumGripper_sld_white_out --> SLD_white_in
+		VacuumGripper_dso_out --> DSO_inp
+		VacuumGripper_mqtt_out --> MQTTControlUnit_mqtt_in
+		subgraph Transporter
+			Transporter_left_out("left_out")
+			Transporter_mqtt_out("mqtt_out")
+			Transporter_mqtt_in("mqtt_in")
+			Transporter_left_in("left_in")
+			Transporter_right_out("right_out")
+			Transporter_right_in("right_in")
+			Transporter_left_in ~~~ Transporter_right_out
+			Transporter_right_in ~~~ Transporter_left_out
+			Transporter_mqtt_in ~~~ Transporter_mqtt_out
 		end
-		SLD_conveyor_out_white --> SLD_white_bay_sld_in
-		SLD_conveyor_out_red --> SLD_red_bay_sld_in
-		SLD_conveyor_out_blue --> SLD_blue_bay_sld_in
-		SLD_conveyor_mqtt_out --> SLD_mqtt_out
-		subgraph SLD_white_bay
-			SLD_white_bay_mqtt_in("mqtt_in")
-			SLD_white_bay_vgr_in("vgr_in")
-			SLD_white_bay_sld_in("sld_in")
-			SLD_white_bay_mqtt_out("mqtt_out")
-			SLD_white_bay_vgr_out("vgr_out")
-			SLD_white_bay_sld_in ~~~ SLD_white_bay_vgr_out
-			SLD_white_bay_vgr_in ~~~ SLD_white_bay_mqtt_out
+		Transporter_right_out --> VacuumGripper_ct_in
+		Transporter_left_out --> HighBayWarehouse_inp
+		Transporter_mqtt_out --> MQTTControlUnit_mqtt_in
+		subgraph HighBayWarehouse
+			HighBayWarehouse_inp("inp")
+			HighBayWarehouse_out("out")
+			HighBayWarehouse_mqtt_in("mqtt_in")
+			HighBayWarehouse_mqtt_out("mqtt_out")
+			HighBayWarehouse_inventory_out("inventory_out")
+			HighBayWarehouse_inp ~~~ HighBayWarehouse_out
+			HighBayWarehouse_mqtt_in ~~~ HighBayWarehouse_mqtt_out
 		end
-		SLD_white_bay_vgr_out --> SLD_white_out
-		SLD_white_bay_mqtt_out --> SLD_mqtt_out
-		subgraph SLD_red_bay
-			SLD_red_bay_mqtt_in("mqtt_in")
-			SLD_red_bay_vgr_out("vgr_out")
-			SLD_red_bay_mqtt_out("mqtt_out")
-			SLD_red_bay_vgr_in("vgr_in")
-			SLD_red_bay_sld_in("sld_in")
-			SLD_red_bay_sld_in ~~~ SLD_red_bay_vgr_out
-			SLD_red_bay_vgr_in ~~~ SLD_red_bay_mqtt_out
+		HighBayWarehouse_out --> Transporter_left_in
+		HighBayWarehouse_mqtt_out --> MQTTControlUnit_mqtt_in
+		HighBayWarehouse_inventory_out --> InventoryPublisher_inp
+		subgraph InventoryPublisher
+			InventoryPublisher_inp("inp")
+			InventoryPublisher_mqtt_out("mqtt_out")
+			InventoryPublisher_inp ~~~ InventoryPublisher_mqtt_out
 		end
-		SLD_red_bay_vgr_out --> SLD_red_out
-		SLD_red_bay_mqtt_out --> SLD_mqtt_out
-		subgraph SLD_blue_bay
-			SLD_blue_bay_vgr_in("vgr_in")
-			SLD_blue_bay_sld_in("sld_in")
-			SLD_blue_bay_mqtt_out("mqtt_out")
-			SLD_blue_bay_vgr_out("vgr_out")
-			SLD_blue_bay_mqtt_in("mqtt_in")
-			SLD_blue_bay_sld_in ~~~ SLD_blue_bay_vgr_out
-			SLD_blue_bay_vgr_in ~~~ SLD_blue_bay_mqtt_out
+		InventoryPublisher_mqtt_out --> MQTTControlUnit_mqtt_in
+		subgraph MPO
+			MPO_mqtt_out("mqtt_out")
+			MPO_vgr_in("vgr_in")
+			MPO_conveyor_out("conveyor_out")
+			MPO_mqtt_in("mqtt_in")
+			MPO_vgr_in ~~~ MPO_mqtt_out
+			MPO_mqtt_in ~~~ MPO_conveyor_out
+			subgraph MPO_oven
+				MPO_oven_gripper_out("gripper_out")
+				MPO_oven_mqtt_in("mqtt_in")
+				MPO_oven_mqtt_out("mqtt_out")
+				MPO_oven_vgr_in("vgr_in")
+				MPO_oven_gripper_in("gripper_in")
+				MPO_oven_vgr_in ~~~ MPO_oven_gripper_out
+				MPO_oven_gripper_in ~~~ MPO_oven_mqtt_out
+			end
+			MPO_oven_gripper_out --> MPO_gripper_oven_in
+			MPO_oven_mqtt_out --> MPO_mqtt_out
+			subgraph MPO_gripper
+				MPO_gripper_mqtt_out("mqtt_out")
+				MPO_gripper_oven_in("oven_in")
+				MPO_gripper_oven_out("oven_out")
+				MPO_gripper_table_in("table_in")
+				MPO_gripper_table_out("table_out")
+				MPO_gripper_mqtt_in("mqtt_in")
+				MPO_gripper_oven_in ~~~ MPO_gripper_oven_out
+				MPO_gripper_table_in ~~~ MPO_gripper_table_out
+				MPO_gripper_mqtt_in ~~~ MPO_gripper_mqtt_out
+			end
+			MPO_gripper_oven_out --> MPO_oven_gripper_in
+			MPO_gripper_table_out --> MPO_saw_gripper_in
+			MPO_gripper_mqtt_out --> MPO_mqtt_out
+			subgraph MPO_saw
+				MPO_saw_gripper_out("gripper_out")
+				MPO_saw_conveyor_out("conveyor_out")
+				MPO_saw_mqtt_in("mqtt_in")
+				MPO_saw_mqtt_out("mqtt_out")
+				MPO_saw_gripper_in("gripper_in")
+				MPO_saw_gripper_in ~~~ MPO_saw_gripper_out
+				MPO_saw_mqtt_in ~~~ MPO_saw_conveyor_out
+			end
+			MPO_saw_gripper_out --> MPO_gripper_table_in
+			MPO_saw_conveyor_out --> MPO_conveyor_out
+			MPO_saw_mqtt_out --> MPO_mqtt_out
 		end
-		SLD_blue_bay_vgr_out --> SLD_blue_out
-		SLD_blue_bay_mqtt_out --> SLD_mqtt_out
-	end
-	SLD_white_out --> VacuumGripper_sld_white_in
-	SLD_red_out --> VacuumGripper_sld_red_in
-	SLD_blue_out --> VacuumGripper_sld_blue_in
-	SLD_mqtt_out --> MQTTControlUnit_mqtt_in
-	SLD_inp --> SLD_conveyor_inp
-	SLD_mqtt_in --> SLD_conveyor_mqtt_in
-	SLD_mqtt_in --> SLD_white_bay_mqtt_in
-	SLD_mqtt_in --> SLD_red_bay_mqtt_in
-	SLD_mqtt_in --> SLD_blue_bay_mqtt_in
-	SLD_white_in --> SLD_white_bay_vgr_in
-	SLD_red_in --> SLD_red_bay_vgr_in
-	SLD_blue_in --> SLD_blue_bay_vgr_in
-	subgraph DSO
-		DSO_inp("inp")
-		DSO_mqtt_in("mqtt_in")
-		DSO_out("out")
-		DSO_mqtt_out("mqtt_out")
-		DSO_inp ~~~ DSO_out
-		DSO_mqtt_in ~~~ DSO_mqtt_out
-	end
-	DSO_out --> Collector_inp
-	DSO_mqtt_out --> MQTTControlUnit_mqtt_in
-	subgraph MQTTControlUnit
-		MQTTControlUnit_mqtt_out("mqtt_out")
-		MQTTControlUnit_REALTIME_OBSERVED("REALTIME_OBSERVED")
-		MQTTControlUnit_mqtt_in("mqtt_in")
-		MQTTControlUnit_REALTIME_INTERRUPT("REALTIME_INTERRUPT")
-		MQTTControlUnit_mqtt_in ~~~ MQTTControlUnit_mqtt_out
-		MQTTControlUnit_REALTIME_INTERRUPT ~~~ MQTTControlUnit_REALTIME_OBSERVED
+		MPO_mqtt_out --> MQTTControlUnit_mqtt_in
+		MPO_conveyor_out --> Conveyor_inp
+		MPO_vgr_in --> MPO_oven_vgr_in
+		MPO_mqtt_in --> MPO_oven_mqtt_in
+		MPO_mqtt_in --> MPO_gripper_mqtt_in
+		MPO_mqtt_in --> MPO_saw_mqtt_in
+		subgraph Conveyor
+			Conveyor_inp("inp")
+			Conveyor_mqtt_out("mqtt_out")
+			Conveyor_out("out")
+			Conveyor_mqtt_in("mqtt_in")
+			Conveyor_inp ~~~ Conveyor_out
+			Conveyor_mqtt_in ~~~ Conveyor_mqtt_out
+		end
+		Conveyor_out --> SLD_inp
+		Conveyor_mqtt_out --> MQTTControlUnit_mqtt_in
+		subgraph SLD
+			SLD_inp("inp")
+			SLD_mqtt_in("mqtt_in")
+			SLD_white_in("white_in")
+			SLD_red_in("red_in")
+			SLD_blue_in("blue_in")
+			SLD_white_out("white_out")
+			SLD_red_out("red_out")
+			SLD_blue_out("blue_out")
+			SLD_mqtt_out("mqtt_out")
+			SLD_inp ~~~ SLD_white_out
+			SLD_mqtt_in ~~~ SLD_red_out
+			SLD_white_in ~~~ SLD_blue_out
+			SLD_red_in ~~~ SLD_mqtt_out
+			subgraph SLD_conveyor
+				SLD_conveyor_out_red("out_red")
+				SLD_conveyor_out_blue("out_blue")
+				SLD_conveyor_mqtt_out("mqtt_out")
+				SLD_conveyor_inp("inp")
+				SLD_conveyor_mqtt_in("mqtt_in")
+				SLD_conveyor_out_white("out_white")
+				SLD_conveyor_inp ~~~ SLD_conveyor_out_white
+				SLD_conveyor_mqtt_in ~~~ SLD_conveyor_out_red
+			end
+			SLD_conveyor_out_white --> SLD_white_bay_sld_in
+			SLD_conveyor_out_red --> SLD_red_bay_sld_in
+			SLD_conveyor_out_blue --> SLD_blue_bay_sld_in
+			SLD_conveyor_mqtt_out --> SLD_mqtt_out
+			subgraph SLD_white_bay
+				SLD_white_bay_sld_in("sld_in")
+				SLD_white_bay_vgr_in("vgr_in")
+				SLD_white_bay_mqtt_in("mqtt_in")
+				SLD_white_bay_vgr_out("vgr_out")
+				SLD_white_bay_mqtt_out("mqtt_out")
+				SLD_white_bay_sld_in ~~~ SLD_white_bay_vgr_out
+				SLD_white_bay_vgr_in ~~~ SLD_white_bay_mqtt_out
+			end
+			SLD_white_bay_vgr_out --> SLD_white_out
+			SLD_white_bay_mqtt_out --> SLD_mqtt_out
+			subgraph SLD_red_bay
+				SLD_red_bay_vgr_out("vgr_out")
+				SLD_red_bay_mqtt_out("mqtt_out")
+				SLD_red_bay_sld_in("sld_in")
+				SLD_red_bay_vgr_in("vgr_in")
+				SLD_red_bay_mqtt_in("mqtt_in")
+				SLD_red_bay_sld_in ~~~ SLD_red_bay_vgr_out
+				SLD_red_bay_vgr_in ~~~ SLD_red_bay_mqtt_out
+			end
+			SLD_red_bay_vgr_out --> SLD_red_out
+			SLD_red_bay_mqtt_out --> SLD_mqtt_out
+			subgraph SLD_blue_bay
+				SLD_blue_bay_sld_in("sld_in")
+				SLD_blue_bay_vgr_in("vgr_in")
+				SLD_blue_bay_mqtt_in("mqtt_in")
+				SLD_blue_bay_vgr_out("vgr_out")
+				SLD_blue_bay_mqtt_out("mqtt_out")
+				SLD_blue_bay_sld_in ~~~ SLD_blue_bay_vgr_out
+				SLD_blue_bay_vgr_in ~~~ SLD_blue_bay_mqtt_out
+			end
+			SLD_blue_bay_vgr_out --> SLD_blue_out
+			SLD_blue_bay_mqtt_out --> SLD_mqtt_out
+		end
+		SLD_white_out --> VacuumGripper_sld_white_in
+		SLD_red_out --> VacuumGripper_sld_red_in
+		SLD_blue_out --> VacuumGripper_sld_blue_in
+		SLD_mqtt_out --> MQTTControlUnit_mqtt_in
+		SLD_inp --> SLD_conveyor_inp
+		SLD_mqtt_in --> SLD_conveyor_mqtt_in
+		SLD_mqtt_in --> SLD_white_bay_mqtt_in
+		SLD_mqtt_in --> SLD_red_bay_mqtt_in
+		SLD_mqtt_in --> SLD_blue_bay_mqtt_in
+		SLD_white_in --> SLD_white_bay_vgr_in
+		SLD_red_in --> SLD_red_bay_vgr_in
+		SLD_blue_in --> SLD_blue_bay_vgr_in
+		subgraph DSO
+			DSO_req_pickup("req_pickup")
+			DSO_inp("inp")
+			DSO_out("out")
+			DSO_mqtt_in("mqtt_in")
+			DSO_mqtt_out("mqtt_out")
+			DSO_inp ~~~ DSO_out
+			DSO_mqtt_in ~~~ DSO_mqtt_out
+		end
+		DSO_out --> FischertechnikFactory_out
+		DSO_mqtt_out --> MQTTControlUnit_mqtt_in
+		subgraph MQTTControlUnit
+			MQTTControlUnit_REALTIME_INTERRUPT("REALTIME_INTERRUPT")
+			MQTTControlUnit_mqtt_in("mqtt_in")
+			MQTTControlUnit_REALTIME_OBSERVED("REALTIME_OBSERVED")
+			MQTTControlUnit_mqtt_out("mqtt_out")
+			MQTTControlUnit_mqtt_in ~~~ MQTTControlUnit_mqtt_out
+			MQTTControlUnit_REALTIME_INTERRUPT ~~~ MQTTControlUnit_REALTIME_OBSERVED
+		end
+		MQTTControlUnit_mqtt_out --> DSI_mqtt_in
+		MQTTControlUnit_mqtt_out --> ReaderStation_mqtt_in
+		MQTTControlUnit_mqtt_out --> VacuumGripper_mqtt_in
+		MQTTControlUnit_mqtt_out --> Transporter_mqtt_in
+		MQTTControlUnit_mqtt_out --> HighBayWarehouse_mqtt_in
+		MQTTControlUnit_mqtt_out --> MPO_mqtt_in
+		MQTTControlUnit_mqtt_out --> SLD_mqtt_in
+		MQTTControlUnit_mqtt_out --> DSO_mqtt_in
+		MQTTControlUnit_REALTIME_OBSERVED --> FischertechnikFactory_REALTIME_OBSERVED
 	end
-	MQTTControlUnit_mqtt_out --> DSI_mqtt_in
-	MQTTControlUnit_mqtt_out --> ReaderStation_mqtt_in
-	MQTTControlUnit_mqtt_out --> VacuumGripper_mqtt_in
-	MQTTControlUnit_mqtt_out --> Transporter_mqtt_in
-	MQTTControlUnit_mqtt_out --> HighBayWarehouse_mqtt_in
-	MQTTControlUnit_mqtt_out --> MPO_mqtt_in
-	MQTTControlUnit_mqtt_out --> SLD_mqtt_in
-	MQTTControlUnit_mqtt_out --> DSO_mqtt_in
-	MQTTControlUnit_REALTIME_OBSERVED --> FischertechnikFactory_REALTIME_OBSERVED
-	subgraph Collector
-		Collector_inp("inp")
+	FischertechnikFactory_out --> WorkpieceCollector_inp
+	FischertechnikFactory_inp --> DSI_inp
+	FischertechnikFactory_mqtt_in --> MQTTControlUnit_REALTIME_INTERRUPT
+	FischertechnikFactory_req_pickup --> DSO_req_pickup
+	subgraph WorkpieceCollector
+		WorkpieceCollector_inp("inp")
 	end
 ```

+ 166 - 10
simulator/realtime_simulation.py

@@ -9,18 +9,31 @@ import paho.mqtt.client as mqtt
 from dotenv import load_dotenv
 from loguru import logger
 from pypdevs.simulator import Simulator
+from pypdevs.DEVS import CoupledDEVS
 
 from config import load_config  # Local config.py using dotenv
 from data_models.workpiece import Workpiece, WorkpieceColor
 from devs_models.fischertechnik_factory import FischertechnikFactory
 from utils.flowchart_generator import FlowchartGenerator
+from devs_models.generator import GeneratorInput
+from devs_models.collector import WorkpieceCollector
+from anomaly_detector import AnomalyDetector
+from diagnostic_simulator import DiagnosticSimulator
 
 load_dotenv()
 CFG = load_config()
 
+class RealTimeModel(CoupledDEVS):
+    """" Fischertechnik Factory model + output collector for realtime simulation. """
+
+    def __init__(self, name: str):
+        super(RealTimeModel, self).__init__(name)
+        self.factory: FischertechnikFactory = self.addSubModel(FischertechnikFactory("FischertechnikFactory"))
+        self.collector: WorkpieceCollector = self.addSubModel(WorkpieceCollector("WorkpieceCollector"))
+        self.connectPorts(self.factory.out, self.collector.inp)
 
 class MQTTSimulationBridge:
-    def __init__(self):
+    def __init__(self, enable_anomaly_detection: bool = False):
         self.sim = None
         self.sim_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="sim_bridge")
         self.real_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="real_bridge")
@@ -29,7 +42,11 @@ class MQTTSimulationBridge:
         self.real_client.on_message = self._on_real
 
         self.trace_real = []  # (time_wall, topic, payload)
-        self.trace_sim = []
+        self.trace_sim = []  # (time_wall, topic, payload)
+
+        self.mqtt_inject_history = []  # History of injected workpieces (time_wall, topic, json_string)
+        self.anomaly_detector = AnomalyDetector(threshold_s=1.0) if enable_anomaly_detection else None
+        self.is_diagnosing = threading.Lock() # A lock to prevent multiple diagnoses at once
 
         self.id_counter = 0  # used to generate unique IDs for workpieces in the simulation (pure sim mode)
 
@@ -43,12 +60,16 @@ class MQTTSimulationBridge:
         return str(self.id_counter).zfill(8)  # zero-padded to 8 digits
     
     def _log(self, origin, msg):
-        """ Logs the received mqtt messages """
+        """ Logs the received mqtt messages + add to anomaly detector. """
         t = time.time()
         if origin == "real":
             self.trace_real.append((t, msg.topic, msg.payload.decode()))
+            if self.anomaly_detector:
+                self.anomaly_detector.process_real_message(msg.topic, msg.payload.decode())
         else:
             self.trace_sim.append((t, msg.topic, msg.payload.decode()))
+            if self.anomaly_detector:
+                self.anomaly_detector.process_sim_message(msg.topic, msg.payload.decode())
 
     def log_trace_to_jsonl(self, filename_prefix="logs/trace_log"):
         """
@@ -80,6 +101,130 @@ class MQTTSimulationBridge:
                     json_line = json.dumps(log_entry)
                     f.write(json_line + "\n")
 
+    def handle_anomaly(self, station: str, real_state: dict, sim_state: dict):
+        """
+        Callback function executed by AnomalyDetector. This is the entry point
+        for the entire diagnostic process.
+        """
+        # Use a lock to ensure we only run one diagnostic process at a time
+        if not self.is_diagnosing.acquire(blocking=False):
+            logger.warning("Anomaly detected, but a diagnosis is already in progress. Skipping.")
+            return
+
+        try:
+            logger.critical(f"💥 HANDLING ANOMALY at station '{station}'. Launching diagnostics...")
+
+            # 1. GATHER CONTEXT
+            # =================
+            # Get a deep copy of the real message trace
+            real_message_log = list(self.trace_real)
+            # Get the current workpieces from the live simulation model
+            # Note: This needs to be thread-safe if the simulator modifies this list
+            current_workpieces = self.sim.model.get_all_workpieces() # Assumes your model has this method
+
+            # 2. CREATE EVENT LIST
+            # ====================
+            # This is a simplified example. You'll need to refine this logic.
+            # It should convert the raw message log and workpiece list into a
+            # timed sequence of GeneratorInput events.
+            diagnostic_events = self._create_events_from_context(real_message_log, current_workpieces)
+            logger.info(f"Generated {len(diagnostic_events)} events for diagnostic simulations.")
+
+
+            # 3. DEFINE FAULT HYPOTHESES
+            # ==========================
+            # For each station in your factory, create a hypothesis that it's disabled.
+            all_stations = self.sim.model.get_station_names() # Assumes your model has this method
+            hypotheses = []
+            for station_name in all_stations:
+                hypotheses.append({
+                    "name": f"Fault_{station_name}_Disabled",
+                    "params": {"disabled_stations": [station_name]}
+                })
+            # Also include a "no fault" baseline simulation
+            hypotheses.append({"name": "Baseline_NoFault", "params": {}})
+
+            # 4. LAUNCH PARALLEL SIMULATIONS
+            # ==============================
+            sim_threads = []
+            for hypo in hypotheses:
+                thread = DiagnosticSimulator(
+                    name=hypo["name"],
+                    events=diagnostic_events,
+                    fault_hypothesis=hypo["params"]
+                )
+                sim_threads.append(thread)
+                thread.start()
+
+            # 5. COLLECT AND ANALYZE RESULTS
+            # ==============================
+            for thread in sim_threads:
+                thread.join() # Wait for the simulation to finish
+
+            logger.info("All diagnostic simulations complete. Analyzing results...")
+
+            best_match = None
+            lowest_distance = float('inf')
+            
+            # The "ground truth" is the output part of the real message log
+            real_output_trace = self._extract_output_trace(real_message_log)
+
+            for thread in sim_threads:
+                # You need a function to compare two traces and return a "distance"
+                distance = self._compare_traces(real_output_trace, thread.result_trace)
+                logger.info(f"  - Hypothesis '{thread.name}' distance: {distance}")
+                if distance < lowest_distance:
+                    lowest_distance = distance
+                    best_match = thread
+
+            if best_match:
+                logger.success(f"🏆 Best match found! Most likely root cause: {best_match.name}")
+            else:
+                logger.error("Could not determine a likely root cause from simulations.")
+
+        finally:
+            self.is_diagnosing.release() # Release the lock so other anomalies can be handled
+
+    def _create_events_from_context(self, message_log, workpieces) -> list[GeneratorInput]:
+        """
+        (Placeholder) Converts raw logs and state into a timed event list for the generator.
+        """
+        time_zero = message_log[0][0] # wall time of the first message
+
+        # create list of workpiece input events
+        workpieces: list[Workpiece] = self.sim.model.collector.get_generated_workpieces()
+        workpiece_events = []
+
+        # Create list of order events
+        order_events = []
+        for message in message_log:
+            if message[1] == "f/o/order" or message[1] == "f/i/state/dso":
+                order_string = {
+                    "topic": "f/o/order",
+                    "payload": json.dumps({"type": "RED"}),
+                    "origin": "sim"
+                }
+                order_events.append(GeneratorInput(time=message[0] - time_zero, item=order_string))
+
+        # Example:
+        # 1. Add all `workpieces` at time 0.
+        # 2. Go through `message_log`, find all "f/o/order" messages, and add them
+        #    as events at the correct relative time.
+        logger.warning("Placeholder `_create_events_from_context` is being used.")
+        return []
+
+    def _extract_output_trace(self, message_log):
+        """ (Placeholder) Extracts the relevant "output" messages from the real log. """
+        # Example: Filter for messages from the collector, DSO, etc.
+        logger.warning("Placeholder `_extract_output_trace` is being used.")
+        return []
+
+    def _compare_traces(self, trace_a, trace_b) -> float:
+        """ (Placeholder) Compares two traces and returns a numerical distance. """
+        # This could be based on Levenshtein distance, sequence matching, etc.
+        logger.warning("Placeholder `_compare_traces` is being used.")
+        return float('inf') if not trace_b else len(trace_b) # Simple placeholder
+
     def _on_sim(self, _cli, _u, msg):
         json_string = json.dumps({
             "topic": msg.topic,
@@ -97,6 +242,10 @@ class MQTTSimulationBridge:
             new_wp = Workpiece(id=self._get_wp_id(), color=WorkpieceColor(wp["type"]), state=wp["state"])
             self._inject_wp_in_sim(new_wp)  # Injects the workpiece into the simulation
 
+        elif msg.topic.startswith("simulation/ctrl/dso") and json.loads(msg.payload)["action"] == "clear": # TODO
+            # If the DSO is cleared, we need to inject a pickup request into the simulation
+            self._inject_pickup_request_in_sim()
+
         elif msg.topic.startswith("simulation/"):
             print(self.sim.server.getTime())
             logger.info(f"Received SIM MQTT message, topic: {msg.topic}")
@@ -131,8 +280,7 @@ class MQTTSimulationBridge:
             if data.get("active") == 0 and data.get("code") == 1:
                 if self.is_dso_busy:
                     command["action"] = "clear"
-                    self._inject_mqtt_in_sim("simulation/ctrl/dso",
-                                             command)  # simulation dso should be cleared (sync with reality)
+                    self._inject_pickup_request_in_sim()  # simulation dso should be cleared (sync with reality)
                 self.is_dso_busy = False
             elif data.get("active") == 0 and data.get("code") == 0:
                 self.is_dso_busy = True
@@ -161,6 +309,10 @@ class MQTTSimulationBridge:
         sys.modules["pypdevs.basesimulator"].interrupt_workpiece = wp  # Sadly this is needed...
         self.sim.realtime_interrupt("WP_INTERRUPT (interrupt_workpiece)")  # This will call the WP_INTERRUPT function in the simulator
 
+    def _inject_pickup_request_in_sim(self):
+        """Injects a pickup request into the simulation."""
+        self.sim.realtime_interrupt("PICKUP_INTERRUPT []") # Empty list is intentional, event matters, no content needed
+
     def start_clients(self):
         self.sim_client.connect(CFG.MQTT_SIM.HOST, CFG.MQTT_SIM.PORT)
         self.real_client.connect(CFG.MQTT_REAL.HOST, CFG.MQTT_REAL.PORT)
@@ -182,14 +334,17 @@ class MQTTSimulationBridge:
 
     def start_simulation(self):
         logger.info("Starting simulation...")
-        model = FischertechnikFactory("FischertechnikFactory")
+        model = RealTimeModel("RealTimeModel")
         FlowchartGenerator(model).generate_file()
 
         self.sim = Simulator(model)
         self.sim.setRealTime(True)
-        self.sim.setRealTimePorts(
-            {"REALTIME_INTERRUPT": model.mqtt_control.REALTIME_INTERRUPT, "WP_INTERRUPT": model.dsi.inp})
-        self.sim.setListenPorts(model.REALTIME_OBSERVED, self.publish_mqtt)
+        self.sim.setRealTimePorts({
+            "REALTIME_INTERRUPT": model.factory.mqtt_control.REALTIME_INTERRUPT,
+            "WP_INTERRUPT": model.factory.dsi.inp,
+            "PICKUP_INTERRUPT": model.factory.dso.req_pickup
+        })
+        self.sim.setListenPorts(model.factory.REALTIME_OBSERVED, self.publish_mqtt)
         self.sim.setRealTimePlatformThreads()
         # self.sim.setVerbose(filename=None)
         self.sim.simulate()
@@ -227,5 +382,6 @@ class MQTTSimulationBridge:
 
 
 if __name__ == "__main__":
-    bridge = MQTTSimulationBridge()
+    anomaly_detection = False  # Enable/Disable anomaly detection
+    bridge = MQTTSimulationBridge(enable_anomaly_detection=anomaly_detection)
     bridge.run()