Просмотр исходного кода

Unify the projector and amplifier serial interface scripts

Zachary Kotlarek 14 лет назад
Родитель
Сommit
a196ab32f1

+ 1 - 0
amplifier/amplifier.pl

@@ -0,0 +1 @@
+serial.pl

+ 26 - 0
amplifier/launchd/com.uberzach.amplifier.plist

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>UserName</key>
+	<string>tv</string>
+	<key>LimitLoadToHosts</key>
+	<array>
+		<string>heady.uberzach.com</string>
+	</array>
+	<key>RunAtLoad</key>
+	<true/>
+	<key>Label</key>
+	<string>com.uberzach.amplifier</string>
+	<key>KeepAlive</key>
+	<dict>
+		<key>NetworkState</key>
+		<true/>
+	</dict>
+	<key>ProgramArguments</key>
+	<array>
+		<string>/Users/tv/bin/video/amplifier/amplifier.pl</string>
+		<string>15</string>
+	</array>
+</dict>
+</plist>

+ 25 - 0
amplifier/launchd/com.uberzach.amplifier.power.plist

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>UserName</key>
+	<string>tv</string>
+	<key>LimitLoadToHosts</key>
+	<array>
+		<string>heady.uberzach.com</string>
+	</array>
+	<key>RunAtLoad</key>
+	<true/>
+	<key>Label</key>
+	<string>com.uberzach.amplifier.off</string>
+	<key>KeepAlive</key>
+	<dict>
+		<key>NetworkState</key>
+		<true/>
+	</dict>
+	<key>ProgramArguments</key>
+	<array>
+		<string>/Users/tv/bin/video/amplifier/power.pl</string>
+	</array>
+</dict>
+</plist>

+ 136 - 0
amplifier/power.pl

@@ -0,0 +1,136 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use POSIX qw(ceil floor);
+use File::Temp qw( tempfile );
+
+# Prototypes
+sub mtime($);
+
+# Config
+my $TIMEOUT   = 900;
+my $COUNTDOWN = 300;
+
+# App config
+my $DELAY    = 60;
+my $TEMP_DIR = `getconf DARWIN_USER_TEMP_DIR`;
+chomp($TEMP_DIR);
+my $DATA_DIR = $TEMP_DIR . '/plexMonitor';
+
+# Debug
+my $DEBUG = 0;
+if ($ENV{'DEBUG'}) {
+	$DEBUG = 1;
+}
+
+# Sanity check
+if (!-d $DATA_DIR) {
+	die("Bad config\n");
+}
+
+# State
+my $state      = 'INIT';
+my $stateLast  = $state;
+my $updateLast = 0;
+my $projector  = 0;
+
+# Loop forever
+while (1) {
+
+	# Monitor the GUI and PLAYING files for changes only
+	{
+		my $mtime = mtime($DATA_DIR . '/PLAYING');
+		if ($mtime > $updateLast) {
+			$updateLast = $mtime;
+		}
+		$mtime = mtime($DATA_DIR . '/GUI');
+		if ($mtime > $updateLast) {
+			$updateLast = $mtime;
+		}
+	}
+
+	# Monitor the PROJECTOR file for state only
+	{
+		$projector = 0;
+		my $fh;
+		open($fh, $DATA_DIR . '/PROJECTOR')
+		  or die("Unable to open PROJECTOR\n");
+		my $text = <$fh>;
+		close($fh);
+		if ($text =~ /1/) {
+			$projector = 1;
+		}
+		if ($DEBUG) {
+			print STDERR 'Projector: ' . $projector . "\n";
+		}
+	}
+
+	# Calculate the new state
+	my $stateLast       = $state;
+	my $timeSinceUpdate = time() - $updateLast;
+	if ($DEBUG) {
+		print STDERR 'Time since update: ' . $timeSinceUpdate . "\n";
+	}
+	if ($state eq 'INIT') {
+		$state = 'OFF';
+		if ($projector) {
+			$state = 'ON';
+		}
+	} elsif ($projector) {
+		if ($timeSinceUpdate > $TIMEOUT) {
+			$state = 'COUNTDOWN';
+			if ($timeSinceUpdate > $TIMEOUT + $COUNTDOWN) {
+				$state = 'OFF';
+			}
+		} else {
+			$state = 'ON';
+		}
+	} else {
+		if ($timeSinceUpdate < $TIMEOUT) {
+			$state = 'ON';
+		}
+	}
+
+	# If the state changed, do something about if
+	if ($state ne $stateLast) {
+		if ($DEBUG) {
+			print STDERR 'State change: ' . $stateLast . ' => ' . $state . "\n";
+		}
+		if ($state eq 'OFF') {
+
+			# Turn off the projector
+			my ($fh, $tmp) = tempfile($DATA_DIR . '/PROJECTOR_CMD.XXXXXXXX', 'UNLINK' => 0);
+			print $fh "OFF\n";
+			close($fh);
+			rename($tmp, $DATA_DIR . '/PROJECTOR_CMD');
+		} elsif ($state eq 'ON') {
+
+			# Do nothing, at least for the moment
+		}
+
+	}
+
+	# Always announce a pending shutdown
+	if ($state eq 'COUNTDOWN') {
+		my $timeLeft = ($TIMEOUT + $COUNTDOWN) - $timeSinceUpdate;
+		$timeLeft = ceil($timeLeft / 60);
+
+		my $plural = 's';
+		if ($timeLeft == 1) {
+			$plural = '';
+		}
+		system('say', 'Projector powerdown in about ' . $timeLeft . ' minute' . $plural);
+	}
+
+	# Wait and loop
+	sleep($DELAY);
+}
+
+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;
+}

