state.pl 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. use IO::Select;
  5. use IO::Socket::UNIX;
  6. use File::Basename;
  7. use File::Temp qw( tempfile );
  8. # Local modules
  9. use Cwd qw(abs_path);
  10. use File::Basename qw(dirname);
  11. use lib dirname(abs_path($0));
  12. use DMX;
  13. # Prototypes
  14. sub mtime($);
  15. # User config
  16. my $STATE_TIMEOUT = 180;
  17. my $MEDIA_PATH = `~/bin/video/mediaPath`;
  18. my %MON_FILES = (
  19. 'GUI' => 'MTIME',
  20. 'MOTION' => 'MTIME',
  21. 'PROJECTOR' => 'STATUS',
  22. 'AMPLIFIER' => 'STATUS',
  23. 'PLAYING' => 'STATUS_PLAYING',
  24. 'NO_MOTION' => 'EXISTS',
  25. 'RAVE' => 'EXISTS',
  26. 'EFFECT' => 'EXISTS',
  27. 'FAN_CMD' => 'EXISTS_ON',
  28. 'LIGHTS' => 'EXISTS_ON',
  29. 'STEREO_CMD' => 'EXISTS_ON',
  30. 'AUDIO_AMP' => 'EXISTS_ON',
  31. 'AMPLIFIER_VOL' => 'STATUS_VALUE',
  32. 'AMPLIFIER_MODE' => 'STATUS_VALUE',
  33. 'AMPLIFIER_INPUT' => 'STATUS_VALUE',
  34. 'AUDIO' => 'STATUS_VALUE',
  35. 'AUDIO_STATE' => 'STATUS_VALUE',
  36. $MEDIA_PATH . '/DMX/cmd/GARAGE_CMD' => 'EXISTS_CLEAR',
  37. );
  38. # App config
  39. my $SOCK_TIMEOUT = 5;
  40. my $DATA_DIR = DMX::dataDir();
  41. my $CMD_FILE = $DATA_DIR . 'STATE.socket';
  42. my $MAX_CMD_LEN = 4096;
  43. my $RESET_CMD = $ENV{'HOME'} . '/bin/video/dmx/reset.sh';
  44. my $PUSH_TIMEOUT = 20;
  45. # Debug
  46. my $DEBUG = 0;
  47. if ($ENV{'DEBUG'}) {
  48. $DEBUG = 1;
  49. }
  50. # Command-line arguments
  51. my ($DELAY) = @ARGV;
  52. if (!$DELAY) {
  53. $DELAY = 0.5;
  54. }
  55. # Socket init
  56. my $select = DMX::stateSocket($CMD_FILE);
  57. # Reset all dependents at starts
  58. system($RESET_CMD);
  59. # Subscribers
  60. my @subscribers = ();
  61. # State
  62. my $state = 'INIT';
  63. my $stateLast = $state;
  64. my $status = '';
  65. my $statusLast = $status;
  66. my $updateLast = 0;
  67. my $pushLast = 0;
  68. # Init the file tracking structure
  69. my %files = ();
  70. foreach my $file (keys(%MON_FILES)) {
  71. my %tmp = (
  72. 'name' => basename($file),
  73. 'type' => $MON_FILES{$file},
  74. 'path' => $DATA_DIR . $file,
  75. 'update' => 0,
  76. 'status' => 0,
  77. 'last' => 0,
  78. );
  79. # Allow absolute paths to override the $DATA_DIR path
  80. if ($file =~ /^\//) {
  81. $tmp{'path'} = $file;
  82. }
  83. # Record the directory name for folder monitoring
  84. $tmp{'dir'} = dirname($tmp{'path'});
  85. # Push a hash ref
  86. $files{$file} = \%tmp;
  87. }
  88. # Loop forever
  89. while (1) {
  90. # Provide a method to force updates even when the state does not change
  91. my $update = 0;
  92. # Check for queued commands
  93. my @ready_clients = $select->can_read($DELAY);
  94. foreach my $fh (@ready_clients) {
  95. # Grab the inbound text
  96. my $path = undef();
  97. $fh->recv($path, $MAX_CMD_LEN);
  98. $path =~ s/^\s+//;
  99. $path =~ s/\s+$//;
  100. if ($DEBUG) {
  101. print STDERR 'Got path: ' . $path . "\n";
  102. }
  103. # Only accept valid paths
  104. if (!-r $path) {
  105. print STDERR 'Invalid socket path: ' . $path . "\n";
  106. next;
  107. }
  108. # Open the new socket
  109. my $sub = IO::Socket::UNIX->new(
  110. 'Peer' => $path,
  111. 'Type' => SOCK_DGRAM,
  112. 'Timeout' => $SOCK_TIMEOUT
  113. );
  114. if (!$sub) {
  115. print STDERR 'Unable to open socket: ' . $path . ": ${@}\n";
  116. next;
  117. }
  118. # Add the socket to our subscriber list
  119. my %tmp = (
  120. 'path' => $path,
  121. 'socket' => $sub,
  122. );
  123. push(@subscribers, \%tmp);
  124. # Force an update
  125. $update = 1;
  126. }
  127. # Use opendir/readdir in each folder to ensure fresh results
  128. my %folders = ();
  129. foreach my $file (values(%files)) {
  130. if (!$folders{ $file->{'dir'} }) {
  131. my $dh = undef();
  132. if (opendir($dh, $file->{'dir'})) {
  133. if ($DEBUG) {
  134. print STDERR 'Refreshed folder: ' . $file->{'dir'} . "\n";
  135. }
  136. my @files = readdir($dh);
  137. closedir($dh);
  138. }
  139. $folders{ $file->{'dir'} } = 1;
  140. }
  141. }
  142. # Monitor files of all types
  143. foreach my $file (values(%files)) {
  144. # Always record the previous status and clear the new one
  145. $file->{'last'} = $file->{'status'};
  146. $file->{'status'} = 0;
  147. # Record the last update for STATUS and MTIME files
  148. if ($file->{'type'} =~ /^STATUS/ || $file->{'type'} =~ /^MTIME/) {
  149. $file->{'update'} = mtime($file->{'path'});
  150. if ($DEBUG) {
  151. print STDERR 'Last change: ' . $file->{'name'} . ': ' . localtime($file->{'update'}) . "\n";
  152. }
  153. }
  154. # Grab the state from STATUS files
  155. if ($file->{'type'} =~ /^STATUS/) {
  156. my $text = '';
  157. {
  158. if (!open(my $fh, $file->{'path'})) {
  159. warn('Unable to open ' . $file->{'path'} . "\n");
  160. } else {
  161. local $/;
  162. $text = <$fh>;
  163. close($fh);
  164. }
  165. }
  166. if ($file->{'type'} eq 'STATUS_PLAYING') {
  167. if ($text =~ /PlayStatus\:Playing/) {
  168. $file->{'status'} = 1;
  169. }
  170. } elsif ($file->{'type'} eq 'STATUS_VALUE') {
  171. $text =~ s/\n$//;
  172. $file->{'status'} = $text;
  173. } else {
  174. if ($text =~ /1/) {
  175. $file->{'status'} = 1;
  176. }
  177. }
  178. if ($DEBUG) {
  179. print STDERR 'Status: ' . $file->{'name'} . ': ' . $file->{'status'} . "\n";
  180. }
  181. }
  182. # Check for the presence of EXISTS files and record their last change
  183. if ($file->{'type'} =~ /^EXISTS/) {
  184. my $mtime = mtime($file->{'path'});
  185. if ($mtime > 0) {
  186. $file->{'status'} = 1;
  187. $file->{'update'} = $mtime;
  188. } elsif ($file->{'last'}) {
  189. $file->{'update'} = time();
  190. }
  191. if ($DEBUG) {
  192. print STDERR 'Exists: ' . $file->{'name'} . ': ' . $file->{'status'} . ':' . $file->{'update'} . "\n";
  193. }
  194. }
  195. }
  196. # Set the global update timestamp
  197. foreach my $file (values(%files)) {
  198. if ($file->{'update'} > $updateLast) {
  199. $updateLast = $file->{'update'};
  200. }
  201. }
  202. # Calculate the new state
  203. $stateLast = $state;
  204. if ($files{'PROJECTOR'}->{'status'}) {
  205. # We are always either playing or paused if the projector is on
  206. if ($files{'PLAYING'}->{'status'}) {
  207. $state = 'PLAY';
  208. } else {
  209. $state = 'PAUSE';
  210. }
  211. } else {
  212. # If the projector is off, check the timeouts
  213. my $timeSinceUpdate = time() - $updateLast;
  214. if ($timeSinceUpdate > $STATE_TIMEOUT) {
  215. $state = 'OFF';
  216. } elsif ($timeSinceUpdate < $STATE_TIMEOUT) {
  217. if ($files{'NO_MOTION'}->{'status'}) {
  218. $state = 'OFF';
  219. } else {
  220. $state = 'MOTION';
  221. }
  222. }
  223. }
  224. if ($state eq 'INIT') {
  225. $state = 'OFF';
  226. }
  227. # Clear EXISTS_ON files when the main state is "OFF" (and before we append their status)
  228. foreach my $file (values(%files)) {
  229. if ($file->{'type'} eq 'EXISTS_ON' && $file->{'status'} && $state eq 'OFF') {
  230. unlink($file->{'path'});
  231. $file->{'stauts'} = 0;
  232. if ($DEBUG) {
  233. print STDERR 'Clearing exists flag for: ' . $file->{'name'} . "\n";
  234. }
  235. }
  236. }
  237. # Calculate the new status
  238. $statusLast = $status;
  239. {
  240. my @statTime = ();
  241. foreach my $file (values(%files)) {
  242. push(@statTime, $file->{'name'} . ':' . $file->{'status'} . ':' . $file->{'update'});
  243. }
  244. $status = ' (' . join(', ', @statTime) . ')';
  245. }
  246. # Clear EXISTS_CLEAR files immediately (but after we append their status)
  247. foreach my $file (values(%files)) {
  248. if ($file->{'type'} eq 'EXISTS_CLEAR' && $file->{'status'}) {
  249. unlink($file->{'path'});
  250. $file->{'stauts'} = 0;
  251. if ($DEBUG) {
  252. print STDERR 'Clearing exists flag for: ' . $file->{'name'} . "\n";
  253. }
  254. }
  255. }
  256. # Force updates on a periodic basis
  257. if (time() - $pushLast > $PUSH_TIMEOUT) {
  258. $update = 1;
  259. }
  260. # Update on any status change
  261. foreach my $file (values(%files)) {
  262. if ($file->{'status'} ne $file->{'last'}) {
  263. $update = 1;
  264. last;
  265. }
  266. }
  267. # Force updates on any state change
  268. if ($stateLast ne $state || $statusLast ne $status) {
  269. if ($DEBUG) {
  270. print STDERR 'State change: ' . $stateLast . $statusLast . ' => ' . $state . $status . "\n";
  271. }
  272. $update = 1;
  273. }
  274. # Push the update
  275. if ($update) {
  276. # Note the push
  277. $pushLast = time();
  278. # Send notifications to all subscribers
  279. foreach my $sub (@subscribers) {
  280. # Drop subscribers that are not available
  281. if (!eval { $sub->{'socket'}->send($state . $status) }) {
  282. print STDERR 'Dropping bad socket from subscriber list: ' . $sub->{'path'} . "\n";
  283. my @new_subscribers = ();
  284. foreach my $new (@subscribers) {
  285. if ($new->{'socket'} eq $sub->{'socket'}) {
  286. next;
  287. }
  288. push(@new_subscribers, $new);
  289. }
  290. @subscribers = @new_subscribers;
  291. }
  292. }
  293. # Save the state and value to disk
  294. my ($fh, $tmp) = tempfile($DATA_DIR . 'STATE.XXXXXXXX', 'UNLINK' => 0);
  295. print $fh $state . "\n";
  296. close($fh);
  297. rename($tmp, $DATA_DIR . 'STATE');
  298. }
  299. }
  300. sub mtime($) {
  301. my ($file) = @_;
  302. my $mtime = 0;
  303. if (-r $file) {
  304. (undef(), undef(), undef(), undef(), undef(), undef(), undef(), undef(), undef(), $mtime, undef(), undef(), undef()) = stat($file);
  305. }
  306. return $mtime;
  307. }