I’d like to build a remote control based on an ESP32 to start/stop multirack recording, and display the status.
These command are not available via MIDI, but are via the TCP protocol used by MixPad.
Is the TCP protocol documented somewhere?
I’d like to build a remote control based on an ESP32 to start/stop multirack recording, and display the status.
These command are not available via MIDI, but are via the TCP protocol used by MixPad.
Is the TCP protocol documented somewhere?
Allen and Heath do not reveal their in house protocol. One way round this is to program your soft keys as motor control functions, then remote trigger over midi. I have done this for SQ. I know there are far more soft keys on SQ so it may not be a solution in your case.
Given the requirement to match the same version number for the app and firmware, I expect the implication is that the network protocol isn’t documented because it’s not guaranteed to stay the same. You can always inspect network packets to reverse engineer but there’s no guarantee that it won’t be broken at some point.
I have developed this prototype that surfaces the recording and playback control as MIDI:
However I stopped that project while developing this:
I thought about using the softkeys, but my main issue is retrieving the status, and I’m afraid that’s not possible over MIDI.
I’m interested in the TCP protocol documentation as well!
There is no official documentation or support provided on TCP paquet protocol for the CQ mixers.
All work is based on reverse engineering using specific software to “listen” TCP dialogue btw CQ mixer and control software on a PC (CQMixPad for exemple).
I manage to have already a good amount of TCP Frame structure and complete messages for most of the CQ control.
I tried sniffing the communication between MixPad and the console, but it’s so verbose that I gave up. ![]()
I use wireshark, listening console IP.
Then it is a matter of filtering all heartbreak traffic (keep alive and pooling messages).
Use AI to help you out for this process.
That’s what I tried, with tcpdump.
I am wondering how the editors of third party apps like Mixing Station do…
I use Wireshark.
CQ is connected to the network.
I use this filter in wireshark:
tcp.dstport == 51326 && (tcp.len == 8 || tcp.len == 9 || tcp.len == 18)
I then use CQ Mix PAD to move/activate function and listen to the TCP Frame in Wireshark.
For Transport function like REC or Stop here are the TCP Frame:
Port: 51326
REC = f711111103ff000000
STOP = f711111001ff000000f711111101ff000000
Here is a little code for ESP32, 2 push button and a status LED:
> #include <WiFi.h>
>
>
>
> // Configuration Réseau
>
> const char\* ssid = "your SSID";
>
> const char\* password = "Your pswd";
>
> const char\* fx_ip = "*.*.*.*"; //CQ's IP
>
> const uint16_t fx_port = 51326;
>
>
>
> // Configuration Hardware (ESP32-S3)
>
> const int BTN_REC_PIN = 4; // GPIO for REC btn
>
> const int BTN_STOP_PIN = 5; // GPIO for STOP btn
>
> const int LED_STAT_PIN = 2; // GPIO for LED status
>
>
>
> // Variables de debounce
>
> const unsigned long debounceDelay = 50;
>
> unsigned long lastRecDebounce = 0;
>
> unsigned long lastStopDebounce = 0;
>
> int lastRecState = HIGH;
>
> int lastStopState = HIGH;
>
>
>
> WiFiClient client;
>
>
>
> // --- DÉFINITION DES TRAMES DE TRANSPORT ---
>
> uint8_t trameREC\[9\] = {0xF7, 0x11, 0x11, 0x11, 0x03, 0xFF, 0x00, 0x00, 0x00};
>
>
>
> // STOP nécessite l'envoi de deux trames successives de 9 octets
>
> uint8_t trameSTOP_1\[9\] = {0xF7, 0x11, 0x11, 0x10, 0x01, 0xFF, 0x00, 0x00, 0x00};
>
> uint8_t trameSTOP_2\[9\] = {0xF7, 0x11, 0x11, 0x11, 0x01, 0xFF, 0x00, 0x00, 0x00};
>
>
>
> void setup() {
>
> Serial.begin(115200);
>
>
>
> // Configuration des broches
>
> pinMode(BTN_REC_PIN, INPUT_PULLUP);
>
> pinMode(BTN_STOP_PIN, INPUT_PULLUP);
>
> pinMode(LED_STAT_PIN, OUTPUT);
>
>
>
> // Au démarrage, rien n'est envoyé : LED éteinte
>
> digitalWrite(LED_STAT_PIN, LOW);
>
>
>
> // Connexion Wi-Fi
>
> WiFi.begin(ssid, password);
>
> while (WiFi.status() != WL_CONNECTED) {
>
> delay(500);
>
> Serial.print(".");
>
> }
>
> Serial.println("\\nWi-Fi connecté !");
>
> }
>
>
>
> void loop() {
>
> // ---- GESTION DU BOUTON REC ----
>
> int readingRec = digitalRead(BTN_REC_PIN);
>
> if (readingRec != lastRecState) {
>
> lastRecDebounce = millis();
>
> }
>
> if ((millis() - lastRecDebounce) > debounceDelay) {
>
> if (readingRec == LOW) {
>
> actionREC();
>
> delay(200); // Évite les répétitions physiques involontaires
>
> }
>
> }
>
> lastRecState = readingRec;
>
>
>
> // ---- GESTION DU BOUTON STOP ----
>
> int readingStop = digitalRead(BTN_STOP_PIN);
>
> if (readingStop != lastStopState) {
>
> lastStopDebounce = millis();
>
> }
>
> if ((millis() - lastStopDebounce) > debounceDelay) {
>
> if (readingStop == LOW) {
>
> actionSTOP();
>
> delay(200);
>
> }
>
> }
>
> lastStopState = readingStop;
>
> }
>
>
>
> // ---- ACTION ENREGISTREMENT ----
>
> void actionREC() {
>
> if (!verifierConnexion()) return;
>
>
>
> Serial.println("\[TRANSPORT\] Envoi commande REC...");
>
> client.write(trameREC, sizeof(trameREC));
>
>
>
> // Allumage de la LED de statut
>
> digitalWrite(LED_STAT_PIN, HIGH);
>
> Serial.println("\[STATUT\] LED Allumée (Enregistrement en cours)");
>
> }
>
>
>
> // ---- ACTION ARRÊT ----
>
> void actionSTOP() {
>
> if (!verifierConnexion()) return;
>
>
>
> Serial.println("\[TRANSPORT\] Envoi commande STOP (Trame 1/2)...");
>
> client.write(trameSTOP_1, sizeof(trameSTOP_1));
>
>
>
> Serial.println("\[TRANSPORT\] Envoi commande STOP (Trame 2/2)...");
>
> client.write(trameSTOP_2, sizeof(trameSTOP_2));
>
>
>
> // Extinction de la LED de statut
>
> digitalWrite(LED_STAT_PIN, LOW);
>
> Serial.println("\[STATUT\] LED Éteinte (Stop)");
>
> }
>
>
>
> // Fonction de vérification/reconnexion TCP
>
> bool verifierConnexion() {
>
> if (!client.connected()) {
>
> Serial.println("Reconnexion à la console...");
>
> if (!client.connect(fx_ip, fx_port)) {
>
> Serial.println("Échec de la connexion TCP.");
>
> return false;
>
> }
>
> }
>
> return true;
>
> }
That’s almost exactly what I am looking for, I’ll give it a try.
(avec les commentaires en français en plus
)
Merci beaucoup !
The status LED is optimistic because it only reflects the command that was sent, not the actual state. My next step will be to query the console to retrieve the real status.
Coder l’ESP32 pour l’envoi et l’écoute sur 2 port (port 51326 pour l’envoi des trames REC et STOP, et port 51325 pour l’écoute du statut de la console et controler la LED) est plus compliqué.
Si tu y arrives je suis preneur.
En attendant voici une optimisation du code avec gestion des handshake (sans reconnection auto):
#include <WiFi.h>
// Configuration Réseau
const char* ssid = "*****";
const char* password = "******";
const char* fx_ip = "*.*.*.*";
const uint16_t fx_port = 51326; // Retour sur le port stable qui accepte les commandes
// Configuration Hardware
const int BTN_REC_PIN = 4;
const int BTN_STOP_PIN = 5;
const int LED_STAT_PIN = 2; // Câblée sur GPIO 2 avec R=1kOhm
// Variables de debounce
const unsigned long debounceDelay = 50;
unsigned long lastRecDebounce = 0;
unsigned long lastStopDebounce = 0;
int lastRecState = HIGH;
int lastStopState = HIGH;
// Variable de KeepAlive
unsigned long d_KeepAlive = 0;
WiFiClient client;
// --- TRAMES D'ÉMISSION (PORT 51326) ---
uint8_t trameREC[9] = {0xF7, 0x11, 0x11, 0x11, 0x03, 0xFF, 0x00, 0x00, 0x00};
uint8_t trameSTOP_1[9] = {0xF7, 0x11, 0x11, 0x10, 0x01, 0xFF, 0x00, 0x00, 0x00};
uint8_t trameSTOP_2[9] = {0xF7, 0x11, 0x11, 0x11, 0x01, 0xFF, 0x00, 0x00, 0x00};
// Votre trame de KeepAlive TCP qui stabilise le port 51326
uint8_t pingTrame[8] = {0xF7, 0x00, 0x04, 0x0F, 0x00, 0x01, 0x03, 0x00};
void setup() {
Serial.begin(115200);
pinMode(BTN_REC_PIN, INPUT_PULLUP);
pinMode(BTN_STOP_PIN, INPUT_PULLUP);
pinMode(LED_STAT_PIN, OUTPUT);
digitalWrite(LED_STAT_PIN, LOW); // LED éteinte au démarrage
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWi-Fi connecté !");
}
void loop() {
// S'assurer de la connexion TCP
verifierConnexion();
// ---- ANTI-DÉCONNEXION TCP (Toutes les 3 secondes) ----
if (client.connected() && (millis() - d_KeepAlive > 3000)) {
d_KeepAlive = millis();
client.write(pingTrame, 8);
}
// ---- GESTION BOUTON REC ----
int readingRec = digitalRead(BTN_REC_PIN);
if (readingRec != lastRecState) lastRecDebounce = millis();
if ((millis() - lastRecDebounce) > debounceDelay) {
if (readingRec == LOW) {
actionREC();
delay(200);
}
}
lastRecState = readingRec;
// ---- GESTION BOUTON STOP ----
int readingStop = digitalRead(BTN_STOP_PIN);
if (readingStop != lastStopState) lastStopDebounce = millis();
if ((millis() - lastStopDebounce) > debounceDelay) {
if (readingStop == LOW) {
actionSTOP();
delay(200);
}
}
lastStopState = readingStop;
}
void actionREC() {
if (!client.connected()) return;
Serial.println("[TX] Envoi commande REC...");
client.write(trameREC, sizeof(trameREC));
// Synchro locale : on allume la LED immédiatement
digitalWrite(LED_STAT_PIN, HIGH);
Serial.println("[LED] Statut : ENREGISTREMENT (ON)");
}
void actionSTOP() {
if (!client.connected()) return;
Serial.println("[TX] Envoi commande STOP...");
client.write(trameSTOP_1, sizeof(trameSTOP_1));
client.write(trameSTOP_2, sizeof(trameSTOP_2));
// Synchro locale : on éteint la LED immédiatement
digitalWrite(LED_STAT_PIN, LOW);
Serial.println("[LED] Statut : ARRÊTÉ (OFF)");
}
bool verifierConnexion() {
if (!client.connected()) {
digitalWrite(LED_STAT_PIN, LOW); // Éteindre la LED en cas de perte réseau
Serial.println("Connexion au port de commande stable 51326...");
if (client.connect(fx_ip, fx_port)) {
Serial.println("-> Connecté à la console.");
d_KeepAlive = millis();
return true;
} else {
Serial.println("Échec de la connexion.");
delay(1000);
return false;
}
}
return true;
}
After quite a bit of time with Wireshark, we’re making progress!
A single TCP port does all the work: 51326.
This same connection is bidirectional: you send the commands over it, and the console sends its responses back over the same connection. (there is no port 51325 involved)
Thanks a lot Frenchie for pointing me on the right direction.
What we’ve confirmed so far:
F7 11 11 11 03 FF 00 00 00F7 11 11 10 01 FF 00 00 00 + F7 11 11 11 01 FF 00 00 00F7 12 12 0D <state> 25 0C 00, where <state> is:
0x55 recording0x50 stoppedAt this stage I don’t know whether the status is pushed automatically or requested by MixPad.
To be continued ![]()
Found!
A heartbeat 7F 00 02 00 00 00 96 CF needs to be sent every 2 or 3 seconds (without it, the connection is closed by the console after approx 5s)
7F 00 02 00 00 00 96 CF (wait for the response)7F 01 00 00 00 00 (wait for the response)7F 0C 02 00 00 00 02 00 (wait for the response)
7F 00 02 00 00 00 96 CF every 3 secFor your need, here are the frame I got up to now:
TCP Frame for Allen & Heath CQ range mixer for non midi parameter.
Packet byte#5 is associated with the consol FX's slot: 00 = FX1, 01 = FX2, 02 = FX3, 03 = FX4
All FX TRAIL ON (if available) = f7 5b 20 0e 00 00 00 00, OFF = f7 5b 20 0e 00 00 01 00
All FX ASSIST (if available) OFF = f7 5b 20 10 00 00 00 00
FX1 EASYVERB Reverb Size Mini = f7 5b 20 0c 00 08 00 00, Max = f7 5b 20 0c 00 08 14 00
FX1 EASYVERB FX ASIST Soften ON= f7 5b 20 10 00 00 13 00
FX1 ECHOVERB Echo Level Mini = f7 5b 20 0c 00 0b 00 00, Max = f7 5b 20 0c 00 0b 14 00
FX1 ECHOVERB Size Mini = f7 5b 20 0c 00 08 00 00, Max = f7 5b 20 0c 00 08 14 00
FX1 ECHOVERB Colour = f7 5b 20 0c 00 0c 00 00, Max = f7 5b 20 0c 00 0c 14 00
FX1 ECHOVERB Delay Level Mini = f7 5b 20 0c 00 0a 00 00, Max = f7 5b 20 0c 00 0a 14 00
FX1 ECHOVERB Delay Time 5ms = f7 5b 20 0c 00 09 05 00, 2500ms = f7 5b 20 0c 00 09 c4 09
FX1 ECHOVERB FX ASSIST Level ON = f7 5b 20 10 00 00 01 00
FX1 ECHOVERB FX ASSIST Whisper ON = f7 5b 20 10 00 00 08 00
FX1 ECHOVERB FX ASSIST Clarity ON = f7 5b 20 10 00 00 0a 00
FX1 ECHOVERB FX ASSIST Clarity ON = f7 5b 20 10 00 00 0b 00
FX1 SPACEVERB Decay Time Mini = f7 5b 20 0c 00 00 5e 74, Max = f7 5b 20 0c 00 00 e9 a2
FX1 SPACEVERB Large Hall = f7 5b 20 0c 00 05 00 00, Small Hall = f7 5b 20 0c 00 05 01 00, Bright Hall = f7 5b 20 0c 00 05 02 00, Chamber = f7 5b 20 0c 00 05 03 00, Arena = f7 5b 20 0c 00 05 04 00, Cathedral = f7 5b 20 0c 00 05 05 00, Large Room = f7 5b 20 0c 00 05 06 00, Small Room = f7 5b 20 0c 00 05 07 00, Plate = f7 5b 20 0c 00 05 08 00, Gold Plate = f7 5b 20 0c 00 05 09 00, Echo Plate = f7 5b 20 0c 00 05 0a 00, Forever Plate = f7 5b 20 0c 00 05 0b 00
FX1 SPACEVERB FX ASSIST DEEP ON = f7 5b 20 10 00 00 0c 00
FX1 SPACEVERB FX ASSIST Tight ON = f7 5b 20 10 00 00 0d 00
FX1 SPACEVERB FX ASSIST Crispy ON = f7 5b 20 10 00 00 0e 00
FX1 SPACEVERB FX ASSIST Shiny ON = f7 5b 20 10 00 00 0f 00
FX1 ECHODELAY Repeat Rate Mini = f7 5b 20 0c 00 01 00 80, Max = f7 5b 20 0c 00 01 40 86
FX1 ECHODELAY Intensity Mini = f7 5b 20 0c 00 03 10 80, Max = f7 5b 20 0c 00 03 50 81
FX1 ECHODELAY Treble Mini = f7 5b 20 0c 00 05 00 80, Max = f7 5b 20 0c 00 05 a0 80
FX1 ECHODELAY Bass mini = f7 5b 20 0c 00 04 00 80, Max = f7 5b 20 0c 00 04 a0 80
FX1 TAPEDELAY TIME Mini (40ms) = f7 5b 20 0c 00 00 28 00, Maxi (2400ms) = f7 5b 20 0c 00 00 60 09
FX1 TAPEDELAY Feedback Mini = f7 5b 20 0c 00 02 10 80, maxi = f7 5b 20 0c 00 02 b0 81
FX1 TAPEDELAY HF Damping Mini = f7 5b 20 0c 00 04 00 80, maxi = f7 5b 20 0c 00 04 40 81
FX1 TAPEDELAY FX ASSIST Space ON = f7 5b 20 10 00 00 1d 00
FX1 TAPEDELAY FX ASSIST Focus ON = f7 5b 20 10 00 00 20 00
FX1 TAPEDELAY Low Cut ON = f7 5b 20 0c 00 08 10 80, Low Cut OFF = f7 5b 20 0c 00 08 00 80
FX1 TAPEDELAY High Cut ON = f7 5b 20 0c 00 09 10 80, High Cut OFF = f7 5b 20 0c 00 09 00 80
FX1 TAPEDELAY Thicken ON = f7 5b 20 0c 00 03 10 80, Thicken OFF = f7 5b 20 0c 00 03 00 80
FADERs Numéro de fader, Val mini (0db) / Val Max (+10db)
Fader 1 = f7 ff 06 0e 00 10 00 00 / f7 ff 06 0e 00 10 00 8a
Fader 2 = f7 ff 06 0e 01 10 00 00 / f7 ff 06 0e 01 10 00 8a
Fader 3 = f7 ff 06 0e 02 10 00 00 / f7 ff 06 0e 02 10 00 8a
Fader 4 = f7 ff 06 0e 03 10 00 00 / f7 ff 06 0e 03 10 00 8a
Fader 5 = f7 ff 06 0e 04 10 00 00 / f7 ff 06 0e 04 10 00 8a
Fader 6 = f7 ff 06 0e 05 10 00 00 / f7 ff 06 0e 05 10 00 8a
Fader 7 = f7 ff 06 0e 06 10 00 00 / f7 ff 06 0e 06 10 00 8a
Fader 8 = f7 ff 06 0e 07 10 00 00 / f7 ff 06 0e 07 10 00 8a
Fader 9 = f7 ff 06 0e 08 10 00 00 / f7 ff 06 0e 08 10 00 8a
Fader 10 = f7 ff 06 0e 09 10 00 00 / f7 ff 06 0e 09 10 00 8a
Fader 11 = f7 ff 06 0e 0a 10 00 00 / f7 ff 06 0e 0a 10 00 8a
Fader 12 = f7 ff 06 0e 0b 10 00 00 / f7 ff 06 0e 0b 10 00 8a
Fader 13 = f7 ff 06 0e 0c 10 00 00 / f7 ff 06 0e 0c 10 00 8a
Fader 14 = f7 ff 06 0e 0d 10 00 00 / f7 ff 06 0e 0d 10 00 8a
Fader 15 = f7 ff 06 0e 0e 10 00 00 / f7 ff 06 0e 0e 10 00 8a
Fader 16 = f7 ff 06 0e 0f 10 00 00 / f7 ff 06 0e 0f 10 00 8a
FADER AUX 1 = f7 3f 06 0e 30 10 00 00 / f7 3f 06 0e 30 10 00 8a
FADER AUX 2 = f7 3f 06 0e 31 10 00 00 / f7 3f 06 0e 31 10 00 8a
FADER AUX 3 = f7 3f 06 0e 32 10 00 00 / f7 3f 06 0e 32 10 00 8a
FADER AUX 4 = f7 3f 06 0e 33 10 00 00 / f7 3f 06 0e 33 10 00 8a
FADER AUX 5 = f7 3f 06 0e 34 10 00 00 / f7 3f 06 0e 34 10 00 8a
FADER AUX 6 = f7 3f 06 0e 35 10 00 00 / f7 3f 06 0e 35 10 00 8a
FADER USB = f7 3f 06 0e 1c 10 00 00 / f7 3f 06 0e 1c 10 00 8a
FADER ST1 = f7 3f 06 0e 18 10 00 00 / f7 3f 06 0e 18 10 00 8a
FADER BT = f7 3f 06 0e 1e 10 00 00 / f7 3f 06 0e 1e 10 00 8a
FADER FX 1 = f7 3f 06 0e 20 10 00 00 / f7 3f 06 0e 20 10 00 8a
FADER FX 2 = f7 3f 06 0e 21 10 00 00 / f7 3f 06 0e 21 10 00 8a
MUTE CHANNEL 1 = f7 3f 06 0c 00 00 01 00 / UNMUTE CHANNEL 1 = f7 3f 06 0c 00 00 00 00
MUTE CHANNEL 2 = f7 3f 06 0c 01 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 01 00 00 00
MUTE CHANNEL 3 = f7 3f 06 0c 02 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 02 00 00 00
MUTE CHANNEL 4 = f7 3f 06 0c 03 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 03 00 00 00
MUTE CHANNEL 5 = f7 3f 06 0c 04 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 04 00 00 00
MUTE CHANNEL 6 = f7 3f 06 0c 05 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 05 00 00 00
MUTE CHANNEL 7 = f7 3f 06 0c 06 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 06 00 00 00
MUTE CHANNEL 8 = f7 3f 06 0c 07 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 07 00 00 00
MUTE CHANNEL 9 = f7 3f 06 0c 08 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 08 00 00 00
MUTE CHANNEL 10 = f7 3f 06 0c 09 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 09 00 00 00
MUTE CHANNEL 11 = f7 3f 06 0c 0a 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 0a 00 00 00
MUTE CHANNEL 12 = f7 3f 06 0c 0b 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 0b 00 00 00
MUTE CHANNEL 13 = f7 3f 06 0c 0c 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 0c 00 00 00
MUTE CHANNEL 14 = f7 3f 06 0c 0d 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 0d 00 00 00
MUTE CHANNEL 15 = f7 3f 06 0c 0e 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 0e 00 00 00
MUTE CHANNEL 16 = f7 3f 06 0c 0f 00 01 00 / UNMUTE CHANNEL 2 = f7 3f 06 0c 0f 00 00 00
MUTE AUX 1 = f7 3f 06 0c 30 00 01 00 / UNMUTE AUX 1 = f7 3f 06 0c 30 00 00 00
MUTE AUX 2 = f7 3f 06 0c 31 00 01 00 / UNMUTE AUX 1 = f7 3f 06 0c 30 00 00 00
MUTE AUX 3 = f7 3f 06 0c 32 00 01 00 / UNMUTE AUX 1 = f7 3f 06 0c 30 00 00 00
MUTE AUX 4 = f7 3f 06 0c 33 00 01 00 / UNMUTE AUX 1 = f7 3f 06 0c 30 00 00 00
MUTE AUX 5 = f7 3f 06 0c 34 00 01 00 / UNMUTE AUX 1 = f7 3f 06 0c 30 00 00 00
MUTE AUX 6 = f7 3f 06 0c 35 00 01 00 / UNMUTE AUX 1 = f7 3f 06 0c 30 00 00 00
LOAD FX1 EASY VERB = f7 00 04 0f 00 01 03 00
LOAD FX2 ECHO VERB = f7 00 04 0f 00 01 04 00
LOAD FX3 SPACE VERB = f7 00 04 0f 00 01 05 00
LOAD FX4 ECHO DELAY = f7 00 04 0f 00 01 06 00
LOAD FX5 TAP DELAY = f7 00 04 0f 00 01 07 00
LOAD FX6 STEREO DELAY = f7 00 04 0f 00 08 03 00
LOAD FX7 BEAT DELAY = f7 00 04 0f 00 01 09 00
LOAD FX8 DOUBLE TRACKER = f7 00 04 0f 00 0a 03 00
LOAD FX9 CHORUS = f7 00 04 0f 00 01 0b 00
LOAD FX10 FLANGER = f7 00 04 0f 00 01 0c 00
LOAD FX11 PHASER = f7 00 04 0f 00 01 0d 00
LOAD FX PRESET 1 = f7 00 04 0f 00 01 00 00
LOAD FX PRESET 2 = f7 00 04 10 00 01 02 00
Well, I’ve run into a limitation.
When MixPad starts, it registers itself with the console. After that, you can send whatever commands you want from the registered IP address until the console is restarted.
So if an ESP32 connects without having gone through this registration process, nothing works.
(All of my previous tests worked because I had launched MixPad on my PC beforehand.)
The only workaround I’ve found so far is to send an 8 KB blob to mimic the registration process performed by MixPad, but I have no idea what it actually contains…
The steps are now:
7F 00 02 00 00 00 96 CF (wait for the response)7F 01 00 00 00 00 (wait for the response)7F 0C 02 00 00 00 02 00
7F 00 02 00 00 00 96 CF every 3 secThis works in Python, let’s try now with an ESP32.
Here we go!
If some of you want to test, this is the ESP32 code for PlatformIO. I can provide that 8KB sequence (handshake.h) which is too big to be posted here.
192.168.2.1 is the IP of the console when configured as an Access Point.
#include <Arduino.h>
#include <WiFi.h>
#include "handshake.h"
const char* ssid = "*********";
const char* password = "*********";
const char* fx_ip = "192.168.2.1";
const uint16_t fx_port = 51326;
// ============================================================
// Configuration Hardware
// ============================================================
const int BTN_TOGGLE_PIN = 4; // Poussoir vers GND, pull-up interne : bascule REC/STOP
const int LED_STAT_PIN = 2; // Allumée pendant l'enregistrement (statut réel envoyé par la console)
const int WIFI_LED_PIN = 15; // Clignote en connexion, fixe une fois connecté
// ============================================================
// Debounce bouton
// ============================================================
const unsigned long debounceDelay = 50;
unsigned long lastBtnDebounce = 0;
int lastBtnRawState = HIGH;
int btnConfirmedState = HIGH; // Pull-up, LOW = pressé
// ============================================================
// Timing (keepalive, reconnexion, clignotement WiFi)
// ============================================================
const unsigned long HEARTBEAT_INTERVAL_MS = 3000;
const unsigned long RECONNECT_DELAY_MS = 2000; // delai initial entre tentatives
const unsigned long RECONNECT_DELAY_MAX_MS = 30000; // plafond du backoff exponentiel
const unsigned long WIFI_BLINK_INTERVAL_MS = 300;
const unsigned long TCP_CONNECT_TIMEOUT_MS = 2000; // evite de geler loop() en cas de console injoignable
unsigned long lastHeartbeat = 0;
unsigned long lastWifiBlink = 0;
unsigned long lastReconnectAttempt = 0;
unsigned long currentReconnectDelay = RECONNECT_DELAY_MS;
bool wifiLedState = false;
WiFiClient client;
bool consoleReady = false; // true une fois le handshake complet effectué
// Trames actions CQ-20B
uint8_t trameREC[9] = {0xF7, 0x11, 0x11, 0x11, 0x03, 0xFF, 0x00, 0x00, 0x00};
uint8_t trameSTOP_1[9] = {0xF7, 0x11, 0x11, 0x10, 0x01, 0xFF, 0x00, 0x00, 0x00};
uint8_t trameSTOP_2[9] = {0xF7, 0x11, 0x11, 0x11, 0x01, 0xFF, 0x00, 0x00, 0x00};
// Handshake de connexion (à faire dans l'ordre, une seule fois par connexion TCP)
uint8_t hsStep1[8] = {0x7F, 0x00, 0x02, 0x00, 0x00, 0x00, 0x96, 0xCF}; // Sert aussi de heartbeat (envoyé toutes les 3s)
uint8_t hsStep2[6] = {0x7F, 0x01, 0x00, 0x00, 0x00, 0x00};
uint8_t hsStep3[8] = {0x7F, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00};
// Format du paquet de statut retourné par la console : F7 12 12 0D <état> 25 0C 00, avec état:
// - 0x55 -> enregistrement actif
// - 0x50 -> arrêté
uint8_t statusPrefix[4] = {0xF7, 0x12, 0x12, 0x0D};
int statusMatchPos = 0; // 0..3 = en cours de match, -1 = prochain octet = état
bool isRecording = false;
bool recordingKnown = false;
void setRecordingLed(bool recording);
void WiFiLED();
void keepAlive();
void processIncomingByte(uint8_t b);
void readButton();
void toggleRecording();
void actionREC();
void actionSTOP();
void onWiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info);
void setup() {
Serial.begin(115200);
pinMode(BTN_TOGGLE_PIN, INPUT_PULLUP);
pinMode(LED_STAT_PIN, OUTPUT);
pinMode(WIFI_LED_PIN, OUTPUT);
digitalWrite(LED_STAT_PIN, LOW);
digitalWrite(WIFI_LED_PIN, LOW);
WiFi.setSleep(false);
WiFi.onEvent(onWiFiEvent);
WiFi.begin(ssid, password);
Serial.println("Connexion Wi-Fi...");
}
void loop() {
WiFiLED();
if (WiFi.status() != WL_CONNECTED) {
if (client.connected()) client.stop();
consoleReady = false;
return;
}
keepAlive();
if (client.connected() && consoleReady) {
if (millis() - lastHeartbeat > HEARTBEAT_INTERVAL_MS) {
lastHeartbeat = millis();
client.write(hsStep1, sizeof(hsStep1));
}
// Lecture PAR BLOC (pas octet par octet)
int avail = client.available();
while (avail > 0) {
uint8_t buf[256];
int n = client.read(buf, min(avail, (int)sizeof(buf)));
if (n <= 0) break;
for (int i = 0; i < n; i++) {
processIncomingByte(buf[i]);
}
avail = client.available();
}
}
readButton();
}
// ============================================================
// LED Wi-Fi : clignote tant que l'abonnement au statut n'est pas
// confirmé (Wi-Fi ET handshake), fixe une fois prêt.
// ============================================================
void WiFiLED() {
bool ready = (WiFi.status() == WL_CONNECTED) && consoleReady;
if (ready) {
digitalWrite(WIFI_LED_PIN, HIGH);
} else {
if (millis() - lastWifiBlink > WIFI_BLINK_INTERVAL_MS) {
lastWifiBlink = millis();
wifiLedState = !wifiLedState;
digitalWrite(WIFI_LED_PIN, wifiLedState ? HIGH : LOW);
}
}
}
// ============================================================
// Diagnostic Wi-Fi si coupure
// ============================================================
void onWiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) {
Serial.printf("[WiFi] Deconnecte, raison=%d\n", info.wifi_sta_disconnected.reason);
} else if (event == ARDUINO_EVENT_WIFI_STA_CONNECTED) {
Serial.println("[WiFi] Associe au point d'acces.");
} else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) {
Serial.println("[WiFi] IP obtenue.");
}
}
// ============================================================
// Connexion TCP + handshake
// ============================================================
void keepAlive() {
if (client.connected()) return;
consoleReady = false;
if (millis() - lastReconnectAttempt < currentReconnectDelay) return;
lastReconnectAttempt = millis();
Serial.println("Connexion a la console...");
if (!client.connect(fx_ip, fx_port, TCP_CONNECT_TIMEOUT_MS)) {
Serial.println("-> Echec de connexion.");
currentReconnectDelay = min(currentReconnectDelay * 2, RECONNECT_DELAY_MAX_MS);
return;
}
Serial.println("-> Connecte, handshake...");
client.write(hsStep1, sizeof(hsStep1));
delay(50);
client.write(hsStep2, sizeof(hsStep2));
delay(50);
size_t sent = client.write(handshake, HS_LEN);
if (sent != HS_LEN) {
Serial.printf("-> ATTENTION : blob envoye partiellement (%u / %u octets).\n",
(unsigned)sent, (unsigned)HS_LEN);
}
delay(50);
client.write(hsStep3, sizeof(hsStep3));
delay(50);
if (!client.connected()) {
Serial.println("-> Connexion perdue pendant le handshake (autre client sur cette IP ?)");
currentReconnectDelay = min(currentReconnectDelay * 2, RECONNECT_DELAY_MAX_MS);
return;
}
consoleReady = true;
statusMatchPos = 0;
lastHeartbeat = millis();
currentReconnectDelay = RECONNECT_DELAY_MS; // reset du backoff apres un succès
Serial.println("-> Abonnement au statut OK.");
}
// ============================================================
// Détection du paquet de statut dans le flux TCP
// ============================================================
void processIncomingByte(uint8_t b) {
if (statusMatchPos == -1) {
if (b == 0x55) {
setRecordingLed(true);
} else if (b == 0x50) {
setRecordingLed(false);
}
statusMatchPos = (b == statusPrefix[0]) ? 1 : 0;
return;
}
if (b == statusPrefix[statusMatchPos]) {
statusMatchPos++;
if (statusMatchPos == 4) {
statusMatchPos = -1;
}
} else {
statusMatchPos = (b == statusPrefix[0]) ? 1 : 0;
}
}
void setRecordingLed(bool recording) {
if (recordingKnown && recording == isRecording) return;
isRecording = recording;
recordingKnown = true;
digitalWrite(LED_STAT_PIN, recording ? HIGH : LOW);
Serial.println(recording ? "[STATUT] Enregistrement ACTIF" : "[STATUT] Enregistrement ARRETE");
}
void readButton() {
int reading = digitalRead(BTN_TOGGLE_PIN);
if (reading != lastBtnRawState) {
lastBtnDebounce = millis();
}
if ((millis() - lastBtnDebounce) > debounceDelay) {
if (reading != btnConfirmedState) {
btnConfirmedState = reading;
if (btnConfirmedState == LOW) { // front descendant = appui confirmé
toggleRecording();
}
}
}
lastBtnRawState = reading;
}
void toggleRecording() {
if (!recordingKnown) {
Serial.println("[BOUTON] Statut inconnu, appui ignore (en attente de la console)");
return;
}
if (isRecording) {
actionSTOP();
} else {
actionREC();
}
}
void actionREC() {
if (!client.connected() || !consoleReady) return;
Serial.println("[TX] Envoi commande REC...");
client.write(trameREC, sizeof(trameREC));
}
void actionSTOP() {
if (!client.connected() || !consoleReady) return;
Serial.println("[TX] Envoi commande STOP...");
client.write(trameSTOP_1, sizeof(trameSTOP_1));
client.write(trameSTOP_2, sizeof(trameSTOP_2));
}
// ============================================================
// RESERVES :
// - hsStep1 contient "96 CF" : identifiant constant sur les sessions
// testées, mais pas prouvé invariable en toute circonstance (ex:
// autre console).
// - Aucune notion de reprise de session dans ce protocole : toute perte
// TCP oblige à refaire tout le handshake.
// - Le blob handshake.h (capture avec MixPad) contient potentiellement
// de vraies données (positions de faders, scènes, etc.). Le rejouer
// identique à chaque reconnexion pourrait potentiellement écraser
// des réglages sur la console. Non confirmé.
// ============================================================