DMX.pm 7.0 KB

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