ソースを参照

Add MQTT message queue, to make factory more robust

anfeny 2 ヶ月 前
コミット
8fd1fa23af

+ 31 - 15
simulator/devs_models/high_bay_warehouse.py

@@ -1,4 +1,4 @@
-from dataclasses import dataclass
+from dataclasses import dataclass, field
 from enum import Enum
 
 from loguru import logger
@@ -111,6 +111,7 @@ class WarehouseState:
     status_requested: bool = True
     delta_t: float = INFINITY
     visual_update_pending: bool = False
+    mqtt_msg_queue: list[MqttMessage] = field(default_factory=list)  # queue for mqtt that came in while busy
 
 
 @dataclass
@@ -211,6 +212,25 @@ class HighBayWarehouse(AtomicDEVS):
             "target_location": self.state.target_location,
         }
         return message
+    
+    def handle_mqtt_message(self, mqtt_msg: MqttMessage) -> None:
+        """ Handle incoming MQTT messages, used for commands and status requests """
+        if mqtt_msg.topic == "simulation/ctrl/all" and mqtt_msg.payload.get("action") == "refresh":
+            self.state.status_requested = True
+            self.state.visual_update_pending = True
+
+        elif self.state.phase != HbwPhase.IDLE and mqtt_msg.topic == "fl/vgr/do":
+            self.state.mqtt_msg_queue.append(mqtt_msg)  # queue the message for later processing
+
+        elif mqtt_msg.topic == "fl/vgr/do":
+            if mqtt_msg.payload['code'] == 1: # FETCH EMPTY
+                self.change_phase(HbwPhase.CMD_FETCH_CRATE)
+                self.state.target = "empty"
+                self.state.target_location = self.state.mqtt_inventory.get_loc_empty_crate()
+            elif mqtt_msg.payload['code'] == 3: # FETCH WP
+                self.change_phase(HbwPhase.CMD_FETCH_CRATE)
+                self.state.target = WorkpieceColor(mqtt_msg.payload['workpiece']['type'])
+                self.state.target_location = self.state.mqtt_inventory.get_loc_workpiece_crate(color=self.state.target)
 
 
     def extTransition(self, inputs):
@@ -230,26 +250,16 @@ class HighBayWarehouse(AtomicDEVS):
 
         elif self.mqtt_in in inputs:
             mqtt_msg: MqttMessage = inputs[self.mqtt_in][0]
-            if mqtt_msg.topic == "fl/vgr/do":
-                if mqtt_msg.payload['code'] == 1: # FETCH EMPTY
-                    self.change_phase(HbwPhase.CMD_FETCH_CRATE)
-                    self.state.target = "empty"
-                    self.state.target_location = self.state.mqtt_inventory.get_loc_empty_crate()
-                elif mqtt_msg.payload['code'] == 3: # FETCH WP
-                    self.change_phase(HbwPhase.CMD_FETCH_CRATE)
-                    self.state.target = WorkpieceColor(mqtt_msg.payload['workpiece']['type'])
-                    self.state.target_location = self.state.mqtt_inventory.get_loc_workpiece_crate(color=self.state.target)
-
-            elif mqtt_msg.topic == "simulation/ctrl/all" and mqtt_msg.payload.get("action") == "refresh":
-                self.state.status_requested = True
-                self.state.visual_update_pending = True
-                return self.state
+            self.handle_mqtt_message(mqtt_msg)
+            return self.state
             
         return self.state  # important, return state
 
     def timeAdvance(self):
         if self.state.visual_update_pending or self.state.status_requested:
             return 0.0 # immediate reply
+        if self.state.phase == HbwPhase.IDLE and self.state.mqtt_msg_queue:
+            return 0.0 # immediately handle queued MQTT messages
         return self.state.delta_t
 
     def outputFnc(self):
@@ -306,6 +316,12 @@ class HighBayWarehouse(AtomicDEVS):
             self.state.target = "" # reset target
             self.state.target_location = ""
 
