Skip to content

Instantly share code, notes, and snippets.

@EionRobb
Created May 31, 2020 11:26
Show Gist options
  • Save EionRobb/09db3fdb71222f57fd10ea968eb20451 to your computer and use it in GitHub Desktop.
Save EionRobb/09db3fdb71222f57fd10ea968eb20451 to your computer and use it in GitHub Desktop.
PHP script for Speakercraft MZC control via 3.5mm control port
<?php
//Script to control a SpeakerCraft MZC via the 3.5mm control port
// Requires a Serial/USB to quad 3.5mm adapter, eg
//https://www.aliexpress.com/item/2017184095.html
// Requires php and php-dio module
// Tested working on Windows and Linux
$config = array(
'com_port' => 11,
'devtty' => 'ttyUSB0',
'zones' => array(
1 => 'Lounge',
2 => '2',
3 => '3',
4 => '4',
5 => '5',
6 => 'Master Bedroom',
),
'sources' => array(
1 => 'Lounge Chromecast',
2 => '2',
3 => '3',
4 => '4',
5 => '5',
6 => 'Bedroom Chromecast',
),
'debug' => false,
);
if (is_file('./last_info.json')) {
$GLOBALS['last_info'] = (array) @json_decode(file_get_contents('./last_info.json'), true);
} else {
$GLOBALS['last_info'] = array();
}
if (isset($_REQUEST['getinfo'])) {
header('Content-type: application/json');
echo json_encode($GLOBALS['last_info']);
return;
}
if (!isset($_REQUEST['connect'])) {
?><!DOCTYPE html><html><head><title>Speakercraft Control</title><meta name="viewport" content="width=device-width, initial-scale=1"></head><body><h1>Speaker Control</h1><form method="post" action="?set"><table><thead><tr><th>Zone</th><th>Source</th></tr></thead><tbody><?php
foreach($config['zones'] as $zone_id => $zone_name) {
echo '<tr><td>' . htmlentities($zone_name) . '</td><td><select name="connect[' . ((int) $zone_id) . ']"><option value="-1">Off</option>';
foreach ($config['sources'] as $source_id => $source_label) {
echo '<option value="' . ((int) $source_id) . '"';
if (($GLOBALS['last_info'][$zone_id - 1]['flags'] & 0x2) &&
$GLOBALS['last_info'][$zone_id - 1]['source'] == ($source_id - 1)) {
echo ' selected="selected"';
}
echo '>' . htmlentities($source_label) . '</option>';
}
echo '</select></td></tr>';
}
?></tbody></table><input type="submit" value="Save" /></form></body></html><?php
return;
}
header('Content-type: text/plain');
//error_reporting(E_ALL);
//ini_set('display_errors', 1);
//while(@ob_end_clean());
if ($config['debug']) {
echo str_pad('', 512); flush();
function debug($out) {
echo $out . "\n"; flush();
}
} else {
function debug($out) {}
}
$dio_flags = O_RDWR;
if(strcasecmp(substr(PHP_OS, 0, 3), 'WIN') == 0) {
exec('mode com' . ((int) $config['com_port']) . ': baud=57600 data=8 stop=1 parity=n xon=off');
$device = '\\\\.\COM' . ((int) $config['com_port']);
} else {
exec('stty -F /dev/' . basename($config['devtty']) . ' cs8 -parenb -cstopb -clocal -echo -crtscts raw speed 57600');
$device = '/dev/' . basename($config['devtty']) . '';
}
$dio = $stream = dio_open($device, $dio_flags);
if (!$dio) return;
$wait_before_start = false;
if ($wait_before_start) {
sleep(1);
debug('Waiting for first 11');
$data = '';
do {
$data .= dio_read($dio);
} while(!$data || $data[strlen($data) - 1] != "\x11");
debug('Received ' . unpack('H*', $data)[1]);
}
$models = array(
0x05 => 'MZC-66',
);
do {
$data = send_command($dio, "\x41");
} while ($data == '' || $data[0] == "\x55");
debug("Model is: " . rtrim(substr($data, 4)));
// 12 05 02 33 56 65 72 73 69 6f 6e 20 32 2e 32 2e ...3Version 2.2.
// 05 MZC-66
// 02 33 - firmware version
// Reset state
// send_command($dio, "\x40");
if (!empty($_REQUEST['connect'])) {
foreach($_REQUEST['connect'] as $zone => $source) {
if ($source == -1) {
send_command($dio, "\xA1" . chr($zone - 1));
} else {
connect($dio, $zone, $source);
}
}
} else {
connect($dio, 6, 6);
connect($dio, 1, 1);
}
sleep(1);
$data = send_command($dio, "\x41");
if (!empty($_REQUEST['connect'])) {
header('Location: ' . basename(__FILE__));
}
// turn off 0xa1 0xff (all)
// send_command($dio, "\xA1\xFF");
// turn off zone 1
// send_command($dio, "\xA1\x00");
/*
0x20 - zone status
0x05 - zone id
0x00 - reserved
0x02 - Flags
b0=0/1 – Unmuted/Muted
b1=0/1 – Zone Off/Zone On
b2=0/1 – Normal Mode/Party Mode
b3=0/1 – Not Party Master/Party Master
b4-b7 – reserved
- 0x02 on 0x00 off
0x05 - source id
0x3d - volume level 0-100
0x00 - bass
0x00 - treble
0x18 - actual volume
*/
//55 0b 20 05 00 02 05 3d 00 00 18 1f - zone 6 connected to source 6
//55 0b 20 05 00 02 04 3d 00 00 18 20 - zone 6 connected to source 5
//55 0b 20 05 00 00 ff 3d 00 00 18 27
//55 0b 20 05 00 00 ff 3d 00 00 18 27
//55 0b 20 05 00 02 03 3d 00 00 18 21 - zone 6 connected to source 4
function capture_last_info($packet) {
if (preg_match('@(\x55.*?)(\x11|$)@', $packet, $match)) {
$info = $match[1];
while ($info && $info[0] != "\x11") {
if ($info[0] != "\x55") {
debug('bad info is ' . unpack('H*', $info[0])[1]);
return;
}
$len = ord($info[1]);
$chunk = substr($info, 2, $len - 1);
if ($chunk[0] == "\x20") { // status update
$zoneinfo = unpack('C/Czone/C/Cflags/Csource/Cvolume/Cbass/Ctreble/Cactualvol', $chunk);
$GLOBALS['last_info'][$zoneinfo['zone']] = $zoneinfo;
}
$info = substr($info, $len + 1);
}
file_put_contents('./last_info.json', json_encode($GLOBALS['last_info']));
}
}
function crc16_arc($data, $raw = true)
{
$crc = 0x0;
for ($pos = 0; $pos < strlen($data); $pos++)
{
$crc ^= ord($data[$pos]); // XOR byte into least sig. byte of crc
for ($i = 8; $i != 0; $i--) { // Loop over each bit
if (($crc & 0x0001) != 0) { // If the LSB is set
$crc >>= 1; // Shift right and XOR 0xA001
$crc ^= 0xA001;
} else { // Else LSB is not set
$crc >>= 1; // Just shift right
}
}
}
if ($raw) {
return pack('n', $crc);
}
return $crc;
}
function send_command($dio, $command) {
$data = '';
do {
$data .= dio_read($dio);
$byte = $data ? $data[strlen($data) - 1] : '';
} while ($byte != "\x11");
debug("Pre-received " . unpack('H*', $data)[1]);
capture_last_info($data);
if ($command == null) return;
$len = strlen($command);
$data = pack('C', $len + 3) . $command;
$tosend = $data . crc16_arc("\x01" . $data);
debug("Sending 01" . unpack('H*', $tosend)[1]);
dio_write($dio, "\x01" . $tosend, ($len + 3) + 1);
$data = '';
do {
$data .= dio_read($dio);
$byte = $data ? $data[strlen($data) - 1] : '';
} while($byte != "\x11");
debug("Received " . unpack('H*', $data)[1]);
capture_last_info($data);
$data = trim($data, "\x11\x13");
if ($data && $data[0] == "\x55") return send_command($dio, $command);
return $data;
}
function connect($dio, $zone, $source) {
$data = send_command($dio, "\xa3" . chr($zone - 1) . chr($source - 1));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment