Created
May 31, 2020 11:26
-
-
Save EionRobb/09db3fdb71222f57fd10ea968eb20451 to your computer and use it in GitHub Desktop.
PHP script for Speakercraft MZC control via 3.5mm control port
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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