+        if self.state.phase == HbwPhase.IDLE and self.state.mqtt_msg_queue:
+            # If we are IDLE and have queued MQTT messages, handle them immediately
+            mqtt_msg = self.state.mqtt_msg_queue.pop(0)
+            self.handle_mqtt_message(mqtt_msg)
+            return self.state
+                
         # move to next phase
         self.change_phase(self.state.phase.next())
         return self.state

+ 60 - 37
simulator/devs_models/vacuum_gripper.py

@@ -1,4 +1,4 @@
-from dataclasses import dataclass
+from dataclasses import dataclass, field
 
 from loguru import logger
 
@@ -81,6 +81,7 @@ class VgrState:
     target_sld_bay_color: str = "NONE" # Target bay color when picking up from SLD
     dso_clear: bool = True # whether the dso (output bay) is clear or not
     visual_update_pending: bool = False
+    mqtt_msg_queue: list[MqttMessage] = field(default_factory=list)  # queue for incoming mqtt messages received while busy, handle later
 
 
 def get_hbw_instruction_message(workpiece: Workpiece, code: int) -> MqttMessage:
@@ -110,8 +111,8 @@ def get_order_response_msg(wp_type: str, state: str="ORDERED"):
     }
     return msg
 
-def is_idle(phase: InputRoutine | OrderRoutine) -> bool:
-    """Is the VGR phase an idle/waiting state?"""
+def is_active(phase: InputRoutine | OrderRoutine) -> bool:
+    """Is the VGR phase an active state?"""
     idle_states = [
         InputRoutine.IDLE,
         InputRoutine.AWAIT_CRATE,
@@ -119,7 +120,7 @@ def is_idle(phase: InputRoutine | OrderRoutine) -> bool:
         OrderRoutine.AWAIT_CRATE,
         OutputRoutine.IDLE
     ]
-    return phase in idle_states
+    return phase not in idle_states
 
 
 class VacuumGripper(AtomicDEVS):
@@ -154,6 +155,11 @@ class VacuumGripper(AtomicDEVS):
         self.state = VgrState()
         self.state.immediate_message = self.get_status_message(use_next_phase=False) # send a status message on startup
 
+
+    def is_idle(self) -> bool:
+        """ Check if the vacuum gripper is in an idle state """
+        return self.state.phase in [InputRoutine.IDLE, OrderRoutine.IDLE, OutputRoutine.IDLE]
+
     def _is_dsi_receive_valid(self, item) -> bool:
         """ Checks if the item we received from DSI, and the combination with the current state is valid"""
         if not self.state.phase == InputRoutine.AWAIT_WORKPIECE:
@@ -186,7 +192,7 @@ class VacuumGripper(AtomicDEVS):
         """ Get the status message for the vacuum gripper """
         # TODO: make codes more accurate to real factory
         phase = self.state.phase.next() if use_next_phase else self.state.phase
-        active = 0 if is_idle(phase) else 1
+        active = 1 if is_active(phase) else 0
         code = 2 if (active == 1) else 1
 
         message = MqttMessage()
@@ -217,7 +223,6 @@ class VacuumGripper(AtomicDEVS):
         elif isinstance(new_phase, OutputRoutine):
             self.state.target = 'dso'
 
-
     def get_visual_update_data(self) -> MqttMessage:
         """ Get visual update data for the animation, contains the action taken and the duration of that action left """
         message = MqttMessage()
@@ -233,6 +238,45 @@ class VacuumGripper(AtomicDEVS):
         }
         return message
 
