From 28ee5c9681fe211eba164f72b89c87c31ce0c5bb Mon Sep 17 00:00:00 2001 From: coon Date: Thu, 18 Sep 2025 21:00:22 +0200 Subject: [PATCH 1/7] mixer.py: implement some cli functions --- mixer.py | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 6 deletions(-) diff --git a/mixer.py b/mixer.py index a2aeaa2..29966b2 100755 --- a/mixer.py +++ b/mixer.py @@ -16,8 +16,8 @@ class Mixer: QU_MIXER = [0x50, 0x11] MAJOR_MINOR = [0x01, 0x00] ALL_CALL_MIDI_CHANNEL = [0x7F] - - sysex_allcall = A_H_ID + QU_MIXER + MAJOR_MINOR + ALL_CALL_MIDI_CHANNEL + SYSEX_HEADER = A_H_ID + QU_MIXER + MAJOR_MINOR + SYSEX_ALL_CALL = SYSEX_HEADER + ALL_CALL_MIDI_CHANNEL def recv(self): p = mido.Parser() @@ -32,16 +32,20 @@ class Mixer: if p.pending(): msg = p.get_message() - print(msg) - print("Hex: ", " ".join(f"{b:02X}" for b in msg.data)) + + # print(msg.type) + print(vars(msg)) + + if hasattr(msg, "data"): + print("Hex: ", " ".join(f"{b:02X}" for b in msg.data)) return msg def get_system_state(self): - id = 0x10 + msg_id = 0x10 i_pad_flag = 0x01 - data = self.sysex_allcall + [id, i_pad_flag] + data = self.SYSEX_ALL_CALL + [msg_id, i_pad_flag] msg = mido.Message('sysex', data=data) msg_bytes = bytes(msg.bytes()) @@ -82,6 +86,65 @@ class Mixer: print(f"Model: {QuModel(i_pad_flag)}") print(f"Firmware Version: {major_ver}.{minor_ver}") + def get_name_from_qu(self, channel_no): + msg_id = 0x01 + + data = self.SYSEX_HEADER + [int(0), msg_id, int(channel_no)] + msg = mido.Message('sysex', data=data) + msg_bytes = bytes(msg.bytes()) + + self.sock.sendall(msg_bytes) + print("Sent:", " ".join(f"{b:02X}" for b in msg_bytes)) + + response = self.recv() + + sysex_header = response.data[:8] + + midi_channel = int(response.data[7]) + id = int(response.data[8]) + channel_name = bytes(response.data[9:]).decode("utf-8") + + print(f"sysex_header: {sysex_header}") + print(f"MIDI channel: {midi_channel}") + print(f"ID: 0x{id:02X}") + print(f"Channel Name: {channel_name}") + + def nrpn_parameter_control(self, midi_ch, mixer_ch, id, va, vx): + a = mido.Message('control_change', channel=midi_ch, control=0x63, value=mixer_ch) + b = mido.Message('control_change', channel=midi_ch, control=0x62, value=id) + c = mido.Message('control_change', channel=midi_ch, control=0x06, value=va) + d = mido.Message('control_change', channel=midi_ch, control=0x26, value=vx) + + msg_bytes = bytes(a.bytes() + b.bytes() + c.bytes() + d.bytes()) + + print (' '.join(f"{b:02X}" for b in msg_bytes)) + self.sock.sendall(msg_bytes) + + def shutdown(self): + self.nrpn_parameter_control(midi_ch=0, mixer_ch=0, id=0x5F, va=0x00, vx=0x00) + + def set_bank_1(self): + msb = mido.Message('control_change', channel=0, control=0x00, value=0x00) + lsb = mido.Message('control_change', channel=0, control=0x20, value=0x00) + + msg_bytes = bytes(msb.bytes() + lsb.bytes()) + + print(' '.join(f"{b:02X}" for b in msg_bytes)) + self.sock.sendall(msg_bytes) + + def scene_recall(self, scene_id): + self.set_bank_1() + msb = mido.Message('program_change', channel=0, program=scene_id) + + msg_bytes = bytes(msb.bytes()) + + print(' '.join(f"{b:02X}" for b in msg_bytes)) + self.sock.sendall(msg_bytes) + + def watch(self): + while True: + self.recv() + def main(): parser = argparse.ArgumentParser(description="Allen & Heath Qu Remote Control") parser.add_argument("ip", help="IP of the mixer") @@ -98,9 +161,29 @@ def main(): if args.command: print(f"Command: {args.command}") + if args.command == 'get_name_from_qu': + mixer = Mixer(args.ip, MIXER_PORT) + mixer.get_name_from_qu(0) + if args.command == 'get_system_state': mixer = Mixer(args.ip, MIXER_PORT) mixer.get_system_state() + if args.command == 'shutdown': + mixer = Mixer(args.ip, MIXER_PORT) + mixer.shutdown() + + if args.command == 'set_default_layer': + mixer = Mixer(args.ip, MIXER_PORT) + # mixer.set_layer() + + if args.command == 'watch': + mixer = Mixer(args.ip, MIXER_PORT) + mixer.watch() + + if args.command == 'scene_recall': + mixer = Mixer(args.ip, MIXER_PORT) + mixer.scene_recall(0) + if __name__ == '__main__': main() From a4cc88fc61f6d357639ac07788d3ee1b1f41dee1 Mon Sep 17 00:00:00 2001 From: coon Date: Thu, 18 Sep 2025 21:20:48 +0200 Subject: [PATCH 2/7] A_H -> ALLEN_HEATH --- mixer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mixer.py b/mixer.py index 29966b2..0d14189 100755 --- a/mixer.py +++ b/mixer.py @@ -12,11 +12,11 @@ class Mixer: def __init__(self, ip, port): self.sock = socket.create_connection((ip, port)) - A_H_ID = [0x00, 0x00, 0x1A] + ALLEN_HEATH_ID = [0x00, 0x00, 0x1A] QU_MIXER = [0x50, 0x11] MAJOR_MINOR = [0x01, 0x00] ALL_CALL_MIDI_CHANNEL = [0x7F] - SYSEX_HEADER = A_H_ID + QU_MIXER + MAJOR_MINOR + SYSEX_HEADER = ALLEN_HEATH_ID + QU_MIXER + MAJOR_MINOR SYSEX_ALL_CALL = SYSEX_HEADER + ALL_CALL_MIDI_CHANNEL def recv(self): From 0af2dabf6b0650a68de014bca50cb57c31226bbc Mon Sep 17 00:00:00 2001 From: coon Date: Thu, 18 Sep 2025 21:26:21 +0200 Subject: [PATCH 3/7] use match / case syntax --- mixer.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/mixer.py b/mixer.py index 0d14189..07645b9 100755 --- a/mixer.py +++ b/mixer.py @@ -161,29 +161,30 @@ def main(): if args.command: print(f"Command: {args.command}") - if args.command == 'get_name_from_qu': - mixer = Mixer(args.ip, MIXER_PORT) - mixer.get_name_from_qu(0) + match args.command: + case 'get_name_from_qu': + mixer = Mixer(args.ip, MIXER_PORT) + mixer.get_name_from_qu(0) - if args.command == 'get_system_state': - mixer = Mixer(args.ip, MIXER_PORT) - mixer.get_system_state() + case 'get_system_state': + mixer = Mixer(args.ip, MIXER_PORT) + mixer.get_system_state() - if args.command == 'shutdown': - mixer = Mixer(args.ip, MIXER_PORT) - mixer.shutdown() + case 'shutdown': + mixer = Mixer(args.ip, MIXER_PORT) + mixer.shutdown() - if args.command == 'set_default_layer': - mixer = Mixer(args.ip, MIXER_PORT) - # mixer.set_layer() + case 'set_default_layer': + mixer = Mixer(args.ip, MIXER_PORT) + # mixer.set_layer() - if args.command == 'watch': - mixer = Mixer(args.ip, MIXER_PORT) - mixer.watch() + case 'watch': + mixer = Mixer(args.ip, MIXER_PORT) + mixer.watch() - if args.command == 'scene_recall': - mixer = Mixer(args.ip, MIXER_PORT) - mixer.scene_recall(0) + case 'scene_recall': + mixer = Mixer(args.ip, MIXER_PORT) + mixer.scene_recall(0) if __name__ == '__main__': main() From 38a3d23b917a6a432fcd3e33b27bcde3917f4a2b Mon Sep 17 00:00:00 2001 From: coon Date: Thu, 18 Sep 2025 21:34:16 +0200 Subject: [PATCH 4/7] implement set_default_mixer command --- mixer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mixer.py b/mixer.py index 07645b9..aca7804 100755 --- a/mixer.py +++ b/mixer.py @@ -117,7 +117,7 @@ class Mixer: msg_bytes = bytes(a.bytes() + b.bytes() + c.bytes() + d.bytes()) - print (' '.join(f"{b:02X}" for b in msg_bytes)) + print(' '.join(f"{b:02X}" for b in msg_bytes)) self.sock.sendall(msg_bytes) def shutdown(self): @@ -176,7 +176,7 @@ def main(): case 'set_default_layer': mixer = Mixer(args.ip, MIXER_PORT) - # mixer.set_layer() + mixer.scene_recall(0) case 'watch': mixer = Mixer(args.ip, MIXER_PORT) From c16c482eb80c0e0b1f7921560a414b2c8035c122 Mon Sep 17 00:00:00 2001 From: coon Date: Thu, 18 Sep 2025 23:08:42 +0200 Subject: [PATCH 5/7] enhance arg parsing --- mixer.py | 45 +++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/mixer.py b/mixer.py index aca7804..1ca6602 100755 --- a/mixer.py +++ b/mixer.py @@ -33,7 +33,7 @@ class Mixer: if p.pending(): msg = p.get_message() - # print(msg.type) + print(msg.type) print(vars(msg)) if hasattr(msg, "data"): @@ -86,15 +86,19 @@ class Mixer: print(f"Model: {QuModel(i_pad_flag)}") print(f"Firmware Version: {major_ver}.{minor_ver}") - def get_name_from_qu(self, channel_no): + def get_name_from_qu(self, channel_no, name_to_set): msg_id = 0x01 data = self.SYSEX_HEADER + [int(0), msg_id, int(channel_no)] + + if name_to_set: + data += list(name_to_set.encode('utf-8')) + msg = mido.Message('sysex', data=data) msg_bytes = bytes(msg.bytes()) - self.sock.sendall(msg_bytes) print("Sent:", " ".join(f"{b:02X}" for b in msg_bytes)) + self.sock.sendall(msg_bytes) response = self.recv() @@ -133,6 +137,8 @@ class Mixer: self.sock.sendall(msg_bytes) def scene_recall(self, scene_id): + print(f"scene_recall: scene_id={scene_id}", scene_id) + self.set_bank_1() msb = mido.Message('program_change', channel=0, program=scene_id) @@ -148,23 +154,34 @@ class Mixer: def main(): parser = argparse.ArgumentParser(description="Allen & Heath Qu Remote Control") parser.add_argument("ip", help="IP of the mixer") - parser.add_argument("command", help="Command to execute") + + subparsers = parser.add_subparsers(dest="command", required=True, help="Available commands") + + channel_naming_parser = subparsers.add_parser("get_name_from_qu", help="Channel naming") + channel_naming_parser.add_argument("channel_id", type=int, choices=range(0, 16), help="Number of channel to get or set its name") + channel_naming_parser.add_argument("-n", "--name", help="Channel name to set") + + subparsers.add_parser("get_system_state", help="Get system information") + subparsers.add_parser("shutdown", help="Shut down the mixer") + + scene_parser = subparsers.add_parser("scene_recall", help="Recall a specific scene") + scene_parser.add_argument("scene_number", type=int, choices=range(0, 100), help="Scene number to recall") + + subparsers.add_parser("scene_recall_default", help="Set the default scene 0") + subparsers.add_parser("watch", help="Just receive data from mixer and print it to console") args = parser.parse_args() - if not args.ip: - print("ip is missing!") - return 1 - print(f"IP: {args.ip}") if args.command: print(f"Command: {args.command}") + print(f"Args: {vars(args)}") match args.command: case 'get_name_from_qu': mixer = Mixer(args.ip, MIXER_PORT) - mixer.get_name_from_qu(0) + mixer.get_name_from_qu(args.channel_id, args.name) case 'get_system_state': mixer = Mixer(args.ip, MIXER_PORT) @@ -174,7 +191,11 @@ def main(): mixer = Mixer(args.ip, MIXER_PORT) mixer.shutdown() - case 'set_default_layer': + case 'scene_recall': + mixer = Mixer(args.ip, MIXER_PORT) + mixer.scene_recall(args.scene_number) + + case 'scene_recall_default': mixer = Mixer(args.ip, MIXER_PORT) mixer.scene_recall(0) @@ -182,9 +203,5 @@ def main(): mixer = Mixer(args.ip, MIXER_PORT) mixer.watch() - case 'scene_recall': - mixer = Mixer(args.ip, MIXER_PORT) - mixer.scene_recall(0) - if __name__ == '__main__': main() From 8bce5aa8991d3eee6ccb79fd5ad293613e62c5c5 Mon Sep 17 00:00:00 2001 From: coon Date: Fri, 19 Sep 2025 19:48:51 +0200 Subject: [PATCH 6/7] DRY: create mixer once --- mixer.py | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/mixer.py b/mixer.py index 1ca6602..2bdcca3 100755 --- a/mixer.py +++ b/mixer.py @@ -173,35 +173,19 @@ def main(): args = parser.parse_args() print(f"IP: {args.ip}") + mixer = Mixer(args.ip, MIXER_PORT) if args.command: print(f"Command: {args.command}") print(f"Args: {vars(args)}") match args.command: - case 'get_name_from_qu': - mixer = Mixer(args.ip, MIXER_PORT) - mixer.get_name_from_qu(args.channel_id, args.name) - - case 'get_system_state': - mixer = Mixer(args.ip, MIXER_PORT) - mixer.get_system_state() - - case 'shutdown': - mixer = Mixer(args.ip, MIXER_PORT) - mixer.shutdown() - - case 'scene_recall': - mixer = Mixer(args.ip, MIXER_PORT) - mixer.scene_recall(args.scene_number) - - case 'scene_recall_default': - mixer = Mixer(args.ip, MIXER_PORT) - mixer.scene_recall(0) - - case 'watch': - mixer = Mixer(args.ip, MIXER_PORT) - mixer.watch() + case 'get_name_from_qu': mixer.get_name_from_qu(args.channel_id, args.name) + case 'get_system_state': mixer.get_system_state() + case 'shutdown': mixer.shutdown() + case 'scene_recall': mixer.scene_recall(args.scene_number) + case 'scene_recall_default': mixer.scene_recall(0) + case 'watch': mixer.watch() if __name__ == '__main__': main() From ebef9e1a54f6619dae5615b46ee3640042db74ea Mon Sep 17 00:00:00 2001 From: coon Date: Fri, 19 Sep 2025 20:19:22 +0200 Subject: [PATCH 7/7] add and use enum for SysEx message ids --- mixer.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/mixer.py b/mixer.py index 2bdcca3..0bccd66 100755 --- a/mixer.py +++ b/mixer.py @@ -19,6 +19,14 @@ class Mixer: SYSEX_HEADER = ALLEN_HEATH_ID + QU_MIXER + MAJOR_MINOR SYSEX_ALL_CALL = SYSEX_HEADER + ALL_CALL_MIDI_CHANNEL + class SysExMessageId(Enum): + GET_NAME_FROM_QU_REQUEST = 0x01, + GET_NAME_FROM_QU_RESPONSE = 0x02, + GET_SYSTEM_STATE_REQUEST = 0x10, + GET_SYSTEM_STATE_RESPONSE = 0x11, + GET_METER_DATA_REQUEST = 0x12, + GET_METER_DATA_RESPONSE = 0x13 + def recv(self): p = mido.Parser() @@ -42,7 +50,7 @@ class Mixer: return msg def get_system_state(self): - msg_id = 0x10 + msg_id = self.SysExMessageId.GET_SYSTEM_STATE_REQUEST i_pad_flag = 0x01 data = self.SYSEX_ALL_CALL + [msg_id, i_pad_flag] @@ -87,7 +95,7 @@ class Mixer: print(f"Firmware Version: {major_ver}.{minor_ver}") def get_name_from_qu(self, channel_no, name_to_set): - msg_id = 0x01 + msg_id = self.SysExMessageId.GET_NAME_FROM_QU_REQUEST data = self.SYSEX_HEADER + [int(0), msg_id, int(channel_no)]