Skip to content

Instantly share code, notes, and snippets.

@m1no
Last active May 3, 2025 10:12
Show Gist options
  • Save m1no/90c5776df3f1c06e067076d14477ef43 to your computer and use it in GitHub Desktop.
Save m1no/90c5776df3f1c06e067076d14477ef43 to your computer and use it in GitHub Desktop.
MOTU Ultralite-mk5 API Bridge
// MOTU Ultralite-mk5 API Bridge
// Author: mino
// Date: 2023-02-22
// License: MIT
//
// Description:
// ------------
// I was looking for a easy way to mute and umute my main speakers via a Stream Deck button on the MOTU Ultralite-mk5.
// Unfortunately, Motu does not provide an HTTP API for this series, this feature is only available on the AVB devices.
// But with a bit of tinkering and reverse engineering of the Motu CueMix 5 app, it is possible to understand the used protocols and to send arbitrary commands.
// A quick and dirty program was written to allow the user to toggle the mute state of the main output of the device with automation in mind (e.g. Stream Deck).
//
// Installation:
// ------------
// Install Node.js and the depdenencies "ws" and "http" with "npm install ws http".
//
// Usage:
// ------
// Start the server with "node motu-bridge.js" and then call the server with a GET request to the endpoint /toggleMuteMain to toggle the mute state of the main output of the device.
//
// Reverse engineering of the Motu Mk5 API:
// ----------------------------------------
// MOTU provides at this time for the Ultralite-mk5 only a Windows application called "CueMix 5".
// This app is based on Electron and uses in the background a WebSocket connection to communicate with the device over its own network interface.
// By further looking into the installed source code of the app, as the app is not obfuscated, it is possible to run the index.html in a browser with enabled developer tools.
// In the developer tools, the network tab can be used to debug the WebSocket traffic between the app and the device and obtain the binary codes that are sent to the device for the different actions.
let motu_device_ip = '169.254.23.124';
let motu_device_port = '1280' // On my Windows machine, but another user had to change this to 1281 on his Mac
const http = require('http');
const WebSocket = require('ws');
let messageIndex = 0;
const server = http.createServer((req, res) => {
if (req.url === '/toggleMuteMain' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
messageIndex = (messageIndex + 1) % 2;
let muteToggle = getMuteToggle(messageIndex);
sendToMotu(muteToggle);
res.end();
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.write('Not found.\n');
res.end();
}
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000');
console.log('[GET] ToggleMute Endpoint: http://localhost:3000/toggleMuteMain');
});
function sendToMotu(binaryCode) {
const ws = new WebSocket(`ws://${motu_device_ip}:${motu_device_port}`);
ws.binaryType = 'arraybuffer';
ws.on('open', () => {
console.log('WebSocket connection opened.');
const buffer = hexStringToBuffer(binaryCode);
const arrayBuffer = bufferToArrayBuffer(buffer);
ws.send(arrayBuffer, { binary: true });
});
ws.on('close', () => {
console.log('WebSocket connection closed.');
});
return ws;
}
function hexStringToBuffer(hexString) {
return Buffer.from(hexString, 'hex');
}
// Convert a Buffer object to an ArrayBuffer
function bufferToArrayBuffer(buffer) {
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
}
function getMuteToggle(index) {
// 03fb0012000101 mute main output
// 03fb0012000100 unmute main output
return index === 0 ? '03fb0012000100' : '03fb0012000101';
}
@m1no
Copy link
Author

m1no commented Aug 14, 2023

"Error connection refused" usually means that the Motu Device doesn't listen on that IP with this specific port or some firewall in-between blocks. From which machine do you try to run this? You could manually start the CUE mix app in a browser to use the browser developer tools -> Networking tab to discover the used IP and ports. See my 2nd last comment line above.

@amrmjd
Copy link

amrmjd commented Aug 14, 2023

I'm on a Mac so not sure if I can do that? I have the macOS firewall disabled.

