serial.pl 8.2 KB

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