-
-
Save m1no/90c5776df3f1c06e067076d14477ef43 to your computer and use it in GitHub Desktop.
| // 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'; | |
| } |
FWIW I have to add my serial number to the URL for it to work.
Ultralite Mk5
Cuemix 5: 2.0.0+96480 (2)
Firmware: 2.0.8+2568:24
MacOS: 15.5
So my URL looks like this ws://127.0.0.1:1281/UL5EFF****"
I was pleased to discover that the server is listening on localhost, even though it has a 169 self-assign address.
If I leave the serial number off I get a 404 error.
Really appreciate the work here. I've been wanting to be able to mute my speakers without muting my headphones by assigning a hotkey, so using your discovery here I wrote a command line program that I can assign. Before I found this I was analyzing packets in wireshark, and that was going to take a very long time. Thank you!
Btw, if you're looking for control of the headphone volume via a remote the commands are also fairly simple:
RX: 13 88 00 0a 32 : Left channel, -50db
RX: 13 88 00 0b 32 : Right channel, -50db
Same as the mutes, except for the missing length parameter, command, index, value
TX: 13 88 00 0a 00 01 32
TX: 13 88 00 0b 00 01 32
or
TX: 13 88 00 0a 00 01 32 13 88 00 0b 00 01 32
The L/R can be combined into a single message, either work and the balance of the phones can be controlled if you like.
Whenever you write a new volume and when you initially connect to the server the volume is echoed back to the client, so a controller can work asynchronously with the existing software and hardware controls on the device.
Hi,
I was just struggling with this.
To be honest I might be in over my head but I want to make a midi bridge and I've been trying (with a little support of chatGPT) to send commands to change faders.
Managed to get the MAIN masterfader to move but the Mic 1 fader will not budge.
Any insights tha might steer me in the correct path?
Also how do you get the cuemix to function inside a browser? I only get a virtual running.
Best,
Lennaert

@ccrome there should be no encryption.
Control commands usually consist of:
they are then transmitted as the following bytes
for me muting the headphones is
these bytes are then concatenated into a
Uint8Arrayand sent over the websockethere are more indices for the mute channels:
you can also add
console.logstatements to the cuemix source code and get the bytes this way.using the dev tools in cuemix looks like this:

here you can see the console output of my print statements.$4* 256+4=1028$ , then the index $0$ for line 1/2 length $1$ and value $0$ for unmute.
when I mute/unmute line out 1/2 it sends the bytes
4, 4, 0, 0, 0, 1, 0in decimal which is0x04040000000100in hex.The first two bytes are the command id e.g
You can also see the binary messages in the network tab when clicking on the websocket request.