4 Комити 4fbce04f88 ... 54724eacd4

Аутор SHA1 Порука Датум
  anfeny 54724eacd4 Add dual inventory system so accurate visual updates of the HBW inventory are possible пре 1 година
  anfeny 10c4f88bf0 Mqtt-messages filter: make dropdown only close on button click or outside click. Stay open on inside click пре 1 година
  anfeny acd54b89fa Add timestamp feature to send-mqtt card пре 1 година
  anfeny 4806b52dae Add filter to MQTT message list пре 1 година

+ 25 - 4
dashboard/src/static/css/style.css

@@ -96,10 +96,31 @@ table.sensor-table td {
     background-color: var(--bs-gray-300);
 }
 
-#messages {
-    height: 240px;
-    overflow: scroll;
-    white-space: pre-line;
+#mqtt-messages {
+  height: 240px;
+  overflow-y: scroll;
+  list-style: none;
+  padding-left: 0;
+}
+
+#mqtt-messages li {
+  border-bottom: 1px solid rgba(0,0,0,0.1);
+  font-family: monospace;
+}
+
+.mqtt-message {
+  font-size: 0.9rem;
+}
+
+.timestamp {
+  color: var(--bs-gray-600);
+  font-size: 0.75rem;
+  margin-right: 8px;
+}
+
+.topic-list {
+  max-height: 200px;        /* scroll if it overflows */
+  overflow-y: auto;
 }
 
 /* Flash animation for new messages */

+ 102 - 17
dashboard/src/static/js/dashboard.js

@@ -6,29 +6,101 @@
 //const socket = io(); // declared in main.js
 let workpiece_color = "";
 
-// console.log(dashboardMode);
+// MQTT message filter
+const excludedTopics = ['i/cam', 'o/broadcast'];
+let allTopics = new Set(); // Tracks all seen topics
+let allowedTopics = new Set(); // Topics currently allowed to display
+
+const filterContainer = document.getElementById('filter-topics');
+const selectAllBtn = document.getElementById('selectAll');
+const deselectAllBtn = document.getElementById('selectNone');
+const messageList = document.getElementById('mqtt-messages');
+
+// Format timestamp as hh:mm:ss.mmm
+function formatTimestamp() {
+    const now = new Date();
+    return now.toLocaleTimeString('en-GB', { hour12: false }) + '.' + now.getMilliseconds().toString().padStart(3, '0');
+}
+
+// Render filter checkboxes
+function renderFilters() {
+    filterContainer.innerHTML = '';
+    Array.from(allTopics).sort().forEach(topic => {
+        if (excludedTopics.includes(topic)) return;
+        const id = 'filter-' + topic.replace(/[^a-zA-Z0-9]/g, '_');
+        const checked = allowedTopics.has(topic);
+        const div = document.createElement('div');
+        div.className = 'form-check';
+        div.innerHTML = `
+      <input class="form-check-input topic-checkbox" type="checkbox" id="${id}" value="${topic}" ${checked ? 'checked' : ''}>
+      <label class="form-check-label" for="${id}">${topic}</label>
+    `;
+        filterContainer.appendChild(div);
+    });
+
+    // Attach change listeners
+    document.querySelectorAll('.topic-checkbox').forEach(chk => {
+        chk.addEventListener('change', () => {
+            chk.checked ? allowedTopics.add(chk.value) : allowedTopics.delete(chk.value);
+            updateMessageVisibility();
+        });
+    });
+}
 
