serial.pl 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. use File::Basename;
  5. use File::Temp qw( tempfile );
  6. use IO::Select;
  7. use IO::Socket::UNIX;
  8. use Device::SerialPort;
  9. use Time::HiRes qw( usleep sleep time );
  10. use IPC::System::Simple qw( system capture );
  11. # Local modules
  12. use Cwd qw(abs_path);
  13. use File::Basename qw(dirname);
  14. use lib dirname(abs_path($0));
  15. use DMX;
  16. # Prototypes
  17. sub sendQuery($$);
  18. sub clearBuffer($);
  19. sub collectUntil($$);
  20. # Device parameters
  21. my ($DEV, $PORT, $BLUETOOTH, $CRLF, $DELIMITER, %CMDS, %STATUS_CMDS);
  22. if (basename($0) =~ /PROJECTOR/i) {
  23. $DEV = 'Projector';
  24. $PORT = '/dev/tty.usbserial-A5006xbj';
  25. $BLUETOOTH = 0;
  26. $CRLF = "\r\n";
  27. $DELIMITER = ':';
  28. %CMDS = (
  29. 'INIT' => '',
  30. 'ON' => 'PWR ON',
  31. 'OFF' => 'PWR OFF',
  32. 'STATUS' => 'PWR?',
  33. 'DYNAMIC' => 'CMODE 06',
  34. 'LIVING_ROOM' => 'CMODE 0C',
  35. 'NATURAL' => 'CMODE 07',
  36. 'THEATER' => 'CMODE 05',
  37. 'THEATER_BLACK_1' => 'CMODE 09',
  38. 'THEATER_BLACK_2' => 'CMODE 0A',
  39. 'XV' => 'CMODE 0B',
  40. 'COLOR' => 'CMODE?',
  41. 'HDMI_1' => 'SOURCE 30',
  42. 'HDMI_2' => 'SOURCE A0',
  43. 'VGA' => 'SOURCE 20',
  44. 'INPUT' => 'SOURCE?',
  45. );
  46. %STATUS_CMDS = (
  47. 'STATUS' => { 'MATCH' => [ qr/^PWR=/, qr/PWR=01/ ] },
  48. 'COLOR' => { 'EVAL' => [ qr/^CMODE=[0-9A-Z]{2}$/i, 'if ($a =~ /06$/i) { $a = "DYNAMIC" } elsif ($a =~ /0C$/i) { $a = "LIVING_ROOM" } elsif ($a =~ /07$/i) { $a = "NATURAL" } elsif ($a =~ /05$/i) { $a = "THEATER" } elsif ($a =~ /09$/i) { $a = "THEATER_BLACK_1" } elsif ($a =~ /0A$/i) { $a = "THEATER_BLACK_2" } elsif ($a =~ /0B$/i) { $a = "XV" }' ] },
  49. 'INPUT' => { 'EVAL' => [ qr/^SOURCE=[0-9A-Z]{2}$/i, 'if ($a =~ /30$/i) { $a = "HDMI_1" } elsif ($a =~ /A0$/i) { $a = "HDMI_2" } elsif ($a =~ /20$/i) { $a = "VGA" }' ] },
  50. );
  51. } elsif (basename($0) =~ /AMPLIFIER/i) {
  52. $DEV = 'Amplifier';
  53. $PORT = '/dev/tty.usbserial-A5006x9u';
  54. $BLUETOOTH = 0;
  55. $CRLF = "\r";
  56. $DELIMITER = "\r";
  57. %CMDS = (
  58. 'INIT' => '',
  59. 'ON' => 'PWON',
  60. 'OFF' => 'PWSTANDBY',
  61. 'STATUS' => 'PW?',
  62. 'VOL' => 'MV?',
  63. 'VOL+' => 'MVUP',
  64. 'VOL-' => 'MVDOWN',
  65. 'MUTE' => 'MUON',
  66. 'UNMUTE' => 'MUOFF',
  67. 'INPUT' => 'SI?',
  68. 'TV' => 'SITV',
  69. 'DVD' => 'SIDVD',
  70. 'MODE' => 'MS?',
  71. 'SURROUND' => 'MSDOLBY DIGITAL',
  72. 'STEREO' => 'MS7CH STEREO',
  73. 'TV' => 'SITV',
  74. 'INPUT' => 'SI?',
  75. );
  76. %STATUS_CMDS = (
  77. 'STATUS' => { 'MATCH' => [ qr/^PW/, qr/$CMDS{'ON'}/ ] },
  78. 'MODE' => { 'EVAL' => [ qr/^MS/, 'if ($a =~ /STEREO/i) { $a = "STEREO" } elsif ($a =~ /MS(?:DOLBY|DTS)/i) { $a = "SURROUND" }' ] },
  79. 'VOL' => { 'EVAL' => [ qr/^MV/, '$a =~ s/^MV//; if (length($a) > 2) { $a =~ s/(\d\d)(\d)/$1.$2/ }' ] },
  80. 'INPUT' => { 'REPLACE' => qr/^SI(.*)/ },
  81. );
  82. } elsif (basename($0) =~ /TV/i) {
  83. $DEV = 'TV';
  84. $PORT = '/dev/tty.' . $DEV . '-DevB';
  85. $BLUETOOTH = 1;
  86. $CRLF = "\r";
  87. $DELIMITER = "\r";
  88. %CMDS = (
  89. 'INIT' => 'RSPW1',
  90. 'ON' => 'POWR1',
  91. 'OFF' => 'POWR0',
  92. 'STATUS' => 'POWR?',
  93. 'VOL+' => 'MVUP',
  94. 'VOL-' => 'MVDOWN',
  95. 'MUTE' => 'MUTE1',
  96. 'UNMUTE' => 'MUTE2',
  97. 'TV' => 'IAVD0',
  98. 'PLEX' => 'IAVD7',
  99. 'VOL_CHECK' => 'VOLM?',
  100. 'VOL6' => 'VOLM6',
  101. 'VOL12' => 'VOLM12',
  102. 'VOL24' => 'VOLM24',
  103. 'VOL+' => 'VOLM',
  104. 'VOL-' => 'VOLM'
  105. );
  106. %STATUS_CMDS = ('STATUS' => { 'EQUAL' => '1' },);
  107. } else {
  108. die("No device specified\n");
  109. }
  110. # App config
  111. my $DATA_DIR = DMX::dataDir();
  112. my $CMD_FILE = $DATA_DIR . uc($DEV) . '.socket';
  113. my $BT_CHECK = $ENV{'HOME'} . '/bin/btcheck';
  114. my $DELAY_STATUS = 1;
  115. my $BYTE_TIMEOUT = 50;
  116. my $SILENCE_TIMEOUT = $BYTE_TIMEOUT * 10;
  117. # Debug
  118. my $DEBUG = 0;
  119. if ($ENV{'DEBUG'}) {
  120. $DEBUG = $ENV{'DEBUG'};
  121. print STDERR "Debug enabled\n";
  122. }
  123. # Command-line arguments
  124. my ($DELAY) = @ARGV;
  125. if (!$DELAY) {
  126. $DELAY = 15;
  127. }
  128. # Sanity check
  129. if (!-r $PORT) {
  130. die('Serial port not available: ' . $PORT . "\n");
  131. } elsif (!-d $DATA_DIR) {
  132. die('Data directory not available: ' . $DATA_DIR . "\n");
  133. }
  134. # Wait for the serial port to become available
  135. if ($BLUETOOTH) {
  136. system($BT_CHECK, $DEV);
  137. if ($? != 0) {
  138. sleep($DELAY_STATUS);
  139. die('Bluetooth device "' . $DEV . "\" not available\n");
  140. }
  141. }
  142. # Socket init
  143. my $select = DMX::selectSock($CMD_FILE);
  144. # Port init
  145. my $port = new Device::SerialPort($PORT)
  146. or die('Unable to open serial connection: ' . $PORT . ": ${!}\n");
  147. $port->read_const_time($BYTE_TIMEOUT);
  148. # Init (clear any previous state)
  149. sendQuery($port, $CMDS{'INIT'});
  150. # State
  151. my $lastStatus = 0;
  152. my %STATUS = ();
  153. foreach my $cmd (keys(%STATUS_CMDS)) {
  154. my %tmp = ('status' => 0, 'last' => 0);
  155. $tmp{'path'} = $DATA_DIR . uc($DEV);
  156. if ($cmd ne 'STATUS') {
  157. $tmp{'path'} .= '_' . uc($cmd);
  158. }
  159. $STATUS{$cmd} = \%tmp;
  160. }
  161. # Loop forever
  162. while (1) {
  163. # Calculate our next timeout
  164. # Hold on select() but not more than $DELAY_STATUS after our last update
  165. my $timeout = ($lastStatus + $DELAY_STATUS) - time();
  166. if ($timeout < 0) {
  167. $timeout = 0;
  168. }
  169. if ($DEBUG > 1) {
  170. print STDERR 'Waiting for commands with timeout: ' . $timeout . "\n";
  171. }
  172. # Check for queued commands
  173. my @ready_clients = $select->can_read($timeout);
  174. foreach my $fh (@ready_clients) {
  175. # Ensure we won't block on recv()
  176. $fh->blocking(0);
  177. # Grab the inbound text
  178. while (defined($fh->recv(my $text, DMX::maxCmdLen()))) {
  179. # Clean the input data
  180. $text =~ s/^\s+//;
  181. $text =~ s/\s+$//;
  182. if ($DEBUG) {
  183. print STDERR 'Got command: ' . $text . "\n";
  184. }
  185. # Only accept valid commands
  186. my $cmd = undef();
  187. foreach my $name (keys(%CMDS)) {
  188. if ($name eq $text) {
  189. $cmd = $name;
  190. last;
  191. }
  192. }
  193. # Send commands to serial device
  194. if ($cmd) {
  195. if ($DEBUG) {
  196. print STDERR 'Sending command: ' . $cmd . "\n";
  197. }
  198. my $result = sendQuery($port, $CMDS{$cmd});
  199. if ($DEBUG && $result) {
  200. print STDERR "\tGot result: " . $result . "\n";
  201. }
  202. }
  203. }
  204. }
  205. # Read periodic data, but not too frequently
  206. if (time() > $lastStatus + $DELAY_STATUS) {
  207. # Record the last status update time
  208. $lastStatus = time();
  209. foreach my $cmd (keys(%STATUS_CMDS)) {
  210. # Save the previous status
  211. $STATUS{$cmd}->{'last'} = $STATUS{$cmd}->{'status'};
  212. # Less typing
  213. my $scmd = $STATUS_CMDS{$cmd};
  214. # Query
  215. if (!defined($CMDS{$cmd})) {
  216. die('No such command: ' . $cmd . "\n");
  217. }
  218. my $result = sendQuery($port, $CMDS{$cmd});
  219. # Process the result as requested
  220. if ($result) {
  221. if ($scmd->{'MATCH'}) {
  222. if ($result =~ $scmd->{'MATCH'}[0]) {
  223. if ($result =~ $scmd->{'MATCH'}[1]) {
  224. $STATUS{$cmd}->{'status'} = 1;
  225. } else {
  226. $STATUS{$cmd}->{'status'} = 0;
  227. }
  228. }
  229. } elsif ($scmd->{'REPLACE'}) {
  230. if ($result =~ $scmd->{'REPLACE'}) {
  231. $STATUS{$cmd}->{'status'} = $result;
  232. $STATUS{$cmd}->{'status'} =~ s/$scmd->{'REPLACE'}/$1/;
  233. }
  234. } elsif ($scmd->{'EVAL'}) {
  235. my $a = $result;
  236. if ($result =~ $scmd->{'EVAL'}[0]) {
  237. eval($scmd->{'EVAL'}[1]);
  238. $STATUS{$cmd}->{'status'} = $a;
  239. }
  240. } else {
  241. die('Invalid status match type "' . (keys(%{$scmd}))[0] . '" in command: ' . $cmd . "\n");
  242. }
  243. }
  244. # Ensure the data is clean
  245. $STATUS{$cmd}->{'status'} =~ s/[^\w\.\-]/_/g;
  246. # If something has changed, save the state to disk
  247. if ($STATUS{$cmd}->{'status'} ne $STATUS{$cmd}->{'last'}) {
  248. if ($DEBUG) {
  249. print STDERR 'New ' . uc($DEV) . ' status: ' . $cmd . ' => ' . $STATUS{$cmd}->{'status'} . "\n";
  250. }
  251. my ($fh, $tmp) = tempfile($STATUS{$cmd}->{'path'} . '.XXXXXXXX', 'UNLINK' => 0);
  252. print $fh $STATUS{$cmd}->{'status'} . "\n";
  253. close($fh);
  254. rename($tmp, $STATUS{$cmd}->{'path'});
  255. }
  256. }
  257. }
  258. }
  259. # Cleanup
  260. undef($select);
  261. $port->close();
  262. undef($port);
  263. exit(0);
  264. sub sendQuery($$) {
  265. my ($port, $query) = @_;
  266. # Enforce an inter-command delay
  267. usleep($SILENCE_TIMEOUT);
  268. # Read until the queue is clear (i.e. no data available)
  269. # Since we don't have flow control this always causes one read timeout
  270. $port->lookclear();
  271. clearBuffer($port);
  272. # Send the command
  273. my $bytes = $port->write($query . $CRLF);
  274. if ($DEBUG) {
  275. print STDERR "\tWrote (" . $bytes . '): ' . $query . "\n";
  276. }
  277. # Wait for a reply (delimited or timeout)
  278. return collectUntil($port, $DELIMITER);
  279. }
  280. sub clearBuffer($) {
  281. my ($port) = @_;
  282. my $byte = 1;
  283. while (length($byte) > 0) {
  284. $byte = $port->read(1);
  285. if ($DEBUG && length($byte)) {
  286. print STDERR "\tIgnored: " . $byte . "\n";
  287. }
  288. }
  289. }
  290. sub collectUntil($$) {
  291. my ($port, $char) = @_;
  292. if (length($char) != 1) {
  293. die('Invalid collection delimiter: ' . $char . "\n");
  294. }
  295. # This byte-by-byte reading is not efficient, but it's safe
  296. # Allow reading forever as long as we don't exceed the silence timeout
  297. my $count = 0;
  298. my $string = '';
  299. while ($count < $SILENCE_TIMEOUT / $BYTE_TIMEOUT) {
  300. my $byte = $port->read(1);
  301. if (length($byte)) {
  302. $count = 0;
  303. $string .= $byte;
  304. if ($DEBUG > 1) {
  305. print STDERR "\tRead: " . $byte . "\n";
  306. }
  307. if ($byte eq $char) {
  308. last;
  309. }
  310. } else {
  311. $count++;
  312. }
  313. }
  314. # Return undef if there was no data (as opposed to just a delimiter and/or whitespace)
  315. if (length($string) < 1) {
  316. if ($DEBUG) {
  317. print STDERR "Read: <NO DATA>\n";
  318. }
  319. return undef();
  320. }
  321. # Translate CRLF and CR to LF
  322. $string =~ s/\r\n/\n/g;
  323. $string =~ s/\r/\n/g;
  324. # Strip the trailing delimiter
  325. $string =~ s/${char}$//;
  326. # Strip leading or trailing whitespace
  327. $string =~ s/^\s+//;
  328. $string =~ s/\s+$//;
  329. # Return our clean string
  330. if ($DEBUG) {
  331. print STDERR 'Read: ' . $string . "\n";
  332. }
  333. return $string;
  334. }