DMX.pm 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  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. # State
  28. my $DMX_FH = undef();
  29. my $SELECT = undef();
  30. # DMX socket init
  31. sub dmxSock() {
  32. if (!defined($DMX_FH)) {
  33. $DMX_FH = IO::Socket::UNIX->new(
  34. 'Peer' => $DMX_SOCK,
  35. 'Type' => IO::Socket::UNIX::SOCK_DGRAM,
  36. 'Timeout' => $SOCK_TIMEOUT
  37. ) or die('Unable to open DMX socket: ' . $DMX_SOCK . ": ${@}\n");
  38. }
  39. return $DMX_FH;
  40. }
  41. # Subscribe to state updates
  42. sub stateSubscribe($) {
  43. my ($STATE_SOCK) = @_;
  44. my $sub_fh = IO::Socket::UNIX->new(
  45. 'Peer' => $SUB_SOCK,
  46. 'Type' => IO::Socket::UNIX::SOCK_DGRAM,
  47. 'Timeout' => $SOCK_TIMEOUT
  48. ) or die('Unable to open state subscription socket: ' . $SUB_SOCK . ": ${@}\n");
  49. $sub_fh->send($STATE_SOCK)
  50. or die('Unable to subscribe: ' . $! . "\n");
  51. shutdown($sub_fh, 2);
  52. undef($sub_fh);
  53. }
  54. # State client socket init
  55. sub stateSocket($) {
  56. my ($STATE_SOCK) = @_;
  57. if (-e $STATE_SOCK) {
  58. unlink($STATE_SOCK);
  59. }
  60. my $state_fh = IO::Socket::UNIX->new(
  61. 'Local' => $STATE_SOCK,
  62. 'Type' => IO::Socket::UNIX::SOCK_DGRAM
  63. ) or die('Unable to open state client socket: ' . $STATE_SOCK . ": ${@}\n");
  64. $SELECT = IO::Select->new($state_fh)
  65. or die('Unable to select state client socket: ' . $STATE_SOCK . ": ${!}\n");
  66. return $SELECT;
  67. }
  68. # Parse the state->client comm string
  69. sub parseState($$) {
  70. my ($fh, $exists) = @_;
  71. # Grab the inbound text
  72. my $text = undef();
  73. $fh->recv($text, $MAX_CMD_LEN);
  74. # Parse the string
  75. my %tmp = ();
  76. %{$exists} = ();
  77. my ($cmdState, $exists_text) = $text =~ /^(\w+)(?:\s+\(([^\)]+)\))?/;
  78. if (!defined($cmdState)) {
  79. print STDERR 'State parse error: ' . $text . "\n";
  80. next;
  81. }
  82. if (defined($exists_text)) {
  83. foreach my $exists_val (split(/\s*,\s*/, $exists_text)) {
  84. my ($name, $value) = $exists_val =~ /(\w+)\:(0|1)/;
  85. if (!defined($name) || !defined($value)) {
  86. print STDERR 'State parse error (exists): ' . $text . "\n";
  87. next;
  88. }
  89. $exists->{$name} = $value;
  90. }
  91. }
  92. if ($DEBUG) {
  93. my @exists_tmp = ();
  94. foreach my $key (keys(%{ $exists })) {
  95. push(@exists_tmp, $key . ':' . $exists->{$key});
  96. }
  97. print STDERR 'Got state: ' . $cmdState . ' (' . join(', ', @exists_tmp) . ")\n";
  98. }
  99. # Translate INIT to OFF
  100. if ($cmdState eq 'INIT') {
  101. $cmdState = 'OFF';
  102. }
  103. # Return the state
  104. return $cmdState;
  105. }
  106. sub dim($) {
  107. my ($args) = @_;
  108. if (!defined($args->{'delay'})) {
  109. $args->{'delay'} = 0;
  110. }
  111. if (!defined($args->{'channel'}) || !defined($args->{'time'}) || !defined($args->{'value'})) {
  112. die('Invalid command for socket: ' . join(', ', keys(%{$args})) . ': ' . join(', ', values(%{$args})) . "\n");
  113. }
  114. # Silently skip invalid channels
  115. if ($args->{'channel'} < 0) {
  116. return;
  117. }
  118. # Adjust for the color curve (for channels where we have such data)
  119. my $value = $args->{'value'};
  120. if ($CHANNEL_ADJ{$args->{'channel'}}) {
  121. $value *= $CHANNEL_ADJ{$args->{'channel'}};
  122. }
  123. # 8-bit values
  124. $value = int($value);
  125. if ($value > 255) {
  126. $value = 255;
  127. } elsif ($value < 0) {
  128. $value = 0;
  129. }
  130. # We only deal in ints
  131. my $time = int($args->{'time'});
  132. my $delay = int($args->{'delay'});
  133. my $cmd = join(':', $args->{'channel'}, $time, $value, $delay);
  134. dmxSock();
  135. $DMX_FH->send($cmd)
  136. or die('Unable to write command to DMX socket: ' . $cmd . ": ${!}\n");
  137. }
  138. sub printDataset($) {
  139. my ($data_set) = @_;
  140. my $sum = 0;
  141. foreach my $data (@{ $data_set }) {
  142. $sum += $data->{'value'};
  143. my $str = $data->{'channel'} . ' => ' . int($data->{'value'}) . ' @ ' . $data->{'time'};
  144. if ($data->{'delay'}) {
  145. $str .= ' (Delay: ' . $data->{'delay'} . ')';
  146. }
  147. print STDERR "\t" . $str . "\n";
  148. }
  149. print STDERR "\tTotal: " . int($sum) . "\n";
  150. }
  151. sub applyDataset($$$) {
  152. my ($data_set, $state, $file) = @_;
  153. if (!defined($state)) {
  154. $state = 'NULL';
  155. }
  156. # Debug
  157. if ($DEBUG) {
  158. print STDERR 'State: ' . $state . "\n";
  159. printDataset($data_set);
  160. }
  161. # Send the dim command
  162. my @values = ();
  163. foreach my $data (@{ $data_set }) {
  164. dim($data);
  165. my $str = $data->{'channel'} . ' => ' . $data->{'value'} . ' @ ' . $data->{'time'};
  166. if ($data->{'delay'}) {
  167. $str .= ' (Delay: ' . $data->{'delay'} . ')';
  168. }
  169. push(@values, $str);
  170. }
  171. # Save the state and value to disk
  172. if (length($file)) {
  173. my ($fh, $tmp) = File::Temp::tempfile($file . '.XXXXXXXX', 'UNLINK' => 0);
  174. print $fh 'State: ' . $state . "\n" . join("\n", @values) . "\n";
  175. close($fh);
  176. rename($tmp, $file);
  177. }
  178. }
  179. sub readState($$$) {
  180. my ($delay, $exists, $valid) = @_;
  181. my $cmdState = undef();
  182. # Wait for state updates
  183. my @ready_clients = $SELECT->can_read($delay);
  184. foreach my $fh (@ready_clients) {
  185. # Read the global state
  186. $cmdState = parseState($fh, $exists);
  187. # Only accept valid states
  188. if (defined($valid)) {
  189. if (!defined($valid->{$cmdState})) {
  190. print STDERR 'Invalid state: ' . $cmdState . "\n";
  191. next;
  192. }
  193. }
  194. }
  195. return $cmdState;
  196. }
  197. # Always return true
  198. 1;