state.pl 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. use IO::Select;
  5. use IO::Socket::UNIX;
  6. use File::Temp qw( tempfile );
  7. # Local modules
  8. use Cwd qw(abs_path);
  9. use File::Basename qw(dirname);
  10. use lib dirname(abs_path($0));
  11. use DMX;
  12. # Prototypes
  13. sub mtime($);
  14. # User config
  15. my $STATE_TIMEOUT = 180;
  16. my %MON_FILES = (
  17. 'PLAYING' => 'MTIME',
  18. 'GUI' => 'MTIME',
  19. 'MOTION' => 'MTIME',
  20. 'PROJECTOR' => 'STATUS',
  21. 'PLAY_STATUS' => 'STATUS',
  22. 'LIGHTS' => 'EXISTS',
  23. 'FAN_CMD' => 'EXISTS',
  24. 'RAVE' => 'EXISTS_OFF',
  25. );
  26. # App config
  27. my $SOCK_TIMEOUT = 5;
  28. my $DATA_DIR = DMX::dataDir();
  29. my $CMD_FILE = $DATA_DIR . 'STATE.socket';
  30. my $MAX_CMD_LEN = 4096;
  31. my $RESET_CMD = $ENV{'HOME'} . '/bin/video/dmx/reset.sh';
  32. my $PUSH_TIMEOUT = 20;
  33. # Debug
  34. my $DEBUG = 0;
  35. if ($ENV{'DEBUG'}) {
  36. $DEBUG = 1;
  37. }
  38. # Command-line arguments
  39. my ($DELAY) = @ARGV;
  40. if (!$DELAY) {
  41. $DELAY = 0.5;
  42. }
  43. # Socket init
  44. my $select = DMX::stateSocket($CMD_FILE);
  45. # Reset all dependents at starts
  46. system($RESET_CMD);
  47. # Subscribers
  48. my @subscribers = ();
  49. # State
  50. my $state = 'INIT';
  51. my $stateLast = $state;
  52. my $playing = 0;
  53. my $updateLast = 0;
  54. my $pushLast = 0;
  55. # Init the file tracking structure
  56. my %files = ();
  57. foreach my $file (keys(%MON_FILES)) {
  58. my %tmp = (
  59. 'name' => $file,
  60. 'type' => $MON_FILES{$file},
  61. 'path' => $DATA_DIR . $file,
  62. 'update' => 0,
  63. 'status' => 0,
  64. 'last' => 0,
  65. );
  66. $files{$file} = \%tmp;
  67. }
  68. # Loop forever
  69. while (1) {
  70. # Provide a method to force updates even when the state does not change
  71. my $update = 0;
  72. # Check for queued commands
  73. my @ready_clients = $select->can_read($DELAY);
  74. foreach my $fh (@ready_clients) {
  75. # Grab the inbound text
  76. my $path = undef();
  77. $fh->recv($path, $MAX_CMD_LEN);
  78. $path =~ s/^\s+//;
  79. $path =~ s/\s+$//;
  80. if ($DEBUG) {
  81. print STDERR 'Got path: ' . $path . "\n";
  82. }
  83. # Only accept valid paths
  84. if (!-r $path) {
  85. print STDERR 'Invalid socket path: ' . $path . "\n";
  86. next;
  87. }
  88. # Open the new socket
  89. my $sub = IO::Socket::UNIX->new(
  90. 'Peer' => $path,
  91. 'Type' => SOCK_DGRAM,
  92. 'Timeout' => $SOCK_TIMEOUT
  93. );
  94. if (!$sub) {
  95. print STDERR 'Unable to open socket: ' . $path . ": ${@}\n";
  96. next;
  97. }
  98. # Add the socket to our subscriber list
  99. my %tmp = (
  100. 'path' => $path,
  101. 'socket' => $sub,
  102. );
  103. push(@subscribers, \%tmp);
  104. # Force an update
  105. $update = 1;
  106. }
  107. # Monitor files of all types
  108. foreach my $file (values(%files)) {
  109. # Always record the previous status and clear the new one
  110. $file->{'last'} = $file->{'status'};
  111. $file->{'status'} = 0;
  112. # Record the last update
  113. if ($file->{'type'} eq 'STATUS' || $file->{'type'} eq 'MTIME') {
  114. $file->{'update'} = mtime($file->{'path'});
  115. if ($DEBUG) {
  116. print STDERR 'Last change: ' . $file->{'name'} . ': ' . localtime($file->{'update'}) . "\n";
  117. }
  118. }
  119. # Grab the new status and save the old one
  120. if ($file->{'type'} eq 'STATUS') {
  121. my $fh;
  122. open($fh, $file->{'path'})
  123. or die('Unable to open ' . $file->{'path'} . "\n");
  124. my $text = <$fh>;
  125. close($fh);
  126. if ($text =~ /1/) {
  127. $file->{'status'} = 1;
  128. }
  129. if ($DEBUG) {
  130. print STDERR 'Status: ' . $file->{'name'} . ': ' . $file->{'status'} . "\n";
  131. }
  132. }
  133. # Check for the presence of a file
  134. if ($file->{'type'} =~ /^EXISTS/) {
  135. if (-e $file->{'path'}) {
  136. $file->{'status'} = 1;
  137. }
  138. if ($DEBUG) {
  139. print STDERR 'Exists: ' . $file->{'name'} . ': ' . $file->{'status'} . "\n";
  140. }
  141. }
  142. }
  143. # Set the global update timestamp
  144. foreach my $file (values(%files)) {
  145. if ($file->{'update'} > $updateLast) {
  146. $updateLast = $file->{'update'};
  147. }
  148. }
  149. # Calculate the new state
  150. $stateLast = $state;
  151. if ($files{'PROJECTOR'}->{'status'}) {
  152. # We are always either playing or paused if the projector is on
  153. if ($files{'PLAY_STATUS'}->{'status'}) {
  154. $state = 'PLAY';
  155. } else {
  156. $state = 'PAUSE';
  157. }
  158. } else {
  159. # If the projector is off, check the timeouts
  160. my $timeSinceUpdate = time() - $updateLast;
  161. if ($timeSinceUpdate > $STATE_TIMEOUT) {
  162. $state = 'OFF';
  163. } elsif ($timeSinceUpdate < $STATE_TIMEOUT) {
  164. $state = 'MOTION';
  165. }
  166. }
  167. if ($state eq 'INIT') {
  168. $state = 'OFF';
  169. }
  170. # Clear exists files when the main state is "OFF"
  171. foreach my $file (values(%files)) {
  172. if ($file->{'type'} eq 'EXISTS' && $file->{'status'} && $state eq 'OFF') {
  173. unlink($file->{'path'});
  174. $file->{'stauts'} = 0;
  175. if ($DEBUG) {
  176. print STDERR 'Clearing exists flag for: ' . $file->{'name'} . "\n";
  177. }
  178. }
  179. }
  180. # Append the status of all "exists" files
  181. {
  182. my @exists = ();
  183. foreach my $file (values(%files)) {
  184. if ($file->{'type'} =~ /^EXISTS/) {
  185. push(@exists, $file->{'name'} . ':' . $file->{'status'});
  186. }
  187. }
  188. $state .= ' (' . join(', ', @exists) . ')';
  189. }
  190. # Force updates on a periodic basis
  191. if (time() - $pushLast > $PUSH_TIMEOUT) {
  192. $update = 1;
  193. }
  194. # Update on any status change
  195. foreach my $file (values(%files)) {
  196. if ($file->{'status'} != $file->{'last'}) {
  197. $update = 1;
  198. last;
  199. }
  200. }
  201. # Force updates on any state change
  202. if ($stateLast ne $state) {
  203. if ($DEBUG) {
  204. print STDERR 'State change: ' . $stateLast . ' => ' . $state . "\n";
  205. }
  206. $update = 1;
  207. }
  208. # Push the update
  209. if ($update) {
  210. # Note the push
  211. $pushLast = time();
  212. # Send notifications to all subscribers
  213. foreach my $sub (@subscribers) {
  214. # Drop subscribers that are not available
  215. if (!eval { $sub->{'socket'}->send($state) }) {
  216. print STDERR 'Dropping bad socket from subscriber list: ' . $sub->{'path'} . "\n";
  217. my @new_subscribers = ();
  218. foreach my $new (@subscribers) {
  219. if ($new->{'socket'} eq $sub->{'socket'}) {
  220. next;
  221. }
  222. push(@new_subscribers, $new);
  223. }
  224. @subscribers = @new_subscribers;
  225. }
  226. }
  227. # Save the state and value to disk
  228. my ($fh, $tmp) = tempfile($DATA_DIR . 'STATE.XXXXXXXX', 'UNLINK' => 0);
  229. print $fh $state . "\n";
  230. close($fh);
  231. rename($tmp, $DATA_DIR . 'STATE');
  232. }
  233. }
  234. sub mtime($) {
  235. my ($file) = @_;
  236. my $mtime = 0;
  237. if (-r $file) {
  238. (undef(), undef(), undef(), undef(), undef(), undef(), undef(), undef(), undef(), $mtime, undef(), undef(), undef()) = stat($file);
  239. }
  240. return $mtime;
  241. }