+    def handle_mqtt_message(self, msg: MqttMessage) -> None:
+        if msg.topic == "simulation/ctrl/all" and msg.payload.get("action") == "refresh":
+            self.state.immediate_message = self.get_status_message(use_next_phase=False) # send a status message
+            self.state.visual_update_pending = True # send visual update
+            return self.state
+        elif msg.topic == "simulation/ctrl/nfc":
+            if self.state.workpiece and msg.payload["action"] == "id_update":
+                self.state.workpiece.id = msg.payload["workpiece"]["id"]
+            elif self.state.workpiece and msg.payload["action"] == "color_update":
+                self.state.workpiece.color = WorkpieceColor[msg.payload["workpiece"]["type"]]
+
+        # Queue some messages for later processing, if we are not IDLE
+        elif not self.is_idle() and msg.topic in ["f/i/state/dsi", "f/o/order", "fl/sld/ack"]:
+            self.state.mqtt_msg_queue.append(msg)
+
+        elif msg.topic == "f/i/state/dsi":
+            if msg.payload['active'] == 1:
+                self.change_phase(InputRoutine.MOVE_TO_DSI)
+                self.state.immediate_message = self.get_status_message(use_next_phase=False) # send a status message
+        elif msg.topic == "f/o/order":
+            # reply with f/i/order
+            self.state.immediate_message = get_order_response_msg(msg.payload["type"])
+        elif msg.topic == "f/i/order" and msg.payload["state"] == 'ORDERED':
+            # start self and hbw to handle the order
+            wp = Workpiece("", WorkpieceColor(msg.payload["type"]), "RAW")
+            self.state.immediate_message = get_hbw_instruction_message(wp, code=3)
+            self.change_phase(OrderRoutine.MOVE_TO_CT)
+        elif msg.topic == "fl/sld/ack" and msg.payload["code"] == 2:
+            # SLD sorted, so go pick up the workpiece from there
+            self.state.target_sld_bay_color = msg.payload["type"]
+            self.change_phase(OutputRoutine.MOVE_TO_SLD)
+            self.state.immediate_message = self.get_status_message(use_next_phase=False) # send a status message
+            self.state.delta_t = SLD_Timings[self.state.target_sld_bay_color][OutputRoutine.MOVE_TO_SLD]
+        elif msg.topic == "f/i/state/dso":
+            self.state.dso_clear = (msg.payload["active"] == 0) # check if dso is clear or not
+            if self.state.phase == OutputRoutine.AWAIT_DSO_CLEAR and self.state.dso_clear:
+                self.change_phase(self.state.phase.next())
+
+
     def extTransition(self, inputs):
         self.state.delta_t -= self.elapsed
 
@@ -282,37 +326,7 @@ class VacuumGripper(AtomicDEVS):
 
         elif self.mqtt_in in inputs:
             msg = inputs[self.mqtt_in][0]
-            if msg.topic == "simulation/ctrl/all" and msg.payload.get("action") == "refresh":
-                self.state.immediate_message = self.get_status_message(use_next_phase=False) # send a status message
-                self.state.visual_update_pending = True # send visual update
-                return self.state
-            elif msg.topic == "simulation/ctrl/nfc":
-                if self.state.workpiece and msg.payload["action"] == "id_update":
-                    self.state.workpiece.id = msg.payload["workpiece"]["id"]
-                elif self.state.workpiece and msg.payload["action"] == "color_update":
-                    self.state.workpiece.color = WorkpieceColor[msg.payload["workpiece"]["type"]]
-
-            
-            elif msg.topic == "f/i/state/dsi":
-                if msg.payload['active'] == 1:
-                    self.change_phase(InputRoutine.MOVE_TO_DSI)
-            elif msg.topic == "f/o/order":
-                # reply with f/i/order
-                self.state.immediate_message = get_order_response_msg(msg.payload["type"])
-            elif msg.topic == "f/i/order" and msg.payload["state"] == 'ORDERED':
-                # start self and hbw to handle the order
-                wp = Workpiece("", WorkpieceColor(msg.payload["type"]), "RAW")
-                self.state.immediate_message = get_hbw_instruction_message(wp, code=3)
-                self.change_phase(OrderRoutine.MOVE_TO_CT)
-            elif msg.topic == "fl/sld/ack" and msg.payload["code"] == 2:
-                # SLD sorted, so go pick up the workpiece from there
-                self.state.target_sld_bay_color = msg.payload["type"]
-                self.change_phase(OutputRoutine.MOVE_TO_SLD)
-                self.state.delta_t = SLD_Timings[self.state.target_sld_bay_color][OutputRoutine.MOVE_TO_SLD]
-            elif msg.topic == "f/i/state/dso":
-                self.state.dso_clear = (msg.payload["active"] == 0) # check if dso is clear or not
-                if self.state.phase == OutputRoutine.AWAIT_DSO_CLEAR and self.state.dso_clear:
-                    self.change_phase(self.state.phase.next())
+            self.handle_mqtt_message(msg)  # handle the mqtt message
 
         # if SLD input
         for color, sld_input in self.sld_in.items():