+ 232 - 0
amplifier/serial.pl

@@ -0,0 +1,232 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use Device::SerialPort;
+use File::Basename;
+use File::Temp qw( tempfile );
+
+# Prototypes
+sub sendQuery($$);
+sub clearBuffer($);
+sub collectUntil($$);
+
+# Device parameters
+my ($DEV, $PORT, $CRLF, $DELIMITER, %CMDS, $STATUS_ON);
+if (basename($0) =~ /PROJECTOR/i) {
+	$DEV       = 'PROJECTOR';
+	$PORT      = '/dev/tty.Projector-DevB';
+	$CRLF      = "\r\n";
+	$DELIMITER = ':';
+	%CMDS      = (
+		'INIT'   => '',
+		'ON'     => 'PWR ON',
+		'OFF'    => 'PWR OFF',
+		'STATUS' => 'PWR?'
+	);
+	$STATUS_ON = 'PW=01';
+} elsif (basename($0) =~ /AMPLIFIER/i) {
+	$DEV       = 'AMPLIFIER';
+	$PORT      = '/dev/tty.Amplifier-DevB';
+	$CRLF      = "\r\n";
+	$DELIMITER = "\r";
+	%CMDS      = (
+		'INIT'   => '',
+		'ON'     => 'PWON',
+		'OFF'    => 'PWSTANDBY',
+		'STATUS' => 'PW?'
+	);
+	$STATUS_ON = 'PWON';
+} else {
+	die("No device specified\n");
+}
+
+# App config
+my $BYTE_TIMEOUT    = 500;
+my $SILENCE_TIMEOUT = $BYTE_TIMEOUT * 10;
+my $TEMP_DIR        = `getconf DARWIN_USER_TEMP_DIR`;
+chomp($TEMP_DIR);
+my $DATA_DIR = $TEMP_DIR . '/plexMonitor';
+my $CMD_FILE = $DATA_DIR . '/' . $DEV . '_CMD';
+
+# Debug
+my $DEBUG = 0;
+if ($ENV{'DEBUG'}) {
+	$DEBUG = 1;
+}
+
+# Command-line arguments
+my ($DELAY) = @ARGV;
+if (!$DELAY) {
+	$DELAY = 15;
+}
+
+# Sanity check
+if (!-r $PORT || !-d $DATA_DIR) {
+	die("Bad config\n");
+}
+
+# Port init
+my $port = new Device::SerialPort($PORT)
+  or die('Unable to open ' . $DEV . " serial connection\n");
+$port->read_const_time($BYTE_TIMEOUT);
+$port->lookclear();
+
+# Init (clear any previous state)
+sendQuery($port, $CMDS{'INIT'});
+
+# Track the power state
+# Check for new commands
+my $power     = -1;
+my $powerLast = $power;
+while (1) {
+
+	# Check for queued commands
+	my $cmd = undef();
+	if (-r $CMD_FILE) {
+		if ($DEBUG) {
+			print STDERR 'Found ' . $DEV . " command file\n";
+		}
+
+		# Open and immediately unlink the file (to de-queue the command)
+		my $fh;
+		open($fh, $CMD_FILE);
+		unlink($CMD_FILE);
+
+		# If we got the file open before it disappeared
+		if ($fh) {
+			my $text = <$fh>;
+			close($fh);
+			$text =~ s/^\s+//;
+			$text =~ s/\s+$//;
+			if ($DEBUG) {
+				print STDERR 'Got command: ' . $text . "\n";
+			}
+
+			# Only accept valid commands
+			foreach my $name (keys(%CMDS)) {
+				if ($name eq $text) {
+					$cmd = $name;
+					last;
+				}
+			}
+		}
+
+		# Send commands
+		if ($cmd) {
+			if ($DEBUG) {
+				print STDERR 'Sending command: ' . $cmd . "\n";
+			}
+			my $result = sendQuery($port, $CMDS{$cmd});
+		}
+	}
+
+	# Check the power state
+	$powerLast = $power;
+	$power     = 0;
+	my $result = sendQuery($port, $CMDS{'STATUS'});
+	if ($result && $result eq $STATUS_ON) {
+		$power = 1;
+	}
+
+	# If something has changed, save the state to disk
+	if ($powerLast != $power) {
+		if ($DEBUG) {
+			print STDERR 'New ' . $DEV . ' power state: ' . $power . "\n";
+		}
+		my ($fh, $tmp) = tempfile($DATA_DIR . '/' . $DEV . '.XXXXXXXX', 'UNLINK' => 0);
+		print $fh $power . "\n";
+		close($fh);
+		rename($tmp, $DATA_DIR . '/' . $DEV);
+	}
+
+	# Delay and loop
+	sleep($DELAY);
+}
+
+# Cleanup
+$port->close();
+undef($port);
+exit(0);
+
+sub sendQuery($$) {
+	my ($port, $query) = @_;
+
+	# Read until the queue is clear (i.e. no data available)
+	# Since we don't have flow control this always causes one read timeout
+	$port->lookclear();
+	clearBuffer($port);
+
+	# Send the command
+	my $bytes = $port->write($query . $CRLF);
+	if ($DEBUG) {
+		print STDERR "\tWrote (" . $bytes . '): ' . $query . "\n";
+	}
+
+	# Wait for a reply (delimited or timeout)
+	return collectUntil($port, $DELIMITER);
+}
+
+sub clearBuffer($) {
+	my ($port) = @_;
+	my $byte = 1;
+	while (length($byte) > 0) {
+		$byte = $port->read(1);
+		if ($DEBUG && length($byte)) {
+			print STDERR "\tIgnored: " . $byte . "\n";
+		}
+	}
+}
+
+sub collectUntil($$) {
+	my ($port, $char) = @_;
+	if (length($char) != 1) {
+		die('Invalid collection delimiter: ' . $char . "\n");
+	}
+
+	# This byte-by-byte reading is not efficient, but it's safe
+	# Allow reading forever as long as we don't exceed the silence timeout
+	my $count  = 0;
+	my $string = '';
+	while ($count < $SILENCE_TIMEOUT / $BYTE_TIMEOUT) {
+		my $byte = $port->read(1);
+		if (length($byte)) {
+			$count = 0;
+			$string .= $byte;
+
+			if ($DEBUG) {
+				print STDERR "\tRead: " . $byte . "\n";
+			}
+
+			if ($byte eq $char) {
+				last;
+			}
+		} else {
+			$count++;
+		}
+	}
+
+	# Return undef if there was no data (as opposed to just a delimiter and/or whitespace)
+	if (length($string) < 1) {
+		if ($DEBUG) {
+			print STDERR "Read: <NO DATA>\n";
+		}
+		return undef();
+	}
+
+	# Strip leading or trailing whitespace
+	$string =~ s/^\s+//;
+	$string =~ s/\s+$//;
+
+	# Translate CRLF and CR to LF
+	$string =~ s/\r\n/\n/g;
+	$string =~ s/\r/\n/g;
+
+	# Strip the trailing delimiter
+	$string =~ s/${char}$//;
+
+	# Return our clean string
+	if ($DEBUG) {
+		print STDERR 'Read: ' . $string . "\n";
+	}
+	return $string;
+}