-// Listen for incoming MQTT messages
+// Show/hide messages based on active filters
+function updateMessageVisibility() {
+    document.querySelectorAll('#mqtt-messages li').forEach(li => {
+        const topic = li.getAttribute('data-topic');
+        li.style.display = allowedTopics.has(topic) ? '' : 'none';
+    });
+}
+
+// Button handlers
+selectAllBtn.addEventListener('click', () => {
+    allowedTopics = new Set(allTopics);
+    renderFilters();
+    updateMessageVisibility();
+});
+
+deselectAllBtn.addEventListener('click', () => {
+    allowedTopics.clear();
+    renderFilters();
+    updateMessageVisibility();
+});
+
+// Handle incoming MQTT messages
 socket.on('mqtt_message', function (data) {
-    // Only show messages from origin that matches our view
+    // Match mode and exclude list
     if (data.origin !== dashboardMode) return;
+    if (excludedTopics.includes(data.topic)) return;
 
-    // Exclude certain topics
-    let excluded_topics = ['i/cam', 'o/broadcast']
-    if (excluded_topics.includes(data.topic)) {
-        return;
+    // Add to topic sets and render if new
+    if (!allTopics.has(data.topic)) {
+        allTopics.add(data.topic);
+        allowedTopics.add(data.topic);
+        renderFilters();
     }
 
-    let messageList = document.getElementById('messages');
-    if (!messageList) return; // Check if the message list exists
-
-    let newMessage = document.createElement('li');
-    newMessage.innerHTML = '<span class="badge bg-secondary me-1">Topic: </span>' + data.topic + '\n' + '<span class="badge bg-secondary me-1">Message:</span>' + data.message;
-    messageList.appendChild(newMessage);
+    // Create and append message item
+    const li = document.createElement('li');
+    li.className = 'list-group-item';
+    li.setAttribute('data-topic', data.topic);
+    li.innerHTML = `
+    <div>
+      <span class="timestamp">${formatTimestamp()}</span>
+      <span class="badge bg-secondary me-1">${data.topic}</span>
+    </div>
+    <div class="mqtt-message">${data.message}</div>`;
+    if (!allowedTopics.has(data.topic)) {
+        li.style.display = 'none';
+    }
+    messageList.appendChild(li);
 
+    // Flash effect
     if (document.getElementById('flashCheckbox').checked) {
-        newMessage.classList.add('flash');
+        li.classList.add('flash');
     }
+    // Auto-scroll
     if (document.getElementById('scrollCheckbox').checked) {
         messageList.scrollTop = messageList.scrollHeight;
     }
@@ -170,10 +242,23 @@ const mqttForm = document.getElementById('mqtt-form');
 if (mqttForm) {
     document.getElementById('mqtt-form').onsubmit = function (event) {
         event.preventDefault();
-        let topic = document.getElementById('topic').value;
-        let message = document.getElementById('message').value;
+        const topic = document.getElementById('topic').value;
+        const messageField = document.getElementById('message');
+        const includeTs = document.getElementById('includeTimestamp').checked;
+
+        let msg;
+        try {
+            msg = JSON.parse(messageField.value || '{}');
+        } catch (e) {
+            alert('Invalid JSON format.');
+            return;
+        }
+
+        if (includeTs) {
+            msg.ts = new Date().toISOString();
+        }
 
-        publishMqtt(topic, message);
+        publishMqtt(topic, JSON.stringify(msg));
     }
 }
 

+ 71 - 0
dashboard/src/static/js/sim/components/hbw-inventory.js

@@ -0,0 +1,71 @@
+import { Crate } from './crate.js';
+import { Workpiece } from './workpiece.js';
+
+
+export class WarehouseInventory {
+    constructor(x, y, height, width = 30) {
+        this.x = x;
+        this.y = y;
+        this.w = width;
+        this.h = height;
+
+        // Create inventory of crates, initially empty
+        this.crateDict = {};
+        const rows = ['A', 'B', 'C'];
+        const cols = [1, 2, 3];
+
+        for (const row of rows) {
+            for (const col of cols) {
+                const key = `${row}${col}`;
+                const pos = this.getTargetPos(key);
+                this.crateDict[key] = new Crate(pos.x, pos.y);
+            }
+        }
+    }
+
+    draw() {
+        push();
+        fill('black');
+        rect(this.x, this.y, this.w, this.h);
+
+        // Draw the top row
+        for (let loc of ["A1", "A2", "A3"]) {
+            this.crateDict[loc].draw();
+        }
+        pop();
+    }
+
+    updateInventory(updateMessage) {
+        // Update the inventory based on the update message
+        for (const item of updateMessage.stockItems) {
+            const location = item.location;
+            const workpiece = item.workpiece;
+
+            if (this.crateDict[location] && workpiece !== null) {
+                this.crateDict[location].addWorkpiece(new Workpiece(0,0, workpiece.type));
+            }
+            else{
+                this.crateDict[location].wp = null;
+            }
+        }
+    }
+
+    /**
+     * Get the position of the row with a certain name
+     * targetName can be "A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"
+     * @param {string} targetName 
+     * @returns {object} {x, y} position of the column
+     */
+    getTargetPos(targetName) {
+        if (targetName.includes("1")) {
+            return { x: this.x + this.w / 2, y: this.y + (this.h / 3 / 2) + 2 * this.h / 3 };
+        }
+        if (targetName.includes("2")) {
+            return { x: this.x + this.w / 2, y: this.y + (this.h / 3 / 2) + this.h / 3  };
+        }
+        if (targetName.includes("3")) {
+            return { x: this.x + this.w / 2, y: this.y + (this.h / 3 / 2) };
+        }
+        return null;
+    }
+}

+ 0 - 61
dashboard/src/static/js/sim/components/inventory.js

@@ -1,61 +0,0 @@
-import { Crate } from './crate.js';
-
-
-export class WarehouseInventory {
-    constructor(x, y, height, width=30) {
-        this.x = x;
-        this.y = y;
-        this.w = width;
-        this.h = height;
-        this.crates = [
-            new Crate(x+width/2, y + (height/3/2)),
-            new Crate(x+width/2, y + (height/3/2) + height/3),
-            new Crate(x+width/2, y + (height/3/2) + 2*height/3)
-        ];
-    }
-
-    draw() {
-        push();
-        fill('black');
-        rect(this.x, this.y, this.w, this.h);
-
-        for (let i = 0; i < this.crates.length; i++) {
-            this.crates[i].draw();
-        }
-        pop();
-    }
-
-    /**
-     * Get column 1 (lowest (left-most) column)
-     */
-    getCol1(){
-        return {x: this.crates[2].x, y: this.crates[2].y};
-    }
-
-    getCol2(){
-        return {x: this.crates[1].x, y: this.crates[1].y};
-    }
-
-    getCol3(){
-        return {x: this.crates[0].x, y: this.crates[0].y};
-    }
-
-    /**
-     * Get the position of the row with a certain name
-     * targetName can be "A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"
-     * @param {string} targetName 
-     * @returns {object} {x, y} position of the column
-     */
-    getTargetPos(targetName){
-        if (targetName.includes("1")) {
-            return this.getCol1();
-        }
-        if (targetName.includes("2")) {
-            return this.getCol2();
-        }
-        if (targetName.includes("3")) {
-            return this.getCol3();
-        }
-        return null;
-    }
-}

+ 1 - 1
dashboard/src/static/js/sim/factory.js

@@ -11,7 +11,7 @@ import { DeliveryStation } from './components/delivery-station.js';
 import { CrateTransporter } from './components/crate-transporter.js';
 import { Vgr } from './components/vgr.js';
 import { Hbw } from './components/hbw.js';
-import { WarehouseInventory } from './components/inventory.js';
+import { WarehouseInventory } from './components/hbw-inventory.js';
 import { Sld } from './components/sld.js';
 
 export class Factory {

+ 5 - 0
dashboard/src/static/js/sim/index.js

@@ -116,6 +116,11 @@ socket.on('visualization_update', function (data) {
         }
     }
 
+    // HBW STOCK (INVENTORY)
+    else if (data.topic.includes("stock")) {
+        factory.inventory.updateInventory(msg);
+    }
+
     // CONVEYOR
     if (data.topic.includes("conveyor")) {
         factory.simpleConveyor.setWorkpiece(workpiece); // update workpiece

+ 28 - 12
dashboard/src/templates/cards/mqtt-messages.html

@@ -1,16 +1,32 @@
 <div class="card">
-    <div class="card-header"><i class="bi bi-card-list me-2"></i>Received Messages</div>
-    <div class="card-body">
-        <ul id="messages"></ul>
-        <div>
-            <div class="form-check form-switch">
-                <label class="form-check-label" for="scrollCheckbox">Auto-scroll</label>
-                <input class="form-check-input" type="checkbox" id="scrollCheckbox" checked>
-            </div>
-            <div class="form-check form-switch">
-                <label class="form-check-label" for="flashCheckbox">Flash-effect</label>
-                <input class="form-check-input" type="checkbox" id="flashCheckbox" checked>
-            </div>
+  <div class="card-header d-flex justify-content-between align-items-center">
+    <div><i class="bi bi-card-list me-2"></i>Received Messages</div>
+    <div class="dropdown">
+      <button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
+        <i class="bi bi-filter"></i>
+      </button>
+      <div class="dropdown-menu dropdown-menu-end p-3" style="min-width: 260px;">
+        <p class="small text-muted mb-2">Select topics to display:</p>
+        <div id="filter-topics" class="topic-list mb-2"></div>
+        <hr>
+        <div class="d-flex justify-content-between">
+          <button class="btn btn-sm btn-outline-primary" id="selectAll">All</button>
+          <button class="btn btn-sm btn-outline-secondary" id="selectNone">None</button>
         </div>
+      </div>
     </div>
+  </div>
+  <div class="card-body">
+    <ul id="mqtt-messages" class="list-group list-group-flush"></ul>
+    <div class="mt-3">
+      <div class="form-check form-switch">
+        <label class="form-check-label" for="scrollCheckbox">Auto-scroll</label>
+        <input class="form-check-input" type="checkbox" id="scrollCheckbox" checked>
+      </div>
+      <div class="form-check form-switch">
+        <label class="form-check-label" for="flashCheckbox">Flash-effect</label>
+        <input class="form-check-input" type="checkbox" id="flashCheckbox" checked>
+      </div>
+    </div>
+  </div>
 </div>

+ 19 - 15
dashboard/src/templates/cards/send-mqtt.html

@@ -1,17 +1,21 @@
 <div class="card">
-    <div class="card-header"><i class="bi bi-send-plus me-2"></i>Send MQTT Message</div>
-    <div class="card-body">
-        <form id="mqtt-form">
-            <div class="row g-3">
-                <div class="col">
-                    <input type="text" id="topic" class="form-control" placeholder="Topic" aria-label="Topic">
-                </div>
-                <div class="col">
-                    <input type="text" id="message" class="form-control" placeholder="Message" aria-label="Message">
-                </div>
-            </div>
-            <br>
-            <button class="btn btn-secondary" type="submit">Send</button>
-        </form>
-    </div>
+  <div class="card-header"><i class="bi bi-send-plus me-2"></i>Send MQTT Message</div>
+  <div class="card-body">
+    <form id="mqtt-form">
+      <div class="row g-3">
+        <div class="col">
+          <input type="text" id="topic" class="form-control" placeholder="Topic" aria-label="Topic">
+        </div>
+        <div class="col">
+          <input type="text" id="message" class="form-control font-monospace" placeholder='Message (e.g. {"key":"value"})' aria-label="Message" value="{}">
+        </div>
+      </div>
+      <div class="form-check mt-2">
+        <input class="form-check-input" type="checkbox" id="includeTimestamp">
+        <label class="form-check-label" for="includeTimestamp">Include timestamp</label>
+      </div>
+      <br>
+      <button class="btn btn-secondary" type="submit">Send</button>
+    </form>
+  </div>
 </div>

+ 5 - 0
simulator/data_models/warehouse_inventory.py

@@ -62,6 +62,11 @@ class WarehouseInventory:
             for location, crate in self.inventory.items()
         ]
         return message
+    
+    def get_visual_update_data(self) -> MqttMessage:
+        message = self.get_stock_update_message()
+        message.topic = "visualization/stock"
+        return message
 
     def get_loc_free_slot(self) -> str | None:
         """ Get the location of the next free slot in the inventory, None if full"""

+ 1 - 1
simulator/devs_models/fischertechnik_factory.py

@@ -77,4 +77,4 @@ class FischertechnikFactory(CoupledDEVS):
 
     def get_inventory(self) -> dict:
         """ Returns the inventory of the system """
-        return self.hbw.state.inventory.inventory
+        return self.hbw.state.mqtt_inventory.inventory

+ 20 - 16
simulator/devs_models/high_bay_warehouse.py

@@ -103,7 +103,8 @@ LOCATION_TIMINGS: dict = {
 @dataclass
 class WarehouseState:
     current_item: Crate | None = None
-    inventory: WarehouseInventory = WarehouseInventory()
+    mqtt_inventory: WarehouseInventory = WarehouseInventory() # Inventory that is indicative of the future (=not accurate to real state, but accurate to factory output)
+    visual_inventory: WarehouseInventory = WarehouseInventory() # Inventory that is accurate to the simulation (-> visual updates)
     phase: HbwPhase = HbwPhase.IDLE
     target: WorkpieceColor | str = "" # workpiece color or 'empty' to denote target is an empty crate
     target_location: str = "" # location of the target in the inventory
@@ -223,9 +224,9 @@ class HighBayWarehouse(AtomicDEVS):
             else:
                 self.change_phase(HbwPhase.CMD_STORE_CRATE)
                 logger.trace(f"{type(self).__name__} '{self.name}' received: {new_crate}")
-                self.state.target_location = self.state.inventory.get_loc_empty_crate()
+                self.state.target_location = self.state.mqtt_inventory.get_loc_free_slot()
                 # if we have a new workpiece, immediately store it
-                self._store_crate(self.state.current_item) # TODO: check if matches with mqtt
+                self._store_crate(self.state.current_item, self.state.mqtt_inventory) # TODO: check if matches with mqtt
 
         elif self.mqtt_in in inputs:
             mqtt_msg: MqttMessage = inputs[self.mqtt_in][0]
@@ -233,11 +234,11 @@ class HighBayWarehouse(AtomicDEVS):
                 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.inventory.get_loc_empty_crate()
+                    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.inventory.get_loc_workpiece_crate(color=self.state.target)
+                    self.state.target_location = self.state.mqtt_inventory.get_loc_workpiece_crate(color=self.state.target)
             elif mqtt_msg.topic == "simulation/get_status":
                 self.state.status_requested = True
         return self.state  # important, return state
@@ -249,7 +250,7 @@ class HighBayWarehouse(AtomicDEVS):
 
     def outputFnc(self):
         if self.state.visual_update_pending:
-            return {self.mqtt_out: [self.get_visual_update_data()]}
+            return {self.mqtt_out: [self.get_visual_update_data(), self.state.visual_inventory.get_visual_update_data()]}
         if self.state.status_requested:
             return {self.mqtt_out: [self.get_status_message(use_next_phase=False)]}
 
@@ -257,22 +258,22 @@ class HighBayWarehouse(AtomicDEVS):
             return {self.out: [self.state.current_item], self.mqtt_out: [self.get_status_message()]}
 
         # else: update the inventory data and status
-        return {self.inventory_out: [self.state.inventory], self.mqtt_out: [self.get_status_message()]}
+        return {self.inventory_out: [self.state.mqtt_inventory], self.mqtt_out: [self.get_status_message()]}
 
-    def _get_crate(self, target: WorkpieceColor | str) -> Crate | None:
+    def _get_crate(self, target: WorkpieceColor | str, inventory: WarehouseInventory) -> Crate | None:
         """Get crate, depending on target"""
         if target == "":
             logger.error(f"{type(self).__name__} '{self.name}' attempting to retrieve crate: {target}")
             return None
         elif target == "empty":
-            empty_loc = self.state.inventory.get_loc_empty_crate()
-            return self.state.inventory.take(empty_loc)
+            empty_loc = inventory.get_loc_empty_crate()
+            return inventory.take(empty_loc)
         else:
-            wp_loc = self.state.inventory.get_loc_workpiece_crate(color=target)
-            return self.state.inventory.take(wp_loc)
+            wp_loc = inventory.get_loc_workpiece_crate(color=target)
+            return inventory.take(wp_loc)
 
-    def _store_crate(self, crate: Crate) -> None:
-        self.state.inventory.insert(crate)
+    def _store_crate(self, crate: Crate, inventory: WarehouseInventory) -> None:
+        inventory.insert(crate)
 
     def intTransition(self):
         if self.state.visual_update_pending:
@@ -285,13 +286,16 @@ class HighBayWarehouse(AtomicDEVS):
 
         if self.state.phase == HbwPhase.FETCH_CRATE:
             # Get the appropriate crate
-            self.state.current_item = self._get_crate(target=self.state.target)
-
+            self.state.current_item = self._get_crate(target=self.state.target, inventory=self.state.mqtt_inventory)
+            # Also update the visual inventory
+            self._get_crate(self.state.target, self.state.visual_inventory)
+            
         elif self.state.phase == HbwPhase.PUSH_ONTO_CT:
             # if we just output a crate, remove it
             self.state.current_item = None
 
         elif self.state.phase == HbwPhase.STORE_CRATE:
+            self._store_crate(crate=self.state.current_item, inventory=self.state.visual_inventory) # Store crate visually
             self.state.current_item = None # remove stored crate
 
         elif self.state.phase == HbwPhase.MOVING_TO_REST_POS:

+ 22 - 22
simulator/flowchart.md

@@ -21,11 +21,11 @@ flowchart LR
 	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_out("nfc_out")
 		ReaderStation_color_in("color_in")
 		ReaderStation_nfc_in("nfc_in")
+		ReaderStation_color_out("color_out")
 		ReaderStation_nfc_in ~~~ ReaderStation_nfc_out
 		ReaderStation_color_in ~~~ ReaderStation_color_out
 	end
@@ -33,24 +33,24 @@ flowchart LR
 	ReaderStation_color_out --> VacuumGripper_color_sensor_in
 	ReaderStation_mqtt_out --> MQTTControlUnit_mqtt_in
 	subgraph VacuumGripper
-		VacuumGripper_sld_blue_in("sld_blue_in")
-		VacuumGripper_nfc_out("nfc_out")
-		VacuumGripper_sld_white_in("sld_white_in")
 		VacuumGripper_sld_red_in("sld_red_in")
-		VacuumGripper_dsi_in("dsi_in")
-		VacuumGripper_color_sensor_out("color_sensor_out")
-		VacuumGripper_nfc_in("nfc_in")
-		VacuumGripper_mpo_out("mpo_out")
-		VacuumGripper_sld_red_out("sld_red_out")
-		VacuumGripper_ct_in("ct_in")
 		VacuumGripper_sld_blue_out("sld_blue_out")
-		VacuumGripper_ct_out("ct_out")
-		VacuumGripper_dso_out("dso_out")
-		VacuumGripper_sld_white_out("sld_white_out")
 		VacuumGripper_color_sensor_in("color_sensor_in")
-		VacuumGripper_dsi_out("dsi_out")
+		VacuumGripper_nfc_out("nfc_out")
+		VacuumGripper_ct_out("ct_out")
 		VacuumGripper_mqtt_out("mqtt_out")
+		VacuumGripper_sld_red_out("sld_red_out")
+		VacuumGripper_nfc_in("nfc_in")
+		VacuumGripper_ct_in("ct_in")
 		VacuumGripper_mqtt_in("mqtt_in")
+		VacuumGripper_color_sensor_out("color_sensor_out")
+		VacuumGripper_dso_out("dso_out")
+		VacuumGripper_dsi_in("dsi_in")
+		VacuumGripper_sld_white_in("sld_white_in")
+		VacuumGripper_mpo_out("mpo_out")
+		VacuumGripper_dsi_out("dsi_out")
+		VacuumGripper_sld_white_out("sld_white_out")
+		VacuumGripper_sld_blue_in("sld_blue_in")
 		VacuumGripper_dsi_in ~~~ VacuumGripper_dsi_out
 		VacuumGripper_ct_in ~~~ VacuumGripper_ct_out
 		VacuumGripper_nfc_in ~~~ VacuumGripper_nfc_out
@@ -71,11 +71,11 @@ flowchart LR
 	VacuumGripper_dso_out --> DSO_inp
 	VacuumGripper_mqtt_out --> MQTTControlUnit_mqtt_in
 	subgraph Transporter
-		Transporter_right_out("right_out")
 		Transporter_left_out("left_out")
 		Transporter_mqtt_out("mqtt_out")
 		Transporter_left_in("left_in")
 		Transporter_right_in("right_in")
+		Transporter_right_out("right_out")
 		Transporter_left_in ~~~ Transporter_right_out
 		Transporter_right_in ~~~ Transporter_left_out
 	end
@@ -150,14 +150,14 @@ flowchart LR
 	Conveyor_out --> SLD_inp
 	Conveyor_mqtt_out --> MQTTControlUnit_mqtt_in
 	subgraph SLD
+		SLD_blue_out("blue_out")
+		SLD_mqtt_out("mqtt_out")
+		SLD_inp("inp")
 		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("inp")
 		SLD_inp ~~~ SLD_white_out
 		SLD_white_in ~~~ SLD_red_out
 		SLD_red_in ~~~ SLD_blue_out
@@ -186,8 +186,8 @@ flowchart LR
 		SLD_white_bay_mqtt_out --> SLD_mqtt_out
 		subgraph SLD_red_bay
 			SLD_red_bay_vgr_out("vgr_out")
-			SLD_red_bay_sld_in("sld_in")
 			SLD_red_bay_mqtt_out("mqtt_out")
+			SLD_red_bay_sld_in("sld_in")
 			SLD_red_bay_vgr_in("vgr_in")
 			SLD_red_bay_sld_in ~~~ SLD_red_bay_vgr_out
 			SLD_red_bay_vgr_in ~~~ SLD_red_bay_mqtt_out
@@ -214,10 +214,10 @@ flowchart LR
 	SLD_red_in --> SLD_red_bay_vgr_in
 	SLD_blue_in --> SLD_blue_bay_vgr_in
 	subgraph DSO
+		DSO_inp("inp")
 		DSO_mqtt_out("mqtt_out")
 		DSO_mqtt_in("mqtt_in")
 		DSO_out("out")
-		DSO_inp("inp")
 		DSO_inp ~~~ DSO_out
 		DSO_mqtt_in ~~~ DSO_mqtt_out
 	end
@@ -225,8 +225,8 @@ flowchart LR
 	subgraph MQTTControlUnit
 		MQTTControlUnit_REALTIME_INTERRUPT("REALTIME_INTERRUPT")
 		MQTTControlUnit_mqtt_in("mqtt_in")
-		MQTTControlUnit_mqtt_out("mqtt_out")
 		MQTTControlUnit_REALTIME_OBSERVED("REALTIME_OBSERVED")
+		MQTTControlUnit_mqtt_out("mqtt_out")
 		MQTTControlUnit_mqtt_in ~~~ MQTTControlUnit_mqtt_out
 		MQTTControlUnit_REALTIME_INTERRUPT ~~~ MQTTControlUnit_REALTIME_OBSERVED
 	end