How we created a Digital Twin of our Hannover Messe WMF Coffee Machine
tl/dr
This is a technical description on how to visualize data from a websocket interface using the 3DVisualizr tools as a Digital Twin monitoring application.
Motivation
I wanted to share a hands-on example on how we used our 3DVisualizr technology to visualize data from our Hannover Messe coffee experience as can be seen here:
Ingredients:
As for the data source, we are using a WMF 1500S+ coffee machine with the CoffeeConnect interface
FritzBox 7530AX to wirelessly receive the data + LTE Surfstick
PC running Microsoft Visual Studio Code with the LiveServer addon installed and the 3DVisualizr JS engine
Data integration:
The WMF machine shares quite a handful of performance and status data through a websocket interface in "real-time". Fetching the data only required connecting the machine via an RJ45 cable to our FritzBox, and of course a couple of lines of code in javascript that ran in our Visual Studio project:
function initWebSocket() {
wsUri = 'ws://192.168.178.20:25000';
try {
if (typeof MozWebSocket == 'function')
WebSocket = MozWebSocket;
if ( websocket && websocket.readyState == 1 )
websocket.close();
websocket = new WebSocket( wsUri );
websocket.onopen = function (evt) {
onConnectedToMachine();
debug("CONNECTED");
};
websocket.onclose = function (evt) {
onDisconnectedToMachine();
debug("DISCONNECTED");
};
websocket.onmessage = function (evt) {
handleReceivedMessage( evt.data );
};
websocket.onerror = function (evt) {
debug('ERROR: ' + evt.data);
};
} catch (exception) {
debug('ERROR: ' + exception);
}
}
The we started fetching the messages from the machine:
function onConnectedToMachine() {
debug('onConnectedToMachine');
sendMessage('startPushErrors');
sendMessage('startPushCleaningRinsingNotifications', { a_uiPreWarningTimeInSeconds: 0 });
sendMessage('startPushDispensingStarted');
sendMessage('startPushDispensingFinished');
startPollingForMessage('getBeverageStatistics')
startPollingForMessage('getDiagnosticData')
startPollingForMessage('getServiceStatistics')
startPollingForMessage('getSystemCleaningState')
}
function onDisconnectedToMachine() {
debug('onDisconnectedToMachine');
}
function sendMessage(type, opts) {
var msg = JSON.stringify({ function: type, ...opts });
if ( websocket != null ) {
websocket.send( msg );
debug('sent: ' + msg)
}
}
function startPollingForMessage(type, opts) {
sendMessage(type, opts);
setTimeout(() => {
startPollingForMessage(type, opts)
}, 2000);
}
function getValueFromMessage (message, value) {
const el = message.find(v => v[value])
if (el) return el[value]
else return null
}
Now, the data connection is established.
Scene creation
The scene creation was done by reusing elements from our 3DV asset library, and creating a BabylonJS file from the machine's CAD file using our CAD converter. Finally, the walls received a little touch up in Blender by our 3D Artists to show the actual wall designs.

Digital Twin implementation:
With the scene and the data connection in place we can start with the implementation.
Machine Data Status
First, we want to change the machine's data label color and status definition based on the actual machine's status. Below example defines a selection of error codes to be handled as "maintenance" events.
case 'startPushErrors':
debug('Started receiving errors', { message })
if ([74, 84].includes(getValueFromMessage(message, 'ErrorCode'))) {
window.coffeMachineData.maintenance = getValueFromMessage(message, 'Error Text')
} else {
window.coffeMachineData.error = getValueFromMessage(message, 'Error Text')
}
For the data label we defined a couple of different status themes. Standard is idle, unless we receive a message from the machine to change it. Error code 74 will change the status to Maintenance.
var status = rsm.getStoredStyle("Idle")
if (window.coffeMachineData.error) {
status = rsm.getStoredStyle("Error")
} else if (window.coffeMachineData.maintenance) {
status = rsm.getStoredStyle("Maintenance")
} else if (window.coffeMachineData.currentDrink) {
status = rsm.getStoredStyle("Running")
} else if (window.coffeMachineData.cleaning) {
status = rsm.getStoredStyle("Cleaning")
}
Each status has its own color code and can have distinguished data label behaviors. One very common setup is to have "error" status in show-full mode so that users will immediately recognize the issue.
Recipe information
Next, let's also fetch the current recipe from the machine to show it in the data label. The machine shares the recipe and some consumption data whenever it dispenses a drink. Here, we combine the recipe number with the amount of water that was consumed.
case 'startPushDispensingStarted':
debug('Started receiving "drink starts"', { message })
window.coffeMachineData.currentDrink = getRecipeName(getValueFromMessage(message, 'RecipeNumber')) + ' (' + getValueFromMessage(message, 'QtyWater') + 'ml Water)'
break;
This can now also be handled by our data label:
input.labels.push(new E3D.LabelData({
id: "Coffee Machine"
, mesh: "Coffee Machine.Coffee Machine" //the ID from the created scene
, title: "WMF 1500S+" //title to be shown in the label
, status
, layout: E3D.LABEL_LAYOUT.LAYOUT_TITLE
, animationMode: 0
, components: [
{
id: "CurrentDrink"
, title: "Currently dispensing:"
, type: E3D.COMPONENT_TYPE.DEFAULT
, value: window.coffeMachineData.currentDrink
}]
}))
Result
The final implementation showed temperature date from the machine, beverage statistics, in combination with the current machine's status. We furthermore were able to embed dashboards from our Elisa IndustrIQ partners.
And even though it was conceived as a show case to attract visitors to the booth, there were plenty of times that I noticed an issue with the machine through the dashboard as I wasn't facing the machine's display. So I was actually using the demo as a monitoring tool to check on the machine and make sure it was handled properly.