@@ -320,6 +334,7 @@ class VacuumGripper(AtomicDEVS):
                 if self.state.phase == OutputRoutine.AWAIT_WP_SLD:
                     self.state.workpiece = inputs[sld_input][0]
                     self.change_phase(OutputRoutine.RAISE_WP)
+                    self.state.immediate_message = self.get_status_message(use_next_phase=False) # send a status message
                 else:
                     logger.error(f"{type(self).__name__} '{self.name}' received SLD input while not expecting")
                     return self.state
@@ -329,6 +344,8 @@ class VacuumGripper(AtomicDEVS):
     def timeAdvance(self):
         if self.state.visual_update_pending or self.state.immediate_message:
             return 0.0
+        if self.state.mqtt_msg_queue and self.is_idle():
+            return 0.0 # immediately handle the next mqtt message in the queue
         return self.state.delta_t
 
     def outputFnc(self):
@@ -389,6 +406,12 @@ class VacuumGripper(AtomicDEVS):
         elif (self.state.phase.next() == OutputRoutine.AWAIT_DSO_CLEAR) and self.state.dso_clear:
             self.change_phase(OutputRoutine.DROP_WP_DSO)
             return self.state # immediately proceed to dropping off workpiece at DSO
+        
+        elif self.state.mqtt_msg_queue and self.is_idle():
+            # handle the next mqtt message in the queue
+            msg = self.state.mqtt_msg_queue.pop(0)
+            self.handle_mqtt_message(msg)
+            return self.state
 
         self.change_phase(self.state.phase.next())  # move to the next phase
         return self.state

+ 51 - 51
simulator/flowchart.md

@@ -10,23 +10,23 @@ flowchart LR
 	end
 	Generator_out --> DSI_inp
 	subgraph DSI
-		DSI_mqtt_in("mqtt_in")
-		DSI_vgr_in("vgr_in")
-		DSI_mqtt_out("mqtt_out")
 		DSI_inp("inp")
+		DSI_mqtt_out("mqtt_out")
+		DSI_vgr_in("vgr_in")
 		DSI_out("out")
+		DSI_mqtt_in("mqtt_in")
 		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_nfc_in("nfc_in")
 		ReaderStation_mqtt_in("mqtt_in")
+		ReaderStation_color_out("color_out")
+		ReaderStation_color_in("color_in")
 		ReaderStation_mqtt_out("mqtt_out")
-		ReaderStation_nfc_in("nfc_in")
 		ReaderStation_nfc_out("nfc_out")
-		ReaderStation_color_in("color_in")
-		ReaderStation_color_out("color_out")
 		ReaderStation_nfc_in ~~~ ReaderStation_nfc_out
 		ReaderStation_color_in ~~~ ReaderStation_color_out
 		ReaderStation_mqtt_in ~~~ ReaderStation_mqtt_out
@@ -35,24 +35,24 @@ flowchart LR
 	ReaderStation_color_out --> VacuumGripper_color_sensor_in
 	ReaderStation_mqtt_out --> MQTTControlUnit_mqtt_in
 	subgraph VacuumGripper
-		VacuumGripper_mqtt_out("mqtt_out")
-		VacuumGripper_nfc_in("nfc_in")
-		VacuumGripper_dso_out("dso_out")
-		VacuumGripper_color_sensor_in("color_sensor_in")
-		VacuumGripper_sld_red_in("sld_red_in")
-		VacuumGripper_ct_in("ct_in")
-		VacuumGripper_dsi_in("dsi_in")
 		VacuumGripper_sld_blue_in("sld_blue_in")
-		VacuumGripper_ct_out("ct_out")
-		VacuumGripper_sld_white_in("sld_white_in")
+		VacuumGripper_sld_red_out("sld_red_out")
+		VacuumGripper_sld_red_in("sld_red_in")
 		VacuumGripper_sld_white_out("sld_white_out")
