The official factory frontend is an Angular application, but custom clients can be built in any language that supports MQTT and/or WebSockets.
Connection Requirements:
- MQTT broker address (e.g.,
mqtt://192.168.0.100:1883) - Username:
default - Password:
default - WebSocket support: Port
9001for browser-based applications - TCP support: Port
1883for native applications
The official Angular frontend subscribes to the following topics to display factory state:
| Topic Pattern | Purpose | Retained | Update Frequency |
|---|---|---|---|
fts/v1/ff/+/state |
AGV position, battery, actions | Yes | On state change |
module/v1/ff/+/state |
Module actions, errors, loads | Yes | On state change |
fts/v1/ff/+/connection |
AGV online/offline status | Yes | On connect/disconnect |
module/v1/ff/+/connection |
Module online/offline status | Yes | On connect/disconnect |
Wildcard +: Matches any serial number (e.g., AGV001, MILL001)
Usage Example:
// Subscribe to all AGV states
mqtt.subscribe('fts/v1/ff/+/state', (topic, message) => {
const state = JSON.parse(message.toString());
console.log(`AGV ${state.serialNumber}: ${state.driving ? 'driving' : 'idle'}`);
});| Topic | Purpose | Retained | Content Type |
|---|---|---|---|
ccu/state/stock |
Current inventory | Yes | CloudStock |
ccu/state/layout |
Factory layout graph | Yes | FactoryLayout |
ccu/state/config |
System configuration | Yes | GeneralConfig |
ccu/state/flows |
Production workflows | Yes | ProductionFlows |
ccu/pairing/state |
Connected modules | Yes | PairingState |
Why Retained: These topics use retained messages so the UI displays current state immediately after connecting, without waiting for the next update.
Usage Example:
// Get current stock levels
mqtt.subscribe('ccu/state/stock', (topic, message) => {
const stock = JSON.parse(message.toString());
console.log(`${stock.stockItems.length} items in warehouse`);
});| Topic | Purpose | Retained | Content Type |
|---|---|---|---|
ccu/order/response |
New order created | No | OrderResponse |
ccu/order/active |
Currently running orders | Yes | OrderResponse[] |
ccu/order/completed |
Finished/cancelled orders | Yes | OrderResponse[] |
Active vs Response:
response: Single order when createdactive: Array of all in-progress orders (updated as orders progress)completed: Array of finished orders (append-only log)
Usage Example:
// Track active orders
mqtt.subscribe('ccu/order/active', (topic, message) => {
const orders = JSON.parse(message.toString());
orders.forEach(order => {
console.log(`Order ${order.orderId}: ${order.state} (${order.type})`);
});
});| Topic | Purpose | Retained | Content Type |
|---|---|---|---|
ccu/state/calibration/+ |
Live calibration data | No | ModuleCalibrationState |
Usage: Subscribe during calibration to show real-time sensor values.
The frontend publishes to these topics to control the factory:
| Topic | Purpose | QoS | Message Type |
|---|---|---|---|
ccu/order/request |
Create new production order | 2 | OrderRequest |
ccu/order/cancel |
Cancel ENQUEUED orders | 2 | string[] (order IDs) |
Create Order Example:
const orderRequest = {
type: "WHITE",
timestamp: new Date().toISOString(),
orderType: "PRODUCTION"
};
mqtt.publish('ccu/order/request', JSON.stringify(orderRequest), {
qos: 2
});Cancel Order Example:
// Only works for ENQUEUED orders
const orderIds = ["order-123", "order-456"];
mqtt.publish('ccu/order/cancel', JSON.stringify(orderIds), {
qos: 2
});Important: You cannot cancel orders once they reach IN_PROGRESS state. See AGV Module Documentation.
| Topic | Purpose | QoS | Message Type |
|---|---|---|---|
ccu/set/layout |
Update factory layout | 2 | FactoryLayout |
ccu/set/flows |
Update production workflows | 2 | ProductionFlows |
ccu/set/config |
Update system settings | 1 | GeneralConfig |
ccu/set/module-duration |
Change module timings | 2 | Module settings |
ccu/set/calibration |
Send calibration values | 2 | Calibration data |
Update Layout Example:
const layout = {
modules: [...],
intersections: [...]
};
mqtt.publish('ccu/set/layout', JSON.stringify(layout), {
qos: 2
});| Topic | Purpose | QoS | Message Type |
|---|---|---|---|
ccu/pairing/pair_fts |
Initialize AGV position | 2 | Pairing request |
ccu/set/charge |
Start/stop AGV charging | 2 | Charge request |
ccu/set/park |
Park all modules for transport | 2 | Park command |
ccu/set/reset |
Reset factory | 2 | Reset command |
ccu/set/delete-module |
Remove offline module | 2 | Serial number |
Pair AGV Example:
const pairRequest = {
ftsSerial: "AGV001",
moduleSerial: "MILL001"
};
mqtt.publish('ccu/pairing/pair_fts', JSON.stringify(pairRequest), {
qos: 2
});A simple monitoring dashboard needs to subscribe to:
const monitoringTopics = [
'ccu/state/stock', // Inventory
'ccu/order/active', // Current orders
'ccu/pairing/state', // Connected devices
'fts/v1/ff/+/state', // AGV status
'module/v1/ff/+/state' // Module status
];
monitoringTopics.forEach(topic => {
mqtt.subscribe(topic, (topic, message) => {
const data = JSON.parse(message.toString());
updateDashboard(topic, data);
});
});Benefits of Retained Messages:
- Dashboard shows data immediately on page load
- No need to wait for next state update
- Works even if devices haven't sent updates recently
A control application that can create orders and manage the factory:
// 1. Subscribe to all monitoring topics (see above)
// 2. Create production order
function createOrder(workpieceType) {
const request = {
type: workpieceType,
timestamp: new Date().toISOString(),
orderType: "PRODUCTION"
};
mqtt.publish('ccu/order/request', JSON.stringify(request), {
qos: 2
});
}
// 3. Cancel order (only if ENQUEUED)
function cancelOrder(orderId) {
mqtt.publish('ccu/order/cancel', JSON.stringify([orderId]), {
qos: 2
});
}
// 4. Update production flow
function updateFlow(flows) {
mqtt.publish('ccu/set/flows', JSON.stringify(flows), {
qos: 2
});
}Track device connectivity in real-time:
const deviceStatus = new Map();
// Subscribe to connection topics
mqtt.subscribe('fts/v1/ff/+/connection');
mqtt.subscribe('module/v1/ff/+/connection');
mqtt.on('message', (topic, message) => {
if (topic.endsWith('/connection')) {
const connection = JSON.parse(message.toString());
const serialNumber = connection.serialNumber;
deviceStatus.set(serialNumber, {
online: connection.connectionState === 'ONLINE',
lastSeen: new Date(connection.timestamp)
});
if (connection.connectionState === 'OFFLINE') {
console.warn(`Device ${serialNumber} went offline!`);
}
}
});Build a complete order tracking view:
const orders = new Map();
// Subscribe to order topics
mqtt.subscribe('ccu/order/response'); // New orders
mqtt.subscribe('ccu/order/active'); // Order updates
mqtt.subscribe('ccu/order/completed'); // Finished orders
// Subscribe to device states for detailed progress
mqtt.subscribe('fts/v1/ff/+/state');
mqtt.subscribe('module/v1/ff/+/state');
mqtt.on('message', (topic, message) => {
const data = JSON.parse(message.toString());
if (topic === 'ccu/order/response') {
// New order created
orders.set(data.orderId, {
...data,
history: [`Order created: ${data.type}`]
});
}
else if (topic === 'ccu/order/active') {
// Bulk update of active orders
data.forEach(order => {
if (orders.has(order.orderId)) {
orders.get(order.orderId).state = order.state;
orders.get(order.orderId).productionSteps = order.productionSteps;
}
});
}
else if (topic.includes('/state')) {
// Device state update - find matching order
const orderId = data.orderId;
if (orderId && orders.has(orderId)) {
const order = orders.get(orderId);
if (data.actionState) {
const msg = `${data.serialNumber}: ${data.actionState.command || 'ACTION'} - ${data.actionState.state}`;
order.history.push(msg);
}
}
}
});Library: MQTT.js
const mqtt = require('mqtt');
const client = mqtt.connect('ws://192.168.0.100:9001', {
username: 'default',
password: 'default'
});
client.on('connect', () => {
client.subscribe('ccu/state/stock');
client.subscribe('ccu/order/active');
});
client.on('message', (topic, message) => {
console.log(`Received on ${topic}:`, message.toString());
});Library: async-mqtt
const mqtt = require('async-mqtt');
async function main() {
const client = await mqtt.connectAsync('mqtt://192.168.0.100:1883', {
username: 'default',
password: 'default'
});
await client.subscribe('ccu/order/active');
client.on('message', (topic, message) => {
const orders = JSON.parse(message.toString());
console.log(`${orders.length} active orders`);
});
}Library: paho-mqtt
import paho.mqtt.client as mqtt
import json
def on_connect(client, userdata, flags, rc):
print("Connected")
client.subscribe("ccu/state/stock")
client.subscribe("ccu/order/active")
def on_message(client, userdata, msg):
data = json.loads(msg.payload.decode())
print(f"Received on {msg.topic}: {data}")
client = mqtt.Client()
client.username_pw_set("default", "default")
client.on_connect = on_connect
client.on_message = on_message
client.connect("192.168.0.100", 1883, 60)
client.loop_forever()Library: react-mqtt or MQTT.js
import React, { useEffect, useState } from 'react';
import mqtt from 'mqtt';
function FactoryDashboard() {
const [activeOrders, setActiveOrders] = useState([]);
const [stock, setStock] = useState(null);
useEffect(() => {
const client = mqtt.connect('ws://192.168.0.100:9001', {
username: 'default',
password: 'default'
});
client.on('connect', () => {
client.subscribe('ccu/order/active');
client.subscribe('ccu/state/stock');
});
client.on('message', (topic, message) => {
const data = JSON.parse(message.toString());
if (topic === 'ccu/order/active') {
setActiveOrders(data);
} else if (topic === 'ccu/state/stock') {
setStock(data);
}
});
return () => client.end();
}, []);
return (
<div>
<h1>Factory Dashboard</h1>
<h2>Active Orders: {activeOrders.length}</h2>
<h2>Stock Items: {stock?.stockItems.length || 0}</h2>
</div>
);
}✅ Do:
- Handle reconnection automatically (most MQTT libraries do this)
- Show connection status in UI
- Buffer commands during disconnection
- Use retained messages to restore state after reconnect
❌ Don't:
- Assume connection is always available
- Send commands without waiting for connection
- Ignore connection state changes
✅ Do:
- Validate JSON before parsing
- Handle malformed messages gracefully
- Use TypeScript types or JSON schemas
- Parse timestamps with timezone awareness (ISO 8601)
❌ Don't:
- Trust message format without validation
- Assume timestamps are in local timezone
- Block UI thread with heavy processing
✅ Do:
- Use retained messages for initial state
- Deduplicate device states by serial number
- Track connection state separately from device state
- Buffer state updates to avoid overwhelming UI
❌ Don't:
- Poll for state changes (use MQTT subscriptions)
- Keep unlimited history in memory
- Update UI on every single state change (debounce)
✅ Do:
- Check order state before attempting cancellation
- Track order history with device state updates
- Handle order creation responses asynchronously
- Show order state in UI (
ENQUEUED,IN_PROGRESS, etc.)
❌ Don't:
- Attempt to cancel
IN_PROGRESSorders (will be ignored) - Publish orders without required fields
- Assume orders succeed immediately
✅ Do:
- Use QoS 2 for commands (
ccu/order/request,ccu/set/*) - Use QoS 1 for subscriptions to state topics
- Let CCU control QoS for published state messages
❌ Don't:
- Use QoS 0 for critical commands
- Override CCU's QoS settings on subscribe
- Default credentials (
default/default) - No TLS/SSL encryption
- No authentication beyond basic username/password
- All clients have full access
🔒 Production Recommendations:
- Change MQTT broker credentials
- Enable TLS/SSL (
mqtts://orwss://) - Implement access control lists (ACLs) in Mosquitto
- Separate read-only and control clients
- Use client certificates for authentication
- Network isolation (VLAN, firewall rules)
Problem: Cannot connect to MQTT broker
Solutions:
- Check broker is running:
mosquitto -v - Verify network connectivity:
ping 192.168.0.100 - Test with CLI:
mosquitto_sub -h 192.168.0.100 -t '#' -u default -P default - Check firewall allows ports 1883/9001
Problem: UI shows old data or no data
Solutions:
- Verify retained messages:
mosquitto_sub -h HOST -t 'ccu/state/stock' -u default -P default - Check topic subscription uses correct wildcards (
+not*) - Ensure JSON parsing doesn't fail silently
- Monitor connection state - may be disconnected
Problem: Published to ccu/order/cancel but order continues
Solutions:
- Check order state - only
ENQUEUEDcan be cancelled - Verify order ID is correct
- Check QoS 2 is used for publish
- Monitor
ccu/order/completedfor cancellation confirmation
Problem: UI becomes slow with many messages
Solutions:
- Debounce UI updates (e.g., max 10 updates/second)
- Limit state history buffer size
- Unsubscribe from unused topics
- Use
react-windowor similar for large lists - Move heavy processing to Web Workers
Here's a complete minimal UI using vanilla JavaScript and HTML:
<!DOCTYPE html>
<html>
<head>
<title>Factory Monitor</title>
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
</head>
<body>
<h1>Factory Monitor</h1>
<div id="status">Connecting...</div>
<h2>Active Orders</h2>
<ul id="orders"></ul>
<h2>Stock</h2>
<ul id="stock"></ul>
<h2>Create Order</h2>
<button onclick="createOrder('WHITE')">White</button>
<button onclick="createOrder('RED')">Red</button>
<button onclick="createOrder('BLUE')">Blue</button>
<script>
const client = mqtt.connect('ws://192.168.0.100:9001', {
username: 'default',
password: 'default'
});
client.on('connect', () => {
document.getElementById('status').textContent = 'Connected';
client.subscribe('ccu/order/active');
client.subscribe('ccu/state/stock');
});
client.on('message', (topic, message) => {
const data = JSON.parse(message.toString());
if (topic === 'ccu/order/active') {
const list = document.getElementById('orders');
list.innerHTML = data.map(order =>
`<li>${order.orderId}: ${order.type} - ${order.state}</li>`
).join('');
}
else if (topic === 'ccu/state/stock') {
const list = document.getElementById('stock');
list.innerHTML = data.stockItems.map(item =>
`<li>${item.location}: ${item.workpiece?.type || 'Empty'}</li>`
).join('');
}
});
function createOrder(type) {
const request = {
type: type,
timestamp: new Date().toISOString(),
orderType: "PRODUCTION"
};
client.publish('ccu/order/request', JSON.stringify(request), {
qos: 2
});
}
</script>
</body>
</html>Save as monitor.html and open in a browser. This provides:
- ✅ Live order tracking
- ✅ Stock monitoring
- ✅ Order creation
- ✅ Connection status
- Continue to OPC-UA Relationship for interface integration details
- Review Message Structure for complete message formats
- Check Module Documentation for device-specific commands