DMX.pm 7.0 KB

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