DMX.pm 7.0 KB

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