#!/usr/bin/perl use FindBin; # find the script's directory use lib $FindBin::Bin; # add that directory to the library path use xPL::common; ################################################################################ # constants # $vendor_id = 'dspc'; # from xplproject.org $device_id = 'canvas'; # max 8 chars $class_id = 'canvas'; # max 8 chars $tty_name_length = 4; $separator = '-' x 80; $indent = ' ' x 2; #------------------------------------------------------------------------------- # global variables # my %configuration; $configuration{'command'} = 'sudo fbi -d /dev/fb0 -T 1 --noverbose'; $configuration{'directory'} = '/home/control/Canvas/Paintings/Veronese'; $configuration{'file_types'} = ['png', 'jpg']; $configuration{'display_duration'} = 60; ################################################################################ # Input arguments # use Getopt::Std; my %opts; getopts('hvp:n:t:w:c:d:e:o:', \%opts); die("\n". "Usage: $0 [parameters]\n". "\n". "Parameters:\n". "${indent}-h display this help message\n". "${indent}-v verbose\n". "${indent}-p port the base UDP port\n". "${indent}-n id the instance id (max. 12 chars)\n". "${indent}-t mins the heartbeat interval in minutes\n". "${indent}-w secs the startup sleep interval\n". "${indent}-c cmd command for displaying an image in the framebuffer\n". "${indent}-d dir the directory where the images are found\n". "${indent}-e ext the extensions of the images to display\n". "${indent}-u secs image display duration in seconds\n". "\n". "Displays images on a screen.\n". "\n". "More information with: perldoc $0\n". "\n". "" ) if ($opts{h}); my $verbose = $opts{v}; my $client_base_port = $opts{'p'} || 50000; my $instance_id = $opts{'n'} || xpl_build_automatic_instance_id; my $heartbeat_interval = $opts{'t'} || 5; my $startup_sleep_time = $opts{'w'} || 0; $configuration{'command'} = $opts{'c'} || $configuration{'command'}; $configuration{'directory'} = $opts{'d'} || $configuration{'directory'}; $configuration{'file_types'} = $opts{'e'} || $configuration{'file_types'}; $configuration{'display_duration'} = $opts{'u'} || $configuration{'display_duration'}; ################################################################################ # Internal functions # #------------------------------------------------------------------------------- # Build list of files to be displayed # sub build_file_list { my ($directory) = @_; # get file list opendir(my $dir, $directory) or die "Cannot open directory: $!"; my @complete_file_list = readdir($dir); closedir($dir); # filter file list my $extensions_ref = $configuration{'file_types'}; my @filtered_file_list = (); foreach my $file (@complete_file_list) { foreach my $extension (@$extensions_ref) { if ($file =~ m/\.$extension\Z/) { push(@filtered_file_list, $file) #print "$file \n"; } } } return(@filtered_file_list); } #------------------------------------------------------------------------------- # Display next picture # sub display_next_picture { my ( $xpl_socket, $xpl_port, $vendor_id, $device_id, $instance_id, $class_id, $picture_index, @file_list ) = @_; # choose new picture $picture_index = ($picture_index + 1) % scalar(@file_list); my $current_picture = $file_list[$picture_index]; if ($verbose > 0) { print "new picture : $current_picture\n"; } # display picture my $to_kill = $configuration{'command'}; $to_kill =~ s/\Asudo\s*//; $to_kill =~ s/\s.*//; system("sudo killall $to_kill 2>/dev/null"); system("$configuration{'command'} $configuration{'directory'}/$current_picture 2>/dev/null"); # send xPL message xpl_send_message( $xpl_socket, $xpl_port, 'stat', "$vendor_id-$device_id.$instance_id", '*', "$class_id.basic", ('picture' => $current_picture) ); return($picture_index); } #------------------------------------------------------------------------------- # Log and send xPL message # sub send_message_with_log { my ( $log_file_spec, $xpl_socket, $xpl_port, $type, $source, $target, $class, %body ) = @_; # get local time my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time); # log info open(LOG_FILE, ">> $log_file_spec"); printf(LOG_FILE '%02d:%02d:%02d', $hour, $min, $sec); foreach my $item (keys %body) { print(LOG_FILE ", $item = $body{$item}"); } print(LOG_FILE "\n"); close(LOG_FILE); # send xPL message xpl_send_message( $xpl_socket, $xpl_port, $type, $source, $target, $class, %body ); } #------------------------------------------------------------------------------- # Build sun times status response # sub build_times_status { my ($query, $declination, $solar_time, $sunrise, $sunset, $dawn, $dusk, $verbose) = @_; my %status = (); # format values $declination = sprintf("%.2f", $declination); $solar_time = sprintf('%.2f', $solar_time); $sunrise = hours_decimal_to_minutes($sunrise); $sunset = hours_decimal_to_minutes($sunset); $dawn = hours_decimal_to_minutes($dawn); $dusk = hours_decimal_to_minutes($dusk); if ($verbose > 0) { print($indent . "Declination: $declination\n"); print($indent . "Solar time : $solar_time\n"); print($indent . "Dawn : $dawn\n"); print($indent . "Sunrise : $sunrise\n"); print($indent . "Sunset : $sunset\n"); print($indent . "Dusk : $dusk\n"); } # build message body $status{'solarTime'} = $solar_time; if ( ($query eq 'declination') or ($query eq 'all') ) { $status{'declination'} = $declination; } if ( ($query eq 'sunrise') or ($query eq 'all') ) { $status{'sunrise'} = $sunrise; } if ( ($query eq 'sunset') or ($query eq 'all') ) { $status{'sunset'} = $sunset; } if ( ($query eq 'dawn') or ($query eq 'all') ) { $status{'dawn'} = $dawn; } if ( ($query eq 'dusk') or ($query eq 'all') ) { $status{'dusk'} = $dusk; } return(%status) } ################################################################################ # Catch control-C interrupt # $SIG{INT} = sub{ $xpl_end++ }; ################################################################################ # Main script # sleep($startup_sleep_time); # xPL parameters my $xpl_id = xpl_build_id($vendor_id, $device_id, $instance_id); my $xpl_ip = xpl_find_ip; # create xPL socket my ($client_port, $xpl_socket) = xpl_open_socket($xpl_port, $client_base_port); # display working parameters if ($verbose > 0) { system("clear"); print("$separator\n"); print("Starting canvas controller on xPL port $client_port.\n"); print("$separator\n"); } #=============================================================================== # Main loop # my $timeout = 1; my $last_heartbeat_time = 0; my $last_change = 0; my @file_list = build_file_list($configuration{'directory'}); my $file_index = 0; while ( (defined($xpl_socket)) && ($xpl_end == 0) ) { # calculate elapsed time my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = gmtime(time); my $time_in_seconds = $sec + 60*($min + 24*($hour)); # check if event if ( ($time_in_seconds-$last_change > $configuration{'display_duration'}) or ($time_in_seconds < $last_change) ) { $file_index = display_next_picture( $xpl_socket, $xpl_port, $vendor_id, $device_id, $instance_id, $class_id, $file_index, @file_list ); $last_change = $time_in_seconds; } # check time and send heartbeat $last_heartbeat_time = xpl_send_heartbeat( $xpl_socket, $xpl_id, $xpl_ip, $client_port, $heartbeat_interval, $last_heartbeat_time ); #print "time: $last_heartbeat_time\n"; # get xpl-UDP message with timeout my ($xpl_message) = xpl_get_message($xpl_socket, $timeout); # process XPL message if ($xpl_message) { my ($type, $source, $target, $schema, %body) = xpl_get_message_elements($xpl_message); if ( xpl_is_for_me($xpl_id, $target) ) { if (lc($schema) eq lc("$class_id.basic")) { #print "$xpl_message\n"; if ($type eq 'xpl-cmnd') { if (my $command = $body{'command'}) { if ($verbose > 0) { print($indent . "Received \"$command\" command from \"$source\"\n"); } if ($command eq 'next') { $file_index = display_next_picture( $xpl_socket, $xpl_port, $vendor_id, $device_id, $instance_id, $class_id, $file_index, @file_list ); } } if (my $duration = $body{'duration'}) { if ($verbose > 0) { print($indent . "New duration is $duration seconds\n"); } $configuration{'display_duration'} = $duration; } if (my $directory = $body{'directory'}) { if ($verbose > 0) { print($indent . "New directory is $directory\n"); } @list = build_file_list($directory); if (scalar(@list) > 0) { $configuration{'directory'} = $directory; @file_list = @list; $file_index = display_next_picture( $xpl_socket, $xpl_port, $vendor_id, $device_id, $instance_id, $class_id, 0, @file_list ); } else { if ($verbose > 0) { print($indent . "Directory contains no usable pictures\n"); } } } } } } } } xpl_disconnect($xpl_socket, $xpl_id, $xpl_ip, $client_port); ################################################################################ # Documentation (access it with: perldoc ) # __END__ =head1 NAME xpl-canvas.pl - Displays images on a screen. =head1 SYNOPSIS xpl-canvas.pl [options] =head1 DESCRIPTION This xPL client displays pictures on a screen. It uses the framebuffer to display the images, and as such works with a minimal console Linux installation. It sends status messages when the image changes. =head1 OPTIONS =over 8 =item B<-h> Display a help message. =item B<-v> Be verbose. =item B<-p port> Specify the base port from which the client searches for a free port. If not specified, the client will take a default value. =item B<-n id> Specify the instance id (name). The id is limited to 8 characters. If not specified, it is constructed from the host name =item B<-t mins> Specify the number of minutes between two heartbeat messages. =item B<-w secs> Specify the number of seconds before sending the first heartbeat. This allows to start the client after the hub, thus eliminating an prospective startup delay of one heartbeat interval. =item B<-c cmd> Specify the command for displaying images to the framebuffer. =item B<-d dir> Specify the directory containing the imageds to display. =item B<-e ext> Specify the extensions of the names of the files to display. Doesn't work yet. =item B<-u secs> Specify the duration in seconds of the picture display. =back =head1 TEST Start C<./xpl-canvas.pl -v> in another terminal window. The display should start showing the images. Start C in a terminal window. At each image change, an xPL message should provide the corresponding information. In a third terminal window, launch the command: C. The image should be immediately replaced by the next one. Other possible commands are: C and C. =head1 AUTHOR Francois Corthay, DSPC =head1 VERSION 1.0, 2019 =cut