DMX.pm 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. use File::Temp;
  5. use IO::Select;
  6. use IO::Socket::UNIX;
  7. # Package name
  8. package DMX;
  9. # Debug
  10. my $DEBUG = 0;
  11. if ($ENV{'DEBUG'}) {
  12. $DEBUG = 1;
  13. }
  14. # Config
  15. my $SOCK_TIMEOUT = 5;
  16. my $MAX_CMD_LEN = 1024;
  17. my $TEMP_DIR = `getconf DARWIN_USER_TEMP_DIR`;
  18. chomp($TEMP_DIR);
  19. my $DATA_DIR = $TEMP_DIR . 'plexMonitor/';
  20. my $DMX_SOCK = $DATA_DIR . 'DMX.socket';
  21. my $SUB_SOCK = $DATA_DIR . 'STATE.socket';
  22. my %CHANNEL_ADJ = (
  23. '13' => 1.00,
  24. '14' => 1.14,
  25. '15' => 1.19,
  26. );
  27. # Sanity check
  28. if (!-d $DATA_DIR) {
  29. die('Data directory not available: ' . $DATA_DIR . "\n");
  30. }
  31. # State
  32. my $DMX_FH = undef();
  33. my $SELECT = undef();
  34. # Data directory
  35. sub dataDir() {
  36. return $DATA_DIR;
  37. }
  38. # Generic client socket
  39. sub clientSock($) {
  40. my ($path) = @_;
  41. my $sock = IO::Socket::UNIX->new(
  42. 'Peer' => $path,
  43. 'Type' => IO::Socket::UNIX::SOCK_DGRAM,
  44. 'Timeout' => $SOCK_TIMEOUT,
  45. ) or die('Unable to open client socket: ' . $path . ": ${@}\n");
  46. return $sock;
  47. }
  48. # DMX socket init
  49. sub dmxSock() {
  50. if (!defined($DMX_FH)) {
  51. $DMX_FH = clientSock($DMX_SOCK);
  52. }
  53. return $DMX_FH;
  54. }
  55. # Subscribe to state updates
  56. sub stateSubscribe($) {
  57. my ($STATE_SOCK) = @_;
  58. my $sub_fh = IO::Socket::UNIX->new(
  59. 'Peer' => $SUB_SOCK,
  60. 'Type' => IO::Socket::UNIX::SOCK_DGRAM,
  61. 'Timeout' => $SOCK_TIMEOUT
  62. ) or die('Unable to open state subscription socket: ' . $SUB_SOCK . ": ${@}\n");
  63. $sub_fh->send($STATE_SOCK)
  64. or die('Unable to subscribe: ' . $! . "\n");
  65. shutdown($sub_fh, 2);
  66. undef($sub_fh);
  67. }
  68. # State client socket init
  69. sub stateSocket($) {
  70. my ($STATE_SOCK) = @_;
  71. if (-e $STATE_SOCK) {
  72. unlink($STATE_SOCK);
  73. }
  74. my $state_fh = IO::Socket::UNIX->new(
  75. 'Local' => $STATE_SOCK,
  76. 'Type' => IO::Socket::UNIX::SOCK_DGRAM,
  77. ) or die('Unable to open state client socket: ' . $STATE_SOCK . ": ${@}\n");
  78. $SELECT = IO::Select->new($state_fh)
  79. or die('Unable to select state client socket: ' . $STATE_SOCK . ": ${!}\n");
  80. return $SELECT;
  81. }
  82. # Parse the state->client comm string
  83. sub parseState($$$) {
  84. my ($text, $exists, $mtime) = @_;
  85. my $cmdState = undef();
  86. my $exists_text = undef();
  87. if (!defined($mtime)) {
  88. my %tmp = ();
  89. $mtime = \%tmp;
  90. }
  91. # Parse the string
  92. %{$exists} = ();
  93. %{$mtime} = ();
  94. ($cmdState, $exists_text) = $text =~ /^(\w+)(?:\s+\(([^\)]+)\))?/;
  95. if (!defined($cmdState)) {
  96. print STDERR 'State parse error: ' . $text . "\n";
  97. next;
  98. }
  99. if (defined($exists_text)) {
  100. foreach my $exists_val (split(/\s*,\s*/, $exists_text)) {
  101. my ($name, $value, $time) = $exists_val =~ /(\w+)\:(\d+)\:(\d+)/;
  102. if (!defined($name) || !defined($value) || !defined($time)) {
  103. print STDERR 'State parse error (exists): ' . $text . "\n";
  104. next;
  105. }
  106. $exists->{$name} = $value;
  107. $mtime->{$name} = $time;
  108. }
  109. }
  110. if ($DEBUG) {
  111. my @exists_tmp = ();
  112. foreach my $key (keys(%{$exists})) {
  113. push(@exists_tmp, $key . ':' . $exists->{$key} . ':' . $mtime->{$key});
  114. }
  115. print STDERR 'Got state: ' . $cmdState . ' (' . join(', ', @exists_tmp) . ")\n";
  116. }
  117. # Translate INIT to OFF
  118. if ($cmdState eq 'INIT') {
  119. $cmdState = 'OFF';
  120. }
  121. # Return
  122. return $cmdState;
  123. }
  124. sub dim($) {
  125. my ($args) = @_;
  126. if (!defined($args->{'delay'})) {
  127. $args->{'delay'} = 0;
  128. }
  129. if (!defined($args->{'channel'}) || !defined($args->{'time'}) || !defined($args->{'value'})) {
  130. die('Invalid command for socket: ' . join(', ', keys(%{$args})) . ': ' . join(', ', values(%{$args})) . "\n");
  131. }
  132. # Silently skip invalid channels
  133. if ($args->{'channel'} < 0) {
  134. return;
  135. }
  136. # Adjust for the color curve (for channels where we have such data)
  137. my $value = $args->{'value'};
  138. if ($CHANNEL_ADJ{ $args->{'channel'} }) {
  139. $value *= $CHANNEL_ADJ{ $args->{'channel'} };
  140. }
  141. # 8-bit values
  142. $value = int($value);
  143. if ($value > 255) {
  144. $value = 255;
  145. } elsif ($value < 0) {
  146. $value = 0;
  147. }
  148. # We only deal in ints
  149. my $time = int($args->{'time'});
  150. my $delay = int($args->{'delay'});
  151. my $cmd = join(':', $args->{'channel'}, $time, $value, $delay);
  152. dmxSock();
  153. $DMX_FH->send($cmd)
  154. or die('Unable to write command to DMX socket: ' . $cmd . ": ${!}\n");
  155. }
  156. sub printDataset($) {
  157. my ($data_set) = @_;
  158. my $sum = 0;
  159. foreach my $data (@{$data_set}) {
  160. $sum += $data->{'value'};
  161. my $str = $data->{'channel'} . ' => ' . int($data->{'value'}) . ' @ ' . $data->{'time'};
  162. if ($data->{'delay'}) {
  163. $str .= ' (Delay: ' . $data->{'delay'} . ')';
  164. }
  165. print STDERR "\t" . $str . "\n";
  166. }
  167. print STDERR "\tTotal: " . int($sum) . "\n";
  168. }
  169. sub applyDataset($$$) {
  170. my ($data_set, $state, $file) = @_;
  171. if (!defined($state)) {
  172. $state = 'NULL';
  173. }
  174. # Debug
  175. if ($DEBUG) {
  176. print STDERR 'State: ' . $state . "\n";
  177. printDataset($data_set);
  178. }
  179. # Send the dim command
  180. my @values = ();
  181. foreach my $data (@{$data_set}) {
  182. dim($data);
  183. my $str = $data->{'channel'} . ' => ' . $data->{'value'} . ' @ ' . $data->{'time'};
  184. if ($data->{'delay'}) {
  185. $str .= ' (Delay: ' . $data->{'delay'} . ')';
  186. }
  187. push(@values, $str);
  188. }
  189. # Save the state and value to disk
  190. if (length($file)) {
  191. my ($fh, $tmp) = File::Temp::tempfile($file . '.XXXXXXXX', 'UNLINK' => 0);
  192. print $fh 'State: ' . $state . "\n" . join("\n", @values) . "\n";
  193. close($fh);
  194. rename($tmp, $file);
  195. }
  196. }
  197. sub readState($$$$) {
  198. my ($delay, $exists, $mtime, $valid) = @_;
  199. my $cmdState = undef();
  200. # Wait for state updates
  201. my @ready_clients = $SELECT->can_read($delay);
  202. foreach my $fh (@ready_clients) {
  203. # Ensure we won't block on recv()
  204. $fh->blocking(0);
  205. # Grab the inbound text
  206. while (defined($fh->recv(my $text, $MAX_CMD_LEN))) {
  207. # Parse the string
  208. my $state = parseState($text, $exists, $mtime);
  209. # Ignore invalid states
  210. if (defined($valid)) {
  211. if (!defined($valid->{$state})) {
  212. if ($DEBUG) {
  213. print STDERR 'Invalid state: ' . $state . "\n";
  214. }
  215. next;
  216. }
  217. }
  218. # Propogate valid states
  219. if ($state) {
  220. $cmdState = $state;
  221. }
  222. }
  223. }
  224. # Return the final valid state
  225. return $cmdState;
  226. }
  227. # Always return true
  228. 1;