-
-
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'; | |
} |
@tomakorea did you try the localhost ip?
How did you come up with the magic mute commands 03fb0012000100
and 03fb0012000101
?
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.
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.
@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:
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
You can also see the binary messages in the network tab when clicking on the websocket request.
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