-		VacuumGripper_nfc_out("nfc_out")
-		VacuumGripper_mqtt_in("mqtt_in")
+		VacuumGripper_mpo_out("mpo_out")
+		VacuumGripper_color_sensor_in("color_sensor_in")
+		VacuumGripper_sld_white_in("sld_white_in")
 		VacuumGripper_color_sensor_out("color_sensor_out")
+		VacuumGripper_nfc_in("nfc_in")
+		VacuumGripper_nfc_out("nfc_out")
+		VacuumGripper_ct_in("ct_in")
+		VacuumGripper_mqtt_out("mqtt_out")
+		VacuumGripper_ct_out("ct_out")
 		VacuumGripper_dsi_out("dsi_out")
-		VacuumGripper_mpo_out("mpo_out")
-		VacuumGripper_sld_red_out("sld_red_out")
+		VacuumGripper_mqtt_in("mqtt_in")
+		VacuumGripper_dso_out("dso_out")
 		VacuumGripper_sld_blue_out("sld_blue_out")
+		VacuumGripper_dsi_in("dsi_in")
 		VacuumGripper_dsi_in ~~~ VacuumGripper_dsi_out
 		VacuumGripper_ct_in ~~~ VacuumGripper_ct_out
 		VacuumGripper_nfc_in ~~~ VacuumGripper_nfc_out
@@ -73,12 +73,12 @@ flowchart LR
 	VacuumGripper_dso_out --> DSO_inp
 	VacuumGripper_mqtt_out --> MQTTControlUnit_mqtt_in
 	subgraph Transporter
-		Transporter_mqtt_out("mqtt_out")
-		Transporter_left_in("left_in")
+		Transporter_right_out("right_out")
+		Transporter_right_in("right_in")
 		Transporter_left_out("left_out")
+		Transporter_left_in("left_in")
+		Transporter_mqtt_out("mqtt_out")
 		Transporter_mqtt_in("mqtt_in")
-		Transporter_right_in("right_in")
-		Transporter_right_out("right_out")
 		Transporter_left_in ~~~ Transporter_right_out
 		Transporter_right_in ~~~ Transporter_left_out
 		Transporter_mqtt_in ~~~ Transporter_mqtt_out
@@ -87,10 +87,10 @@ flowchart LR
 	Transporter_left_out --> HighBayWarehouse_inp
 	Transporter_mqtt_out --> MQTTControlUnit_mqtt_in
 	subgraph HighBayWarehouse
-		HighBayWarehouse_inp("inp")
-		HighBayWarehouse_inventory_out("inventory_out")
-		HighBayWarehouse_mqtt_out("mqtt_out")
 		HighBayWarehouse_mqtt_in("mqtt_in")
+		HighBayWarehouse_mqtt_out("mqtt_out")
+		HighBayWarehouse_inventory_out("inventory_out")
+		HighBayWarehouse_inp("inp")
 		HighBayWarehouse_out("out")
 		HighBayWarehouse_inp ~~~ HighBayWarehouse_out
 		HighBayWarehouse_mqtt_in ~~~ HighBayWarehouse_mqtt_out
@@ -105,30 +105,30 @@ flowchart LR
 	end
 	InventoryPublisher_mqtt_out --> MQTTControlUnit_mqtt_in
 	subgraph MPO
-		MPO_mqtt_in("mqtt_in")
-		MPO_vgr_in("vgr_in")
 		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_mqtt_out("mqtt_out")
 			MPO_oven_gripper_out("gripper_out")
 			MPO_oven_gripper_in("gripper_in")