@amrmjd
Copy link

amrmjd commented Aug 15, 2023

UPDATE.. so if I change to port 1281 it works. Thanks for putting this code together and posting it!

@amrmjd
Copy link

amrmjd commented May 9, 2024

Hi again.. i just got a new computer and migrated to it.. as I was doing that, I noticed there was new firmware as well as new drivers for the UltraLite mk5.. and now I am getting a 404 error when I try to run this.. it's possible that I messed up the set up on the new computer.. but curious if anyone has upgraded to the new firmware + software and if it is still working as is.

@MindStudioOfficial
Copy link

@amrmjd the websocket url changed to ws://127.0.0.1:1281/ULM5FFF1AC

@tomakorea
Copy link

I tried on my Mac with the latest updates and firmware, but it didn't work, I have also 404 error. The ip of my Motu Ultralite MK5 is 169.254.72.230 in the network settings, but nothing seems to work

@MindStudioOfficial
Copy link

@tomakorea did you try the localhost ip?

@ccrome
Copy link

ccrome commented Mar 15, 2025

How did you come up with the magic mute commands 03fb0012000100 and 03fb0012000101?

@MindStudioOfficial
Copy link

MindStudioOfficial commented Mar 18, 2025

@ccrome

As stated in the comment at the top of the code:

// By further looking into the installed source code of the app, as the app is not obfuscated, it is possible to run the index.html in a browser with enabled developer tools.
// In the developer tools, the network tab can be used to debug the WebSocket traffic between the app and the device and obtain the binary codes that are sent to the device for the different actions.

For me opening the source code in the browser didn't really work as the app wouldn't connect to the motu anymore but I could enable the dev tools directly inside the electron app source code.

@ccrome
Copy link

ccrome commented Mar 19, 2025

@ccrome

As stated in the comment at the top of the code:

// By further looking into the installed source code of the app, as the app is not obfuscated, it is possible to run the index.html in a browser with enabled developer tools.
// In the developer tools, the network tab can be used to debug the WebSocket traffic between the app and the device and obtain the binary codes that are sent to the device for the different actions.

For me opening the source code in the browser didn't really work as the app wouldn't connect to the motu anymore but I could enable the dev tools directly inside the electron app source code.

I actually got it to connect! Kind of amazing. But now, it looks like the control data is encrypted. So, I'd need to figure out how to do that from the javascript, if that's possible. I was able to get and decode the periodic meter message, but wasn't able to do any controls. But, it's definitely a good place to start.

@MindStudioOfficial
Copy link

MindStudioOfficial commented Mar 20, 2025

@ccrome there should be no encryption.

Control commands usually consist of:

  • 16bit command id (find by looking at the dev tools websocket messages)
  • 16bit index
  • 16bit length of the value (1 for booleans)
  • value

they are then transmitted as the following bytes

id MSB id LSB index MSB index LSB value length MSB value length LSB value (1 or 0 for boolean) ...

for me muting the headphones is

id index length value
1028 10 1 1 or 0

these bytes are then concatenated into a Uint8Array and sent over the websocket

here are more indices for the mute channels:

  • line 1/2: 0
  • line 3/4: 2
  • line 5/6: 4
  • line 7/8: 6
  • line 9/10: 8
  • headphones: 10

you can also add console.log statements to the cuemix source code and get the bytes this way.

using the dev tools in cuemix looks like this:
cuemix_devtools_console

here you can see the console output of my print statements.
when I mute/unmute line out 1/2 it sends the bytes 4, 4, 0, 0, 0, 1, 0 in decimal which is 0x04040000000100 in hex.
The first two bytes are the command id e.g $4* 256+4=1028$, then the index $0$ for line 1/2 length $1$ and value $0$ for unmute.

You can also see the binary messages in the network tab when clicking on the websocket request.

@MindStudioOfficial
Copy link

grafik

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment