DMX.pm 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. use POSIX;
  5. use File::Temp;
  6. use IO::Select;
  7. use IO::Socket::UNIX;
  8. use Sys::Hostname;
  9. # Package name
  10. package DMX;
  11. # Debug
  12. my $DEBUG = 0;
  13. if ($ENV{'DEBUG'}) {
  14. $DEBUG = 1;
  15. }
  16. # Config
  17. my $SOCK_TIMEOUT = 5;
  18. my $MAX_CMD_LEN = 16384;
  19. my $TEMP_DIR = `getconf DARWIN_USER_TEMP_DIR`;
  20. chomp($TEMP_DIR);
  21. my $DATA_DIR = $TEMP_DIR . 'plexMonitor/';
  22. my $DMX_SOCK = $DATA_DIR . 'DMX.socket';
  23. my $SUB_SOCK = $DATA_DIR . 'STATE.socket';
  24. my %CHANNEL_ADJ = (
  25. '13' => 1.00,
  26. '14' => 1.14,
  27. '15' => 1.19,
  28. );
  29. # Sanity check
  30. if (!-d $DATA_DIR) {
  31. # Allow use on dev systems without the full environment
  32. my $hostname = Sys::Hostname::hostname();
  33. if ($hostname =~ /baldwin/i || $hostname =~ /archer/i) {
  34. print STDERR 'Ignoring missing DATA_DIR due to hostname: ' . $hostname . ". Using TEMP_DIR directly...\n";
  35. $DATA_DIR = $TEMP_DIR;
  36. } else {
  37. die('Data directory not available: ' . $DATA_DIR . "\n");
  38. }
  39. }
  40. # State
  41. my $DMX_FH = undef();
  42. my $SELECT = undef();
  43. my %SAY = ();
  44. # Data directory
  45. sub dataDir() {
  46. return $DATA_DIR;
  47. }
  48. # Maximum command length
  49. sub maxCmdLen() {
  50. return $MAX_CMD_LEN;
  51. }
  52. # Generic client socket
  53. sub clientSock($) {
  54. my ($path) = @_;
  55. my $sock = IO::Socket::UNIX->new(
  56. 'Peer' => $path,
  57. 'Type' => IO::Socket::UNIX::SOCK_DGRAM,
  58. 'Timeout' => $SOCK_TIMEOUT,
  59. ) or die('Unable to open client socket: ' . $path . ": ${@}\n");
  60. return $sock;
  61. }
  62. # Generic server socket
  63. sub serverSock($) {
  64. my ($path) = @_;
  65. if (-e $path) {
  66. unlink($path);
  67. }
  68. my $sock = IO::Socket::UNIX->new(
  69. 'Local' => $path,
  70. 'Type' => IO::Socket::UNIX::SOCK_DGRAM,
  71. ) or die('Unable to open server socket: ' . $path . ": ${@}\n");
  72. return $sock;
  73. }
  74. # Select on a server socket
  75. sub selectSock($) {
  76. my ($path) = @_;
  77. my $sock = serverSock($path);
  78. my $sel = IO::Select->new($sock)
  79. or die('Unable to select socket: ' . $path . ": ${!}\n");
  80. return $sel;
  81. }
  82. # DMX socket init
  83. sub dmxSock() {
  84. if (!defined($DMX_FH)) {
  85. $DMX_FH = clientSock($DMX_SOCK);
  86. }
  87. return $DMX_FH;
  88. }
  89. # Subscribe to state updates
  90. sub stateSubscribe($) {
  91. my ($STATE_SOCK) = @_;
  92. my $sub_fh = clientSock($SUB_SOCK);
  93. $sub_fh->send($STATE_SOCK)
  94. or die('Unable to subscribe: ' . $! . "\n");
  95. shutdown($sub_fh, 2);
  96. undef($sub_fh);
  97. }
  98. # Alias for selectSock() that also sets the global $SELECT pointer
  99. sub stateSocket($) {
  100. my ($STATE_SOCK) = @_;
  101. $SELECT = selectSock($STATE_SOCK);
  102. return $SELECT;
  103. }
  104. # Parse the state->client comm string
  105. sub parseState($$$) {
  106. my ($text, $exists, $mtime) = @_;
  107. my $cmdState = undef();
  108. if (!defined($exists)) {
  109. my %tmp = ();
  110. $exists = \%tmp;
  111. }
  112. if (!defined($mtime)) {
  113. my %tmp = ();
  114. $mtime = \%tmp;
  115. }
  116. # Parse the string
  117. %{$exists} = ();
  118. %{$mtime} = ();
  119. my @lines = split("\n", $text);
  120. $cmdState = shift(@lines);
  121. if (!defined($cmdState)) {
  122. print STDERR 'State parse error: ' . $text . "\n";
  123. next;
  124. }
  125. if (scalar(@lines)) {
  126. foreach my $line (@lines) {
  127. my ($name, $value, $time) = $line =~ /(\w+)\|([^|]*)\|(\d*)/;
  128. if (!defined($name) || !defined($value) || !defined($time)) {
  129. print STDERR 'State parse error (exists): ' . $text . "\n";
  130. next;
  131. }
  132. $exists->{$name} = $value ? $value : 0;
  133. $mtime->{$name} = $time ? $time : 0;
  134. }
  135. }
  136. if ($DEBUG) {
  137. my @exists_tmp = ();
  138. foreach my $key (sort(keys(%{$exists}))) {
  139. push(@exists_tmp, $key . ":\t" . $exists->{$key} . ' @ ' . $mtime->{$key});
  140. }
  141. print STDERR 'Got state: ' . $cmdState . "\n" . join("\n", @exists_tmp) . "\n";
  142. }
  143. # Translate INIT to OFF
  144. if ($cmdState eq 'INIT') {
  145. $cmdState = 'OFF';
  146. }
  147. # Return
  148. return $cmdState;
  149. }
  150. sub dim($) {
  151. my ($args) = @_;
  152. if (!defined($args->{'delay'})) {
  153. $args->{'delay'} = 0;
  154. }
  155. if (!defined($args->{'channel'}) || !defined($args->{'time'}) || !defined($args->{'value'})) {
  156. die('Invalid command for socket: ' . join(', ', keys(%{$args})) . ': ' . join(', ', values(%{$args})) . "\n");
  157. }
  158. # Silently skip invalid channels
  159. if ($args->{'channel'} < 0) {
  160. return;
  161. }
  162. # Adjust for the color curve (for channels where we have such data)
  163. my $value = $args->{'value'};
  164. if ($CHANNEL_ADJ{ $args->{'channel'} }) {
  165. $value *= $CHANNEL_ADJ{ $args->{'channel'} };
  166. }
  167. # 8-bit values
  168. $value = int($value);
  169. if ($value > 255) {
  170. $value = 255;
  171. } elsif ($value < 0) {
  172. $value = 0;
  173. }
  174. # We only deal in ints
  175. my $time = int($args->{'time'});
  176. my $delay = int($args->{'delay'});
  177. my $cmd = join(':', $args->{'channel'}, $time, $value, $delay);
  178. dmxSock();
  179. $DMX_FH->send($cmd)
  180. or die('Unable to write command to DMX socket: ' . $cmd . ": ${!}\n");
  181. }
  182. sub printDataset($) {
  183. my ($data_set) = @_;
  184. my $sum = 0;
  185. foreach my $data (@{$data_set}) {
  186. $sum += $data->{'value'};
  187. my $str = $data->{'channel'} . ' => ' . int($data->{'value'}) . ' @ ' . $data->{'time'};
  188. if ($data->{'delay'}) {
  189. $str .= ' (Delay: ' . $data->{'delay'} . ')';
  190. }
  191. print STDERR "\t" . $str . "\n";
  192. }
  193. print STDERR "\tTotal: " . int($sum) . "\n";
  194. }
  195. sub applyDataset($$$) {
  196. my ($data_set, $state, $file) = @_;
  197. if (!defined($state)) {
  198. $state = 'NULL';
  199. }
  200. # Debug
  201. if ($DEBUG) {
  202. print STDERR 'State: ' . $state . "\n";
  203. printDataset($data_set);
  204. }
  205. # Send the dim command
  206. my @values = ();
  207. foreach my $data (@{$data_set}) {
  208. dim($data);
  209. my $str = $data->{'channel'} . ' => ' . $data->{'value'} . ' @ ' . $data->{'time'};
  210. if ($data->{'delay'}) {
  211. $str .= ' (Delay: ' . $data->{'delay'} . ')';
  212. }
  213. push(@values, $str);
  214. }
  215. # Save the state and value to disk
  216. if (length($file)) {
  217. my ($fh, $tmp) = File::Temp::tempfile($file . '.XXXXXXXX', 'UNLINK' => 0);
  218. print $fh 'State: ' . $state . "\n" . join("\n", @values) . "\n";
  219. close($fh);
  220. rename($tmp, $file);
  221. }
  222. }
  223. sub readState($$$$) {
  224. my ($delay, $exists, $mtime, $valid) = @_;
  225. my $cmdState = undef();
  226. # Cleanup previous say calls
  227. sayCleanup();
  228. # Wait for state updates
  229. my @ready_clients = $SELECT->can_read($delay);
  230. foreach my $fh (@ready_clients) {
  231. # Ensure we won't block on recv()
  232. $fh->blocking(0);
  233. # Grab the inbound text
  234. while (defined($fh->recv(my $text, $MAX_CMD_LEN))) {
  235. # Save the old data, in case the new stuff is bad
  236. my %existsOld = ();
  237. my %mtimeOld = ();
  238. if ($exists) {
  239. %existsOld = %{$exists};
  240. }
  241. if ($mtime) {
  242. %mtimeOld = %{$mtime};
  243. }
  244. # Parse the string
  245. my $state = parseState($text, $exists, $mtime);
  246. # Ignore invalid states
  247. if (defined($valid)) {
  248. if (!defined($valid->{$state})) {
  249. if ($DEBUG) {
  250. print STDERR 'Invalid state: ' . $state . "\n";
  251. }
  252. next;
  253. }
  254. }
  255. # Restore the old data, if the new stuff is bad
  256. if (scalar(keys(%{$exists})) < 1) {
  257. %{$exists} = ();
  258. %{$mtime} = ();
  259. foreach my $key (keys(%existsOld)) {
  260. $exists->{$key} = $existsOld{$key};
  261. }
  262. foreach my $key (keys(%mtimeOld)) {
  263. $mtime->{$key} = $mtimeOld{$key};
  264. }
  265. }
  266. # Propogate valid states
  267. if ($state) {
  268. $cmdState = $state;
  269. }
  270. }
  271. }
  272. # Return the final valid state
  273. return $cmdState;
  274. }
  275. # Reap zombie children
  276. sub sayCleanup() {
  277. foreach my $pid (keys(%SAY)) {
  278. waitpid($pid, POSIX::WNOHANG);
  279. delete($SAY{$pid});
  280. }
  281. }
  282. # Speak
  283. sub say($) {
  284. my ($str) = @_;
  285. if ($DEBUG) {
  286. print STDERR 'Say: ' . $str . "\n";
  287. }
  288. # Cleanup previous say calls
  289. sayCleanup();
  290. # Fork and speak in the background
  291. my $pid = fork();
  292. if (!defined($pid)) {
  293. die('Unable to fork: ' . $! . "\n");
  294. }
  295. if (!$pid) {
  296. exec('say', $str);
  297. } else {
  298. $SAY{$pid} = 1;
  299. }
  300. }
  301. # Always return true
  302. 1;