-			MPO_oven_vgr_in("vgr_in")
 			MPO_oven_mqtt_in("mqtt_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
 		end
 		MPO_oven_gripper_out --> MPO_gripper_oven_in
 		MPO_oven_mqtt_out --> MPO_mqtt_out
 		subgraph MPO_gripper
+			MPO_gripper_table_out("table_out")
 			MPO_gripper_table_in("table_in")
-			MPO_gripper_mqtt_out("mqtt_out")
 			MPO_gripper_oven_out("oven_out")
-			MPO_gripper_mqtt_in("mqtt_in")
+			MPO_gripper_mqtt_out("mqtt_out")
 			MPO_gripper_oven_in("oven_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
@@ -137,11 +137,11 @@ flowchart LR
 		MPO_gripper_table_out --> MPO_saw_gripper_in
 		MPO_gripper_mqtt_out --> MPO_mqtt_out
 		subgraph MPO_saw
-			MPO_saw_mqtt_in("mqtt_in")
+			MPO_saw_gripper_in("gripper_in")
 			MPO_saw_gripper_out("gripper_out")
 			MPO_saw_mqtt_out("mqtt_out")
 			MPO_saw_conveyor_out("conveyor_out")
-			MPO_saw_gripper_in("gripper_in")
+			MPO_saw_mqtt_in("mqtt_in")
 			MPO_saw_gripper_in ~~~ MPO_saw_gripper_out
 			MPO_saw_mqtt_in ~~~ MPO_saw_conveyor_out
 		end
@@ -156,36 +156,36 @@ flowchart LR
 	MPO_mqtt_in --> MPO_gripper_mqtt_in
 	MPO_mqtt_in --> MPO_saw_mqtt_in
 	subgraph Conveyor
-		Conveyor_out("out")
 		Conveyor_inp("inp")
-		Conveyor_mqtt_in("mqtt_in")
 		Conveyor_mqtt_out("mqtt_out")
+		Conveyor_mqtt_in("mqtt_in")
+		Conveyor_out("out")
 		Conveyor_inp ~~~ Conveyor_out
 		Conveyor_mqtt_in ~~~ Conveyor_mqtt_out
 	end
 	Conveyor_out --> SLD_inp
 	Conveyor_mqtt_out --> MQTTControlUnit_mqtt_in
 	subgraph SLD
-		SLD_red_out("red_out")
-		SLD_blue_out("blue_out")
 		SLD_mqtt_out("mqtt_out")
 		SLD_inp("inp")
+		SLD_blue_out("blue_out")
 		SLD_mqtt_in("mqtt_in")
+		SLD_blue_in("blue_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_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_white("out_white")
+			SLD_conveyor_inp("inp")
 			SLD_conveyor_mqtt_out("mqtt_out")
+			SLD_conveyor_out_white("out_white")
+			SLD_conveyor_out_red("out_red")
 			SLD_conveyor_mqtt_in("mqtt_in")
-			SLD_conveyor_inp("inp")
 			SLD_conveyor_out_blue("out_blue")
-			SLD_conveyor_out_red("out_red")
 			SLD_conveyor_inp ~~~ SLD_conveyor_out_white
 			SLD_conveyor_mqtt_in ~~~ SLD_conveyor_out_red
 		end
@@ -194,33 +194,33 @@ flowchart LR
 		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_mqtt_in("mqtt_in")
 			SLD_white_bay_vgr_in("vgr_in")
-			SLD_white_bay_vgr_out("vgr_out")
+			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
 		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_vgr_in("vgr_in")
-			SLD_red_bay_sld_in("sld_in")
-			SLD_red_bay_mqtt_out("mqtt_out")
 			SLD_red_bay_mqtt_in("mqtt_in")
+			SLD_red_bay_vgr_out("vgr_out")
+			SLD_red_bay_mqtt_out("mqtt_out")
+			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
 		end
 		SLD_red_bay_vgr_out --> SLD_red_out
 		SLD_red_bay_mqtt_out --> SLD_mqtt_out
 		subgraph SLD_blue_bay
-			SLD_blue_bay_mqtt_out("mqtt_out")
-			SLD_blue_bay_sld_in("sld_in")
 			SLD_blue_bay_vgr_in("vgr_in")
-			SLD_blue_bay_vgr_out("vgr_out")
 			SLD_blue_bay_mqtt_in("mqtt_in")
+			SLD_blue_bay_vgr_out("vgr_out")
+			SLD_blue_bay_sld_in("sld_in")
+			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