+ 0 - 201
projector/projector.pl

@@ -1,201 +0,0 @@
-#!/usr/bin/perl
-use strict;
-use warnings;
-use Device::SerialPort;
-use File::Temp qw( tempfile );
-
-# Prototypes
-sub sendQuery($$);
-sub collectUntil($$);
-
-# Serial parameters
-my $PORT = '/dev/tty.Projector-DevB';
-
-# Protocol parameters
-my $CRLF      = "\r\n";
-my $DELIMITER = ':';
-
-# App config
-my $BYTE_TIMEOUT    = 500;
-my $SILENCE_TIMEOUT = $BYTE_TIMEOUT * 10;
-my $TEMP_DIR        = `getconf DARWIN_USER_TEMP_DIR`;
-chomp($TEMP_DIR);
-my $DATA_DIR = $TEMP_DIR . '/plexMonitor';
-my $CMD_FILE = $DATA_DIR . '/PROJECTOR_CMD';
-
-# Debug
-my $DEBUG = 0;
-if ($ENV{'DEBUG'}) {
-	$DEBUG = 1;
-}
-
-# Command-line arguments
-my ($DELAY) = @ARGV;
-if (!$DELAY) {
-	$DELAY = 15;
-}
-
-# Sanity check
-if (!-r $PORT || !-d $DATA_DIR) {
-	die("Bad config\n");
-}
-
-# Port init
-my $port = new Device::SerialPort($PORT)
-  or die("Unable to open projector serial connection\n");
-$port->read_const_time($BYTE_TIMEOUT);
-$port->lookclear();
-
-# Projector init (clear any previous state)
-sendQuery($port, '');
-
-# Track the projector power state
-my $power     = -1;
-my $powerLast = $power;
-while (1) {
-
-	# Check for queued commands
-	my $cmd = undef();
-	if (-r $CMD_FILE) {
-		if ($DEBUG) {
-			print STDERR "Found projector command file\n";
-		}
-
-		# Open and immediately unlink the file (to de-queue the command)
-		my $fh;
-		open($fh, $CMD_FILE);
-		unlink($CMD_FILE);
-
-		# If we got the file open before it disappeared
-		if ($fh) {
-			my $text = <$fh>;
-			chomp($text);
-			close($fh);
-			if ($DEBUG) {
-				print STDERR 'Got raw command: ' . $text . "\n";
-			}
-
-			# Check for known commands
-			if ($text =~ /OFF/i) {
-				$cmd = 'OFF';
-			} elsif ($text =~ /ON/i) {
-				$cmd = 'ON';
-			}
-			if ($DEBUG) {
-				print STDERR 'Found command: ' . $cmd . "\n";
-			}
-		}
-
-		# If we got a command, validate it
-		if ($cmd) {
-			if ($cmd eq 'ON' && $power) {
-				$cmd = undef();
-			} elsif ($cmd eq 'OFF' && !$power) {
-				$cmd = undef();
-			}
-		}
-
-		# Send the command
-		if ($cmd) {
-			if ($DEBUG) {
-				print STDERR 'Sending command: ' . $cmd . "\n";
-			}
-			my $result = undef();
-			if ($cmd eq 'OFF') {
-				$result = sendQuery($port, 'PWR OFF');
-			} elsif ($cmd eq 'ON') {
-				$result = sendQuery($port, 'PWR ON');
-			}
-		}
-	}
-
-	# Check the power state
-	$powerLast = $power;
-	$power     = 0;
-	my $result = sendQuery($port, 'PWR?');
-	if ($result =~ /PWR\=(\d+)/) {
-		if ($1 > 0) {
-			$power = 1;
-		}
-	}
-
-	# If something has changed, save the state to disk
-	if ($powerLast != $power) {
-		if ($DEBUG) {
-			print STDERR 'New projector power state: ' . $power . "\n";
-		}
-		my ($fh, $tmp) = tempfile($DATA_DIR . '/PROJECTOR.XXXXXXXX', 'UNLINK' => 0);
-		print $fh $power . "\n";
-		close($fh);
-		rename($tmp, $DATA_DIR . '/PROJECTOR');
-	}
-
-	# Delay and loop
-	sleep($DELAY);
-}
-
-# Cleanup
-$port->close();
-undef($port);
-exit(0);
-
-sub sendQuery($$) {
-	my ($port, $query) = @_;
-	$port->lookclear();
-	my $bytes = $port->write($query . $CRLF);
-	if ($DEBUG) {
-		print STDERR "\tWrote (" . $bytes . '): ' . $query . "\n";
-	}
-	my $data = collectUntil($port, $DELIMITER);
-	return $data;
-}
-
-sub collectUntil($$) {
-	my ($port, $char) = @_;
-	if (length($char) != 1) {
-		die('Invalid collection delimiter: ' . $char . "\n");
-	}
-
-	# This byte-by-byte reading is not efficient, but it's safe
-	# Allow reading forever as long as we don't exceed the silence timeout
-	my $count  = 0;
-	my $string = '';
-	while ($count < $SILENCE_TIMEOUT / $BYTE_TIMEOUT) {
-		my $byte = $port->read(1);
-		if ($byte) {
-			$count = 0;
-			$string .= $byte;
-
-			if ($DEBUG) {
-				print STDERR "\tRead: " . $byte . "\n";
-			}
-
-			if ($byte eq $char) {
-				last;
-			}
-		} else {
-			$count++;
-		}
-	}
-
-	# Return undef if there was no data (as opposed to just a delimiter and/or whitespace)
-	if (length($string) < 1) {
-		if ($DEBUG) {
-			print STDERR "Read: <NO DATA>\n";
-		}
-		return undef();
-	}
-
-	# Strip leading or trailing whitespace
-	$string =~ s/^\s+//;
-	$string =~ s/\s+$//;
-
-	# Strip the trailing delimiter
-	$string =~ s/${char}$//;
-
-	# Return our clean string
-	if ($DEBUG) {
-		print STDERR 'Read: ' . $string . "\n";
-	}
-	return $string;
-}

+ 1 - 0
projector/projector.pl

@@ -0,0 +1 @@
+../amplifier/serial.pl