#!/usr/bin/perl use strict; use warnings; use IO::Select; use IO::Socket::UNIX; use File::Basename; use File::Temp qw( tempfile ); # Local modules use Cwd qw(abs_path); use File::Basename qw(dirname); use lib dirname(abs_path($0)); use DMX; # Prototypes sub mtime($); # User config my $STATE_TIMEOUT = 180; my $MEDIA_PATH = `~/bin/video/mediaPath`; my %MON_FILES = ( 'MOTION' => 'MTIME', 'PROJECTOR' => 'STATUS', 'AMPLIFIER' => 'STATUS', 'GUI' => 'STATUS_GUI', 'PLAYING' => 'STATUS_PLAYING', 'NO_MOTION' => 'EXISTS', 'RAVE' => 'EXISTS', 'EFFECT' => 'EXISTS', 'FAN_CMD' => 'EXISTS_ON', 'LIGHTS' => 'EXISTS_ON', 'STEREO_CMD' => 'EXISTS_ON', 'AUDIO_AMP' => 'EXISTS_ON', 'AMPLIFIER_VOL' => 'STATUS_VALUE', 'AMPLIFIER_MODE' => 'STATUS_VALUE', 'AMPLIFIER_INPUT' => 'STATUS_VALUE', 'AUDIO' => 'STATUS_VALUE', 'AUDIO_STATE' => 'STATUS_VALUE', 'PROJECTOR_COLOR' => 'STATUS_VALUE', 'PROJECTOR_INPUT' => 'STATUS_VALUE', 'COLOR' => 'STATUS_VALUE', $MEDIA_PATH . '/DMX/cmd/GARAGE_CMD' => 'EXISTS_CLEAR', ); my %EXTRAS = ( 'PLAYING' => { 'URL' => qr/^\Filename\:(.+)$/m, 'YEAR' => qr/^\Year\:(\d+)/m, 'SERIES' => qr/^\Show Title\:(.+)$/m, 'SEASON' => qr/^\Season\:(\d+)/m, 'EPISODE' => qr/^\Episode\:(\d+)/m, 'TITLE' => qr/^\Title\:(.+)$/m, 'LENGTH' => qr/^\Duration\:([\d\:]+)/m, 'POSITION' => qr/^\Time\:([\d\:]+)/m, 'SIZE' => qr/^\File size\:(\d+)/m, } ); # App config my $SOCK_TIMEOUT = 5; my $DATA_DIR = DMX::dataDir(); my $CMD_FILE = $DATA_DIR . 'STATE.socket'; my $MAX_CMD_LEN = 4096; my $RESET_CMD = $ENV{'HOME'} . '/bin/video/dmx/reset.sh'; my $PUSH_TIMEOUT = 20; # Debug my $DEBUG = 0; if ($ENV{'DEBUG'}) { $DEBUG = 1; } # Command-line arguments my ($DELAY) = @ARGV; if (!$DELAY) { $DELAY = 0.5; } # Socket init my $select = DMX::stateSocket($CMD_FILE); # Reset all dependents at starts system($RESET_CMD); # Subscribers my @subscribers = (); # State my $state = 'INIT'; my $stateLast = $state; my $status = ''; my $statusLast = $status; my $updateLast = 0; my $pushLast = 0; # Add the extras to the main file list foreach my $file (keys(%EXTRAS)) { foreach my $extra (keys(%{ $EXTRAS{$file} })) { $MON_FILES{ $file . '_' . $extra } = 'NONE'; } } # Init the file tracking structure my %files = (); foreach my $file (keys(%MON_FILES)) { my %tmp = ( 'name' => basename($file), 'type' => $MON_FILES{$file}, 'path' => $DATA_DIR . $file, 'update' => 0, 'status' => 0, 'last' => 0, ); # Allow absolute paths to override the $DATA_DIR path if ($file =~ /^\//) { $tmp{'path'} = $file; } # Record the directory name for folder monitoring $tmp{'dir'} = dirname($tmp{'path'}); # Push a hash ref $files{$file} = \%tmp; } # Loop forever while (1) { # Provide a method to force updates even when the state does not change my $update = 0; # Check for queued commands my @ready_clients = $select->can_read($DELAY); foreach my $fh (@ready_clients) { # Grab the inbound text my $path = undef(); $fh->recv($path, $MAX_CMD_LEN); $path =~ s/^\s+//; $path =~ s/\s+$//; if ($DEBUG) { print STDERR 'Got path: ' . $path . "\n"; } # Only accept valid paths if (!-r $path) { print STDERR 'Invalid socket path: ' . $path . "\n"; next; } # Open the new socket my $sub = IO::Socket::UNIX->new( 'Peer' => $path, 'Type' => SOCK_DGRAM, 'Timeout' => $SOCK_TIMEOUT ); if (!$sub) { print STDERR 'Unable to open socket: ' . $path . ": ${@}\n"; next; } # Add the socket to our subscriber list my %tmp = ( 'path' => $path, 'socket' => $sub, ); push(@subscribers, \%tmp); # Force an update $update = 1; } # Use opendir/readdir in each folder to ensure fresh results my %folders = (); foreach my $file (values(%files)) { if (!$folders{ $file->{'dir'} }) { my $dh = undef(); if (opendir($dh, $file->{'dir'})) { if ($DEBUG) { print STDERR 'Refreshed folder: ' . $file->{'dir'} . "\n"; } my @files = readdir($dh); closedir($dh); } $folders{ $file->{'dir'} } = 1; } } # Monitor files of all types foreach my $file (values(%files)) { # Skip "NONE" files -- they are data stores handled in other actions if ($file->{'type'} eq 'NONE') { next; } # Always record the previous status and clear the new one $file->{'last'} = $file->{'status'}; $file->{'status'} = 0; # Record the last update for STATUS and MTIME files if ($file->{'type'} =~ /^STATUS/ || $file->{'type'} =~ /^MTIME/) { $file->{'update'} = mtime($file->{'path'}); if ($DEBUG) { print STDERR 'Last change: ' . $file->{'name'} . ': ' . localtime($file->{'update'}) . "\n"; } } # Grab the state from STATUS files if ($file->{'type'} =~ /^STATUS/) { my $text = ''; { if (!open(my $fh, $file->{'path'})) { warn('Unable to open ' . $file->{'path'} . "\n"); } else { local $/; $text = <$fh>; close($fh); } } # Record the main status for the file if ($file->{'type'} eq 'STATUS_PLAYING') { if ($text =~ /PlayStatus\:Playing/) { $file->{'status'} = 1; } } elsif ($file->{'type'} eq 'STATUS_GUI') { if (!($text =~ /ActiveWindowName\:Fullscreen video/)) { $file->{'status'} = 1; } } elsif ($file->{'type'} eq 'STATUS_VALUE') { $text =~ s/\n$//; $file->{'status'} = $text; } else { if ($text =~ /1/) { $file->{'status'} = 1; } } if ($DEBUG) { print STDERR 'Status: ' . $file->{'name'} . ': ' . $file->{'status'} . "\n"; } # Handle extras, if any if ($EXTRAS{ $file->{'name'} }) { foreach my $extra (keys(%{ $EXTRAS{ $file->{'name'} } })) { my $name = $file->{'name'} . '_' . $extra; $files{$name}{'last'} = $files{$name}{'status'}; $files{$name}{'status'} = ''; $files{$name}{'update'} = $file->{'update'}; if ($text =~ $EXTRAS{ $file->{'name'} }{$extra}) { $files{$name}{'status'} = $1; } if ($DEBUG) { print STDERR 'Status (extra): ' . $name . ': ' . $files{$name}{'status'} . "\n"; } } } } # Check for the presence of EXISTS files and record their last change if ($file->{'type'} =~ /^EXISTS/) { my $mtime = mtime($file->{'path'}); if ($mtime > 0) { $file->{'status'} = 1; $file->{'update'} = $mtime; } elsif ($file->{'last'}) { $file->{'update'} = time(); } if ($DEBUG) { print STDERR 'Exists: ' . $file->{'name'} . ': ' . $file->{'status'} . ':' . $file->{'update'} . "\n"; } } } # Set the global update timestamp foreach my $file (values(%files)) { if ($file->{'update'} > $updateLast) { $updateLast = $file->{'update'}; } } # Calculate the new state $stateLast = $state; if ($files{'PROJECTOR'}->{'status'}) { # We are always either playing or paused if the projector is on if ($files{'PLAYING'}->{'status'}) { $state = 'PLAY'; } else { $state = 'PAUSE'; } } else { # If the projector is off, check the timeouts my $timeSinceUpdate = time() - $updateLast; if ($timeSinceUpdate > $STATE_TIMEOUT) { $state = 'OFF'; } elsif ($timeSinceUpdate < $STATE_TIMEOUT) { if ($files{'NO_MOTION'}->{'status'}) { $state = 'OFF'; } else { $state = 'MOTION'; } } } if ($state eq 'INIT') { $state = 'OFF'; } # Clear EXISTS_ON files when the main state is "OFF" (and before we append their status) foreach my $file (values(%files)) { if ($file->{'type'} eq 'EXISTS_ON' && $file->{'status'} && $state eq 'OFF') { unlink($file->{'path'}); $file->{'stauts'} = 0; if ($DEBUG) { print STDERR 'Clearing exists flag for: ' . $file->{'name'} . "\n"; } } } # Calculate the new status $statusLast = $status; { my @statTime = (); foreach my $file (values(%files)) { my $text = $file->{'status'}; $text =~ s/\s/ /g; $text =~ s/\,/_/g; $text =~ s/\:/-/g; push(@statTime, $file->{'name'} . ':' . $text . ':' . $file->{'update'}); } $status = ' (' . join(', ', @statTime) . ')'; } # Clear EXISTS_CLEAR files immediately (but after we append their status) foreach my $file (values(%files)) { if ($file->{'type'} eq 'EXISTS_CLEAR' && $file->{'status'}) { unlink($file->{'path'}); $file->{'stauts'} = 0; if ($DEBUG) { print STDERR 'Clearing exists flag for: ' . $file->{'name'} . "\n"; } } } # Force updates on a periodic basis if (time() - $pushLast > $PUSH_TIMEOUT) { $update = 1; } # Update on any status change foreach my $file (values(%files)) { if ($file->{'status'} ne $file->{'last'}) { $update = 1; last; } } # Force updates on any state change if ($stateLast ne $state || $statusLast ne $status) { if ($DEBUG) { print STDERR 'State change: ' . $stateLast . $statusLast . ' => ' . $state . $status . "\n"; } $update = 1; } # Push the update if ($update) { # Note the push $pushLast = time(); # Send notifications to all subscribers foreach my $sub (@subscribers) { # Drop subscribers that are not available if (!eval { $sub->{'socket'}->send($state . $status) }) { print STDERR 'Dropping bad socket from subscriber list: ' . $sub->{'path'} . "\n"; my @new_subscribers = (); foreach my $new (@subscribers) { if ($new->{'socket'} eq $sub->{'socket'}) { next; } push(@new_subscribers, $new); } @subscribers = @new_subscribers; } } # Save the state and value to disk my ($fh, $tmp) = tempfile($DATA_DIR . 'STATE.XXXXXXXX', 'UNLINK' => 0); print $fh $state . "\n"; close($fh); rename($tmp, $DATA_DIR . 'STATE'); } } sub mtime($) { my ($file) = @_; my $mtime = 0; if (-r $file) { (undef(), undef(), undef(), undef(), undef(), undef(), undef(), undef(), undef(), $mtime, undef(), undef(), undef()) = stat($file); } return $mtime; }