Zachary Kotlarek 14 лет назад
Родитель
Сommit
9f8c5a0408
39 измененных файлов с 2933 добавлено и 0 удалено
  1. 1 0
      amplifier/amplifier.pl
  2. 98 0
      amplifier/amplifierPower.pl
  3. 29 0
      amplifier/launchd/com.uberzach.amplifier.plist
  4. 28 0
      amplifier/launchd/com.uberzach.amplifier.power.plist
  5. 1 0
      dmx/cmd.pl
  6. 1 0
      dmx/dimChannels.py
  7. 126 0
      dmx/dmx.pl
  8. 28 0
      dmx/launchd/com.uberzach.ola.dmx.plist
  9. 31 0
      dmx/launchd/com.uberzach.ola.plist
  10. 31 0
      dmx/launchd/com.uberzach.ola.rope.plist
  11. 262 0
      dmx/rope.pl
  12. 88 0
      dmx/setChannels.py
  13. 3 0
      motion/.gitignore
  14. 829 0
      motion/USBMicroX.c
  15. 350 0
      motion/USBMicroX.h
  16. 25 0
      motion/launchd/com.uberzach.motion.plist
  17. 173 0
      motion/main.c
  18. 13 0
      motion/makefile
  19. 56 0
      plexCmd/plex
  20. 1 0
      plexCmd/plex-button
  21. 23 0
      plexCmd/plex-checkTV
  22. 1 0
      plexCmd/plex-clean
  23. 1 0
      plexCmd/plex-files
  24. 18 0
      plexCmd/plex-filter
  25. 1 0
      plexCmd/plex-gui
  26. 1 0
      plexCmd/plex-playing
  27. 1 0
      plexCmd/plex-resolution
  28. 1 0
      plexCmd/plex-update
  29. 27 0
      plexMonitor/launchd/com.uberzach.plexMonitor.gui.plist
  30. 27 0
      plexMonitor/launchd/com.uberzach.plexMonitor.playing.plist
  31. 1 0
      plexMonitor/plexMonitor-gui
  32. 1 0
      plexMonitor/plexMonitor-playing
  33. 104 0
      plexMonitor/plexMonitor.pl
  34. 29 0
      projector/launchd/com.uberzach.projector.plist
  35. 28 0
      projector/launchd/com.uberzach.projector.power.plist
  36. 1 0
      projector/projector.pl
  37. 153 0
      projector/projectorPower.pl
  38. 42 0
      serial/cmd.pl
  39. 299 0
      serial/serial.pl

+ 1 - 0
amplifier/amplifier.pl

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

+ 98 - 0
amplifier/amplifierPower.pl

@@ -0,0 +1,98 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use IO::Socket::UNIX;
+use POSIX qw(ceil floor);
+
+# App config
+my $DELAY    = 5;
+my $TIMEOUT  = 5;
+my $TEMP_DIR = `getconf DARWIN_USER_TEMP_DIR`;
+chomp($TEMP_DIR);
+my $DATA_DIR = $TEMP_DIR . 'plexMonitor/';
+my $CMD_FILE = $DATA_DIR . 'AMPLIFIER.socket';
+
+# Debug
+my $DEBUG = 0;
+if ($ENV{'DEBUG'}) {
+	$DEBUG = 1;
+}
+
+# Sanity check
+if (!-d $DATA_DIR || !-S $CMD_FILE) {
+	die("Bad config\n");
+}
+
+# Socket init
+my $sock = IO::Socket::UNIX->new(
+	'Peer'    => $CMD_FILE,
+	'Type'    => SOCK_DGRAM,
+	'Timeout' => $TIMEOUT
+) or die('Unable to open socket: ' . $CMD_FILE . ": ${@}\n");
+
+# State
+my $state     = 'INIT';
+my $stateLast = $state;
+my $projector = 0;
+
+# Loop forever
+while (1) {
+
+	# 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";
+		}
+	}
+
+	# Monitor the AMPLIFIER file for state only
+	{
+		$stateLast = 'OFF';
+		my $fh;
+		open($fh, $DATA_DIR . 'AMPLIFIER')
+		  or die("Unable to open AMPLIFIER\n");
+		my $text = <$fh>;
+		close($fh);
+		if ($text =~ /1/) {
+			$stateLast = 'ON';
+		}
+		if ($DEBUG) {
+			print STDERR 'Amplifier: ' . $stateLast . "\n";
+		}
+	}
+
+	# Calculate the new state -- track the projector power state
+	if ($projector) {
+		$state = 'ON';
+	} else {
+		$state = 'OFF';
+	}
+
+	# 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' || $state eq 'ON') {
+			$sock->send($state)
+			  or die('Unable to write command to socket: ' . $CMD_FILE . ': ' . $state . ": ${!}\n");
+		}
+	}
+
+	# Wait and loop
+	sleep($DELAY);
+}
+
+# Cleanup
+$sock->close();
+undef($sock);
+exit(0);

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

@@ -0,0 +1,29 @@
+<?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>PathState</key>
+		<dict>
+			<key>/dev/tty.Amplifier-DevB</key>
+			<true/>
+		</dict>
+	</dict>
+	<key>ProgramArguments</key>
+	<array>
+		<string>/Users/tv/bin/video/amplifier/amplifier.pl</string>
+		<string>15</string>
+	</array>
+</dict>
+</plist>

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

@@ -0,0 +1,28 @@
+<?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.power</string>
+	<key>KeepAlive</key>
+	<dict>
+		<key>OtherJobEnabled</key>
+		<dict>
+			<key>com.uberzach.amplifier</key>
+			<true/>
+		</dict>
+	</dict>
+	<key>ProgramArguments</key>
+	<array>
+		<string>/Users/tv/bin/video/amplifier/amplifierPower.pl</string>
+	</array>
+</dict>
+</plist>

+ 1 - 0
dmx/cmd.pl

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

+ 1 - 0
dmx/dimChannels.py

@@ -0,0 +1 @@
+setChannels.py

+ 126 - 0
dmx/dmx.pl

@@ -0,0 +1,126 @@
+#!/opt/local/bin/python2.7
+
+# Append the path to help find our OLA packages
+import sys
+sys.path.append('/opt/local/lib/python2.7/site-packages')
+sys.path.append('/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages')
+
+import os
+import array
+import select
+import socket
+import string
+import subprocess
+from ola.ClientWrapper import ClientWrapper
+
+# ====================================
+# Globals
+# ====================================
+max_channels = 512
+wrapper = None
+state = [ 0 ] * max_channels
+cmds = { 'value' : [ 0 ] * max_channels, 'ticks' : [ 0 ] * max_channels }
+sock = None
+max_mesg_len = 1024
+
+# ====================================
+# Defaults
+# ====================================
+interval = 50
+universe = 0
+
+# ====================================
+# Wrapper callback -- exit on errors
+# ====================================
+def DmxSent(state):
+  # Stop on errors
+  if not state.Succeeded():
+    wrapper.Stop()
+
+# ====================================
+# Main calculation
+# ====================================
+def SendDMXFrame():
+  # Re-schedule ourselves in interval ms (do this first to keep the timing consistent)
+  wrapper.AddEvent(interval, SendDMXFrame)
+  
+  # Check for new commands
+  
+  ready = select.select([sock], [], [], interval / 2000)[0]
+  for fh in ready:
+    cmd = fh.recvfrom(max_mesg_len)[0]
+    channel, duration, intensity = string.split(cmd, ':')
+    try:
+      channel = int(channel)
+      duration = int(duration)
+      intensity = int(intensity)
+    except ValueError:
+      print 'Invalid command:', cmd
+      channel = -1
+    if (channel >= 0 and channel <= 512 and intensity >= 0 and intensity <= 255 and duration >= 0 and duration <= 60000):
+      if (channel > 0):
+        cmds['value'][channel - 1] = intensity
+        cmds['ticks'][channel - 1] = duration / interval
+      else:
+        for i in range(len(state)):
+          cmds['value'][i] = intensity
+          cmds['ticks'][i] = duration / interval
+    else:
+      print 'Invalid command parameters:', channel, duration, intensity
+  
+  # Update values for each channel
+  for i in range(len(cmds['value'])):
+    if (cmds['value'][i] != state[i]):
+      diff = cmds['value'][i] - state[i]
+      if (cmds['ticks'][i] < 1):
+        delta = diff
+      else:
+        delta = float(diff) / float(cmds['ticks'][i])
+      state[i] += int(delta)
+      cmds['ticks'][i] -= 1
+    
+  # Send all DMX channels
+  data = array.array('B')
+  for i in range(len(state)):
+    data.append(state[i])
+  wrapper.Client().SendDmx(universe, data, DmxSent)
+
+# ====================================
+# Main
+# ====================================
+
+# Pick a universe (default from above, or as specified in the environment)
+if 'UNIVERSE' in os.environ:
+  universe = int(os.environ['UNIVERSE'])
+
+# Pick a tick interval (default from above, or as specified in the environment)
+if 'INTERVAL' in os.environ:
+  universe = int(os.environ['INTERVAL'])
+
+# Pick a socket file ($TMPDIR/plexMonitor/DMX.socket, or as specified in the environment)
+cmd_file = None
+data_dir = None
+if 'SOCKET' in os.environ:
+  cmd_file = os.environ['SOCKET']
+  data_dir = os.dirname(cmd_file)
+else:
+  proc = subprocess.Popen(['getconf', 'DARWIN_USER_TEMP_DIR'], stdout=subprocess.PIPE, shell=False)
+  (tmp_dir, err) = proc.communicate()
+  tmp_dir = tmp_dir.strip()
+  data_dir = tmp_dir + 'plexMonitor/'
+  cmd_file = data_dir + 'DMX.socket'
+
+# Sanity checks
+if (not os.path.isdir(data_dir)):
+  raise Exception('Bad config: ' + data_dir)
+
+# Open the socket
+if (os.path.exists(cmd_file)):
+  os.unlink(cmd_file)
+sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
+sock.bind(cmd_file)
+
+# Start the DMX loop
+wrapper = ClientWrapper()
+wrapper.AddEvent(interval, SendDMXFrame)
+wrapper.Run()

+ 28 - 0
dmx/launchd/com.uberzach.ola.dmx.plist

@@ -0,0 +1,28 @@
+<?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.ola.dmx</string>
+	<key>KeepAlive</key>
+	<dict>
+		<key>OtherJobEnabled</key>
+		<dict>
+			<key>com.uberzach.ola</key>
+			<true/>
+		</dict>
+	</dict>
+	<key>ProgramArguments</key>
+	<array>
+		<string>/Users/tv/bin/video/dmx/dmx.pl</string>
+	</array>
+</dict>
+</plist>

+ 31 - 0
dmx/launchd/com.uberzach.ola.plist

@@ -0,0 +1,31 @@
+<?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.ola</string>
+	<key>KeepAlive</key>
+	<dict>
+		<key>PathState</key>
+		<dict>
+			<key>/dev/tty.usbserial-EN099597</key>
+			<true/>
+		</dict>
+	</dict>
+	<key>ProgramArguments</key>
+	<array>
+		<string>/opt/local/bin/olad</string>
+		<string>-s</string>
+		<string>-i</string>
+		<string>127.0.0.1</string>
+	</array>
+</dict>
+</plist>

+ 31 - 0
dmx/launchd/com.uberzach.ola.rope.plist

@@ -0,0 +1,31 @@
+<?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.ola.rope</string>
+	<key>KeepAlive</key>
+	<dict>
+		<key>OtherJobEnabled</key>
+		<dict>
+			<key>com.uberzach.ola.dmx</key>
+			<true/>
+			<key>com.uberzach.projector</key>
+			<true/>
+		</dict>
+	</dict>
+	<key>ProgramArguments</key>
+	<array>
+		<string>/Users/tv/bin/video/dmx/rope.pl</string>
+		<string>0.25</string>
+	</array>
+</dict>
+</plist>

+ 262 - 0
dmx/rope.pl

@@ -0,0 +1,262 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use IO::Socket;
+use Time::HiRes qw( usleep );
+use File::Temp qw( tempfile );
+
+# Prototypes
+sub mtime($);
+sub dim($$$);
+
+# Channels:
+#	0 => All channels
+#	1 => Couch-side hidden
+#	2 => Couch-side exposed
+#	3 => Stairs
+#	4 => <Unused>
+#	5 => Bar-side hidden
+#	6 => Bar-side bar-shelves
+#	7 => Bar-side exposed
+#	8 => Bar-side ceiling
+
+# User config
+my %DIM = (
+	'OFF'    => [
+		{ 'channel' => 0, 'value' => 0,   'time' => 60000 }
+	],
+	'PLAY'      => [
+		{ 'channel' => 1, 'value' => 64,  'time' => 500   },
+		{ 'channel' => 2, 'value' => 32,  'time' => 500   },
+		{ 'channel' => 3, 'value' => 80,  'time' => 1500  },
+		{ 'channel' => 5, 'value' => 64,  'time' => 500   },
+		{ 'channel' => 6, 'value' => 36,  'time' => 500   },
+		{ 'channel' => 7, 'value' => 32,  'time' => 500   },
+		{ 'channel' => 8, 'value' => 16,  'time' => 500   },
+	],
+	'PLAY_HIGH' => [
+		{ 'channel' => 1, 'value' => 255, 'time' => 500   },
+		{ 'channel' => 2, 'value' => 128, 'time' => 500   },
+		{ 'channel' => 3, 'value' => 192, 'time' => 1500  },
+		{ 'channel' => 5, 'value' => 128, 'time' => 500   },
+		{ 'channel' => 6, 'value' => 36,  'time' => 500   },
+		{ 'channel' => 7, 'value' => 128, 'time' => 500   },
+		{ 'channel' => 8, 'value' => 16,  'time' => 500   },
+	],
+	'PAUSE'     => [
+		{ 'channel' => 1, 'value' => 255, 'time' => 1000  },
+		{ 'channel' => 2, 'value' => 192, 'time' => 10000 },
+		{ 'channel' => 3, 'value' => 192, 'time' => 5000  },
+		{ 'channel' => 5, 'value' => 255, 'time' => 1000  },
+		{ 'channel' => 6, 'value' => 104, 'time' => 12000 },
+		{ 'channel' => 7, 'value' => 192, 'time' => 10000 },
+		{ 'channel' => 8, 'value' => 92,  'time' => 16000 },
+	],
+	'MOTION'    => [
+		{ 'channel' => 1, 'value' => 255, 'time' => 1000  },
+		{ 'channel' => 2, 'value' => 192, 'time' => 1000  },
+		{ 'channel' => 3, 'value' => 192, 'time' => 1000  },
+		{ 'channel' => 5, 'value' => 192, 'time' => 1000  },
+		{ 'channel' => 6, 'value' => 104, 'time' => 1000  },
+		{ 'channel' => 7, 'value' => 192, 'time' => 1000  },
+		{ 'channel' => 8, 'value' => 92,  'time' => 1000  },
+	],
+);
+my $TIMEOUT = 180;
+
+# App config
+my $SOCK_TIMEOUT = 5;
+my $TEMP_DIR     = `getconf DARWIN_USER_TEMP_DIR`;
+chomp($TEMP_DIR);
+my $DATA_DIR = $TEMP_DIR . 'plexMonitor/';
+my $CMD_FILE = $DATA_DIR . 'DMX.socket';
+
+# Debug
+my $DEBUG = 0;
+if ($ENV{'DEBUG'}) {
+	$DEBUG = 1;
+}
+
+# Command-line arguments
+my ($DELAY) = @ARGV;
+if (!$DELAY) {
+	$DELAY = 0.5;
+}
+$DELAY *= 1000000;    # Microseconds;
+
+# Sanity check
+if (!-d $DATA_DIR || !-S $CMD_FILE) {
+	die("Bad config\n");
+}
+
+# Socket init
+my $sock = IO::Socket::UNIX->new(
+	'Peer'    => $CMD_FILE,
+	'Type'    => SOCK_DGRAM,
+	'Timeout' => $SOCK_TIMEOUT
+) or die('Unable to open socket: ' . $CMD_FILE . ": ${@}\n");
+
+# State
+my $state      = 'INIT';
+my $stateLast  = $state;
+my $playing    = 0;
+my $projector  = 0;
+my $lights     = 0;
+my $updateLast = 0;
+
+# Always force lights out at launch
+dim(0, 0, 0);
+
+# Loop forever
+while (1) {
+
+	# Monitor the PLAY_STATUS file for changes and state
+	{
+		my $mtime = mtime($DATA_DIR . 'PLAY_STATUS');
+		if ($mtime > $updateLast) {
+			$updateLast = $mtime;
+		}
+
+		# Grab the PLAY_STATUS value
+		$playing = 0;
+		my $fh;
+		open($fh, $DATA_DIR . 'PLAY_STATUS')
+		  or die("Unable to open PLAY_STATUS\n");
+		my $text = <$fh>;
+		close($fh);
+		if ($text =~ /1/) {
+			$playing = 1;
+		}
+		if ($DEBUG) {
+			print STDERR 'Playing: ' . $playing . "\n";
+		}
+	}
+
+	# Monitor the PROJECTOR file for changes and state
+	{
+		my $mtime = mtime($DATA_DIR . 'PROJECTOR');
+		if ($mtime > $updateLast) {
+			$updateLast = $mtime;
+		}
+
+		# Grab the PROJECTOR value
+		$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";
+		}
+	}
+
+	# Monitor the LIGHTS file for presence
+	{
+		$lights = 0;
+		if (-e $DATA_DIR . 'LIGHTS') {
+			$lights = 1;
+		}
+		if ($DEBUG) {
+			print STDERR 'Lights: ' . $lights . "\n";
+		}
+
+		# Clear the override when the projector is off
+		if ($lights && !$projector) {
+			unlink($DATA_DIR . 'LIGHTS');
+		}
+	}
+
+	# Monitor the GUI, PLAYING, and MOTION 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;
+		}
+		$mtime = mtime($DATA_DIR . 'MOTION');
+		if ($mtime > $updateLast) {
+			$updateLast = $mtime;
+		}
+	}
+
+	# Calculate the new state
+	$stateLast = $state;
+	if ($projector) {
+
+		# We are always either playing or paused if the projector is on
+		if ($playing) {
+
+			# Allow an override to higher brightness during playback
+			if ($lights) {
+				$state = 'PLAY_HIGH';
+			} else {
+				$state = 'PLAY';
+			}
+		} else {
+			$state = 'PAUSE';
+		}
+
+	} else {
+
+		# If the projector is off, check the timeouts
+		my $timeSinceUpdate = time() - $updateLast;
+		if ($timeSinceUpdate > $TIMEOUT) {
+			$state = 'OFF';
+		} elsif ($timeSinceUpdate < $TIMEOUT) {
+			$state = 'MOTION';
+		}
+	}
+	if ($state eq 'INIT') {
+		$state = 'OFF';
+	}
+
+	# Update the lighting state
+	if ($stateLast ne $state) {
+		if ($DEBUG) {
+			print STDERR 'State: ' . $stateLast . ' => ' . $state . "\n";
+			foreach my $data (@{ $DIM{$state} }) {
+				print STDERR "\t" . $data->{'channel'} . ' => ' . $data->{'value'} . ' @ ' . $data->{'time'} . "\n";
+			}
+		}
+
+		# Send the dim command
+		my @values = ();
+		foreach my $data (@{ $DIM{$state} }) {
+			dim($data->{'channel'}, $data->{'time'}, $data->{'value'});
+			push(@values, $data->{'channel'} . ' => ' . $data->{'value'} . ' @ ' . $data->{'time'});
+		}
+
+		# Save the state and value to disk
+		my ($fh, $tmp) = tempfile($DATA_DIR . 'ROPE.XXXXXXXX', 'UNLINK' => 0);
+		print $fh 'State: ' . $state . "\n" . join("\n", @values) . "\n";
+		close($fh);
+		rename($tmp, $DATA_DIR . 'ROPE');
+	}
+
+	# Wait and loop
+	usleep($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;
+}
+
+# Send the command
+sub dim($$$) {
+	my ($channel, $duration, $intensity) = @_;
+	my $cmd = join(':', $channel, $duration, $intensity);
+	$sock->send($cmd)
+	  or die('Unable to write command to socket: ' . $CMD_FILE . ': ' . $cmd . ": ${!}\n");
+}

+ 88 - 0
dmx/setChannels.py

@@ -0,0 +1,88 @@
+#!/usr/bin/python
+import os
+import sys
+import array
+import string
+from ola.ClientWrapper import ClientWrapper
+
+wrapper = None
+dim = False
+dimTick = 0
+dimTime = 0
+numTicks = 0
+
+# Wrapper callback
+def DmxSent(state):
+  # Stop on errors
+  if not state.Succeeded():
+    wrapper.Stop()
+  
+  # Only run one command if we're setting
+  if not dim:
+    wrapper.Stop()
+  
+  # Stop when we're done dimming
+  if dim and dimTick >= numTicks:
+    wrapper.Stop()
+
+# Pick a universe (0, or as specified in the environment)
+universe = 0
+if 'UNIVERSE' in os.environ:
+  universe = int(os.environ['UNIVERSE'])
+
+# Pick a tick interval (50ms, or as specified in the environment)
+interval = 50
+if 'INTERVAL' in os.environ:
+  universe = int(os.environ['INTERVAL'])
+
+# Choose direct-set or dimming
+dim = False
+if 'dim' in string.lower(sys.argv[0]):
+  dim = True
+
+# If we are dimming, parse the command line into 3-part blocks
+# Otherwise just collect the intensities directly
+cmdData = [];
+if dim:
+  dimTime = int(sys.argv[1])
+  numTicks = dimTime / interval
+
+  for i in range(2, len(sys.argv), 2):
+      dimData = array.array('I')
+      dimData.append(int(sys.argv[i]))
+      dimData.append(int(sys.argv[i + 1]))
+      cmdData.append(dimData)
+else:
+  for i in range(1, len(sys.argv)):
+    cmdData.append(int(sys.argv[i]))
+
+def SendDMXFrame():
+  # Re-schedule ourselves in interval ms (do this first to keep the timing consistent)
+  if dim:
+    wrapper.AddEvent(interval, SendDMXFrame)
+  
+  # Eventually we need an array of intesnty bytes for each channel
+  data = array.array('B')
+
+  # Calculate intensities
+  if dim:
+    global dimTick
+    dimTick += 1
+    
+    for i in cmdData:
+      totalRange = i[1] - i[0]
+      delta = float(totalRange) / float(numTicks) * float(dimTick)
+      data.append(int(i[0] + delta))
+
+  else:
+    # Copy each intensity level from the command line
+    for i in cmdData:
+      data.append(i)
+  
+  # Send
+  wrapper.Client().SendDmx(universe, data, DmxSent)
+
+# Send the DMX command
+wrapper = ClientWrapper()
+wrapper.AddEvent(interval, SendDMXFrame)
+wrapper.Run()

+ 3 - 0
motion/.gitignore

@@ -0,0 +1,3 @@
+*.o
+MOTION
+USBMicroX

+ 829 - 0
motion/USBMicroX.c

@@ -0,0 +1,829 @@
+#include "USBMicroX.h"
+#define UNLESS(S) if(!(S))
+
+#define USBMX_ID_PRODUCT 0x01A5
+#define USBMX_ID_VENDOR 0x0DE7
+
+static long dict_get_long(CFDictionaryRef properties, CFStringRef key)
+{
+	long number;
+	CFTypeRef value = CFDictionaryGetValue(properties, key);
+	if (!value) return 0;
+	CFTypeID type = CFGetTypeID(value);
+	if (type != CFNumberGetTypeID()) return 0 ;
+	CFNumberGetValue((CFNumberRef)value, kCFNumberLongType, (void *)&number);
+	return number;
+}
+
+static char* dict_get_string(CFDictionaryRef properties, CFStringRef key, CFStringEncoding encoding)
+{
+	static char cstring[512];
+	CFTypeRef value = CFDictionaryGetValue(properties, key);
+	if (!value) return "";
+	CFTypeID type = CFGetTypeID(value) ;
+	if (type != CFStringGetTypeID()) return "";	
+	CFStringGetCString((CFStringRef)value, cstring, sizeof(cstring), encoding);
+	return cstring; // WARNING: be sure to copy string after return
+}
+
+static void nsp(int L) {
+	int i = 0;
+	while(i++ < L) printf("    ");
+}
+
+static void cftype_dump(CFTypeRef o, int L);
+
+static void cftype_dump_arrayapply(const void *value, void *context) {
+	int L = (int)context;
+	nsp(L); printf("0x%08x\n", value);
+	cftype_dump((CFTypeRef)value, L+1);
+}
+
+static void cftype_dump_dictapply (const void *key, const void *value, void *context) {
+	int L = (int)context;
+	nsp(L); printf("\"%s\" => 0x%08x\n",
+		CFStringGetCStringPtr(key, CFStringGetSystemEncoding()), value);
+	cftype_dump((CFTypeRef)value, L+1);
+}
+
+static void cftype_dump(CFTypeRef o, int L)
+{
+	if(CFGetTypeID(o) == CFArrayGetTypeID()) {
+		nsp(L); printf("[\n");
+		CFArrayApplyFunction(
+			o,
+			CFRangeMake(0, CFArrayGetCount(o)),
+			cftype_dump_arrayapply,
+			(void*)(L+1));
+		nsp(L); printf("]\n");
+	} else if(CFGetTypeID(o) == CFDictionaryGetTypeID()) {
+		nsp(L); printf("{\n");
+		CFDictionaryApplyFunction(
+			o,
+			cftype_dump_dictapply,
+			(void*)(L+1));
+		nsp(L); printf("}\n");
+	} else if(CFGetTypeID(o) == CFNumberGetTypeID()) {
+		long number;
+		CFNumberGetValue((CFNumberRef)o, kCFNumberLongType, (void*)&number);
+		nsp(L); printf("number: 0x%08x\n", number);
+	} else if(CFGetTypeID(o) == CFStringGetTypeID()) {
+		const char *s = CFStringGetCStringPtr(o, CFStringGetSystemEncoding());
+		nsp(L); printf("string: \"%s\"\n", s);
+	} else if(CFGetTypeID(o) == CFBooleanGetTypeID()) {
+		nsp(L); printf("boolean: %s\n", CFBooleanGetValue(o)?"TRUE":"FALSE");		
+	} else {
+		nsp(L); printf("??? unknown type\n");
+	}
+}
+
+int USBmX_DeviceAddedHandler(
+			USBmX_ContextRef ctx, 
+			USBmX_DeviceAddedProc proc, 
+			void *cookie)
+{
+	ctx->deviceAdded = proc;
+	ctx->deviceAdded_cookie = cookie;
+	return 0;
+}
+
+int USBmX_DeviceRemovedHandler(
+			USBmX_ContextRef ctx, 
+			USBmX_DeviceRemovedProc proc, 
+			void *cookie)
+{
+	ctx->deviceRemoved = proc;
+	ctx->deviceRemoved_cookie = cookie;
+	return 0;	
+}
+
+int USBmX_DeviceInfo(USBmX_DeviceRef dev)
+{
+	if(!dev) goto err;
+	cftype_dump(dev->properties, 0);
+	return 0;
+err:
+	return -1;
+}
+
+static void DumpBuffer(const u_char buf[], unsigned int buf_size)
+{
+	const u_char *p = buf;
+	while(buf_size--) printf("0x%02x ", *p++);
+	printf("\n");
+}
+
+void USBmX_DumpCommand(u_char buf[])
+{
+	DumpBuffer(buf, 8);
+}
+
+void DeviceAddedHandler(void *refCon, io_iterator_t iterator)
+{
+	#if USBMX_DEBUG_LOG
+	USBMX_LOG("Device added notification");
+	#endif
+	io_object_t o;
+	USBmX_ContextRef ctx;
+	// Must iterate in order to re-arm notification
+	while(o = IOIteratorNext(iterator)) {
+		IOObjectRelease(o);
+	}
+	ctx = (USBmX_ContextRef)refCon;
+	#if USBMX_DEBUG_LOG
+	USBMX_LOG("ctx = %x", ctx);
+	#endif
+	USBmX_ScanForDevices(ctx);
+}
+
+void DestroyDeviceRef(USBmX_DeviceRef d)
+{
+	#if USBMX_DEBUG_LOG
+	USBMX_LOG("Destroying: %x %x %x %x", d->properties, d->serialNumber, d->interface, d);
+	#endif
+	CFRelease(d->properties);
+	free((void *)d->serialNumber);
+	free((void *)d->deviceName);
+	free((void *)d->manufacturer);
+	(*d->interface)->close(d->interface);
+	free(d);
+}
+
+void DeviceRemovedHandler(void *target, IOReturn result, void *refcon, void *sender)
+{
+	USBmX_DeviceRef d = (USBmX_DeviceRef)refcon;
+	USBmX_ContextRef ctx = d->ctx;
+	#if USBMX_DEBUG_LOG
+	USBMX_LOG("Device Removed (# %s)", d->serialNumber);
+	#endif
+		
+	USBmX_DeviceRef p = ctx->deviceList;
+	USBmX_DeviceRef pl = NULL;
+	while(p) {
+		if(p == d)
+		{
+			if(pl == NULL)	ctx->deviceList = d->next;
+			else			pl->next = d->next;
+			////////////////////////////
+			//USBMX_LOG("NEED TO CALL DEVICE REMOVED CALLBACK");
+			if(ctx->deviceRemoved)
+			{
+				(*ctx->deviceRemoved)(d, ctx->deviceRemoved_cookie);
+			}
+			////////////////////////////
+			DestroyDeviceRef(d);
+			break;
+		}
+		pl = p;
+		p = p->next;
+	}
+}
+
+static void InterruptReportHandler(
+	void *target, 
+	IOReturn result, 
+	void *refcon, 
+	void *sender, 
+	UInt32 bufferSize)
+{
+	USBmX_DeviceRef d = (USBmX_DeviceRef)target;
+
+	#if USBMX_DEBUG_LOG
+	printf("Response: ");
+	DumpBuffer(d->response_buffer, bufferSize);
+	#endif
+	
+	d->response_ready = 1;
+}
+
+static int SetReport(USBmX_DeviceRef d, const u_char *buf, unsigned int buf_size)
+{
+	#if USBMX_DEBUG_LOG
+	printf("Report: "); DumpBuffer(buf, buf_size);
+	#endif
+	d->response_ready = 0;
+	IOReturn rc = (*d->interface)->setReport(
+		d->interface, 
+		kIOHIDReportTypeFeature,
+		0, //reportID
+		(void *)buf,
+		buf_size,
+		1000, //timeout
+		NULL,
+		NULL,
+		NULL
+		);
+	while(!d->response_ready) {
+		SInt32 r = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.001, false);
+		#if USBMX_DEBUG_LOG
+		printf("wait r=%x\n", r);
+		#endif
+	}
+	if(rc != kIOReturnSuccess) {
+		USBMX_LOG("SetReport: (*interface)->setReport() Error %x", rc);
+		return -1;
+	}
+	return 0;
+}
+
+int USBmX_DeviceCount(USBmX_ContextRef ctx)
+{
+	if(!ctx) return 0;
+	USBmX_DeviceRef d = ctx->deviceList;
+	int count = 0;
+	while(d) {
+		++count;
+		d = d->next;
+	}
+	return count;
+}
+
+USBmX_DeviceRef USBmX_DefaultDevice(USBmX_ContextRef ctx)
+{
+	if(ctx) return ctx->deviceList;
+	return NULL;
+}
+
+USBmX_DeviceRef USBmX_DeviceWithSerial(USBmX_ContextRef ctx, const char *serial)
+{
+	if(!ctx || !serial) return NULL;
+	USBmX_DeviceRef dev = ctx->deviceList;
+	#if USBMX_DEBUG_LOG
+	USBMX_LOG("Searching for device...");
+	#endif
+	while(dev) {
+		if(!strcmp(serial, dev->serialNumber))
+		{
+			#if USBMX_DEBUG_LOG
+			USBMX_LOG("Device found! %x (# %s)", dev, dev->serialNumber);
+			#endif			
+			return dev;
+		}
+		dev = dev->next;
+	}
+	#if USBMX_DEBUG_LOG
+	USBMX_LOG("Device not found");
+	#endif
+	return NULL;
+}
+
+int USBmX_RawCommand(USBmX_DeviceRef dev, 
+	const u_char command[8], 
+	u_char response[8])
+{
+	if(!dev) goto err;
+	if(SetReport(dev, command, 8) < 0) goto err;
+	if(response) {
+		memcpy(response, dev->response_buffer, 8);
+	}
+	return 0;
+err:
+	return -1;
+}
+
+static int CreateDeviceRef(USBmX_ContextRef ctx, io_object_t dev, USBmX_DeviceRef *dev_out)
+{
+	IOReturn rc;
+	kern_return_t result;
+
+	USBmX_DeviceRef dr = USBMX_ALLOC(USBmX_Device);
+	dr->next = NULL;
+	dr->ctx = ctx;
+	dr->userData = NULL;
+	
+	// Get device properties
+	result = IORegistryEntryCreateCFProperties(
+		dev, &dr->properties,
+		kCFAllocatorDefault, kNilOptions);
+	if(result != KERN_SUCCESS || !dr->properties) {
+		USBMX_LOG("IORegistryEntryCreateCFProperties Error %x", result);			
+		goto err;
+	}
+
+	dr->serialNumber = strdup(
+		dict_get_string(
+			dr->properties,
+			CFSTR(kIOHIDSerialNumberKey), 
+			CFStringGetSystemEncoding()));
+	dr->deviceName = strdup(
+		dict_get_string(
+			dr->properties,
+			CFSTR(kIOHIDProductKey), 
+			CFStringGetSystemEncoding()));
+	dr->manufacturer = strdup(
+		dict_get_string(
+			dr->properties,
+			CFSTR(kIOHIDManufacturerKey), 
+			CFStringGetSystemEncoding()));
+
+	// Add device to head of list if we don't already have it
+	if(USBmX_DeviceWithSerial(ctx, dr->serialNumber) == NULL)
+	{
+		#if USBMX_DEBUG_LOG
+		USBMX_LOG("Adding device");
+		#endif
+		dr->next = ctx->deviceList;
+		ctx->deviceList = dr;
+	} else {
+		#if USBMX_DEBUG_LOG
+		USBMX_LOG("Already have device");
+		#endif
+		return 0;
+	}
+
+	#if USBMX_DEBUG_LOG
+	USBMX_LOG("Found Device (# %s)", dr->serialNumber);
+	USBMX_LOG("kIOHIDManufacturerKey: \"%s\"",
+		dict_get_string(dr->properties, 
+		CFSTR(kIOHIDManufacturerKey), CFStringGetSystemEncoding()));
+	#endif
+
+	{	// Set up the device
+		SInt32 score = 0;
+		IOCFPlugInInterface **plugIn;
+
+		if((rc = IOCreatePlugInInterfaceForService(
+			dev,
+			kIOHIDDeviceUserClientTypeID,
+			kIOCFPlugInInterfaceID,
+			&plugIn, &score)) != kIOReturnSuccess)
+		{
+				USBMX_LOG("IOCreatePlugInInterfaceForService Error %x", rc);
+				goto err;
+		}
+		
+		// Grab interface
+		IOHIDDeviceInterface **interface;
+		HRESULT prc;
+		prc = (*plugIn)->QueryInterface(plugIn,
+			CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID),
+			(LPVOID)&interface);
+		(*plugIn)->Release(plugIn);
+		if(prc != S_OK) {
+			USBMX_LOG("QueryInterface Error %x", prc);
+			goto err;
+		}
+		
+		dr->interface = interface;
+
+		// Set up removal callback
+		if((rc = (*interface)->setRemovalCallback(interface, 
+			DeviceRemovedHandler, NULL, (void *)dr)) != kIOReturnSuccess)
+		{
+			USBMX_LOG("(*interface)->setRemovalCallback() Error %x", rc);
+			goto err1;
+		}
+
+		if((rc = (*interface)->open(interface, 0)) != kIOReturnSuccess)
+		{
+			USBMX_LOG("(*interface)->open() Error %x", rc);
+			goto err1;
+        }
+
+		mach_port_t async_port;
+		if((rc = (*interface)->createAsyncPort(interface, &async_port)) != kIOReturnSuccess)
+		{
+			USBMX_LOG("(*interface)->createAsyncPort() Error %x", rc);
+			goto err1;
+        }
+		
+		CFRunLoopSourceRef rl_source;
+		if((rc = (*interface)->createAsyncEventSource(interface, &rl_source)) != kIOReturnSuccess)
+		{
+			USBMX_LOG("(*interface)->createAsyncEventSource() Error %x", rc);
+			goto err1;
+		}
+
+		CFRunLoopAddSource(
+			CFRunLoopGetCurrent(),
+			rl_source,
+			kCFRunLoopDefaultMode);
+		if((rc = ((IOHIDDeviceInterface122*)*interface)->setInterruptReportHandlerCallback(
+			interface,
+			dr->response_buffer,
+			USBMX_RESPONSE_BUFFER_SIZE,
+			InterruptReportHandler,
+			(void *)dr,
+			NULL)) != kIOReturnSuccess)
+		{
+			USBMX_LOG("(*interface)->setInterruptReportHandlerCallback() Error %x", rc);
+			goto err1;
+		}
+	}
+	
+	*dev_out = dr;
+	////////////////
+	//USBMX_LOG("NEED TO CALL DEVICE ADDED CALLBACK");
+	if(ctx->deviceAdded)
+	{
+		(*ctx->deviceAdded)(dr, ctx->deviceAdded_cookie);
+	}
+	////////////////
+	return 0;
+err1:
+	(*dr->interface)->Release(dr->interface);
+err:
+	IOObjectRelease(dev);
+	free(dr);
+	return -1;
+}
+
+int USBmX_ScanForDevices(USBmX_ContextRef ctx)
+{
+	IOReturn rc;	
+	io_iterator_t iterator;
+	io_object_t d;
+	int n_devices = 0;
+	
+	CFRetain(ctx->matchingDict);
+	
+	// Narrow it down to devices matching vendor and product IDs
+	if(rc = IOServiceGetMatchingServices(
+		kIOMasterPortDefault,
+		ctx->matchingDict,
+		&iterator) != kIOReturnSuccess)
+	{
+			USBMX_LOG("IOServiceGetMatchingServices Error %x", rc);
+			goto err;
+	}
+
+	// Iterate over each found device
+	while(d = IOIteratorNext(iterator))
+	{
+		USBmX_DeviceRef dev;
+		if(CreateDeviceRef(ctx, d, &dev) < 0) goto err1;
+		n_devices++;
+	}
+	
+	IOObjectRelease(iterator);
+
+noerr:
+	return n_devices;
+err1:
+	IOObjectRelease(iterator);
+err:
+	USBMX_LOG("USBmX_FindDevices Failed!");
+	return -1;
+}
+
+USBmX_ContextRef USBmX_Create(void)
+{
+	USBmX_ContextRef ctx = NULL;
+	if(ctx = USBMX_ALLOC(USBmX_Context))
+	{
+		ctx->deviceList = NULL;
+		ctx->matchingDict = NULL;
+		ctx->deviceAdded = NULL;
+		ctx->deviceRemoved = NULL;
+		ctx->deviceAdded_cookie = NULL;
+		ctx->deviceRemoved_cookie = NULL;
+	}
+	return ctx;
+}
+
+int USBmX_Begin(USBmX_ContextRef ctx)
+{
+//	USBmX_ContextRef ctx = USBMX_ALLOC(USBmX_Context);
+//	ctx->deviceList = NULL;
+//	ctx->matchingDict = NULL;
+	if(!ctx) return -1;
+
+	// Grab root IOKit HID services
+	UNLESS(ctx->matchingDict = IOServiceMatching(kIOHIDDeviceKey))
+	{
+		USBMX_LOG("IOServiceMatching(kIOHIDDeviceKey) Error");
+		goto err;
+	}
+
+	{	// Add product and vendor keys to matchingDict
+		SInt32 product = USBMX_ID_PRODUCT;
+		SInt32 vendor = USBMX_ID_VENDOR;
+		CFNumberRef num = NULL;
+		#define USBMX_DNA(NAME, VALUE) \
+			UNLESS(num = CFNumberCreate( \
+					kCFAllocatorDefault, \
+					kCFNumberSInt32Type, \
+					& VALUE )) { \
+				USBMX_LOG("CFNumber Error"); \
+				goto err; \
+			} \
+			CFDictionaryAddValue(ctx->matchingDict, CFSTR(NAME), num); \
+			CFRelease(num);
+		USBMX_DNA(kIOHIDVendorIDKey, vendor)
+		USBMX_DNA(kIOHIDProductIDKey, product)
+		#undef USBMX_DNA
+	}
+
+	{	// Set up onPlug handler
+		io_iterator_t plugIterator;
+		IONotificationPortRef plugPort;
+		//CFRunLoopSourceRef runLoopSource;
+		
+		//CFMutableDictionaryRef hd = IOServiceMatching(kIOHIDDeviceKey);
+		CFRetain(ctx->matchingDict);
+
+		UNLESS(plugPort = IONotificationPortCreate(kIOMasterPortDefault))
+		{
+			USBMX_LOG("IONotificationPortCreate error");		
+		}
+		
+		CFRunLoopAddSource(
+			CFRunLoopGetCurrent(),
+			IONotificationPortGetRunLoopSource(plugPort),
+			kCFRunLoopDefaultMode);
+		if(IOServiceAddMatchingNotification(
+			plugPort,
+			kIOFirstMatchNotification,
+			ctx->matchingDict,
+			DeviceAddedHandler,
+			(void *)ctx,
+			&plugIterator) != KERN_SUCCESS)
+		{
+			USBMX_LOG("IOServiceAddMatchingNotification error");		
+		}
+		DeviceAddedHandler((void *)ctx, plugIterator);
+	}
+
+	//if(USBmX_ScanForDevices(ctx) < 0) goto err;
+noerr:
+	//return ctx;
+	return 0;
+err:
+	USBMX_LOG("USBmX_Init Failed!");
+	free(ctx);
+	//return NULL;
+	return -1;
+}
+
+int USBmX_Destroy(USBmX_ContextRef ctx)
+{
+	if(!ctx) return -1;
+	free(ctx);
+	return 0;
+}
+
+int USBmX_InitPorts(USBmX_DeviceRef dev)
+{
+	int r;
+	u_char res[8];
+	static const u_char cmd[8] = {
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00
+	};
+	r = USBmX_RawCommand(dev, cmd, res);
+	if(r < 0) return r;
+	if(res[0] != cmd[0]) return -1;
+	return 0;
+}
+
+int USBmX_WriteA(USBmX_DeviceRef dev, u_char value)
+{
+	int r;
+	u_char res[8];
+	u_char cmd[8] = {
+		0x01,
+		value,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00
+	};
+	r = USBmX_RawCommand(dev, cmd, res);
+	if(r < 0) return r;
+	if(res[0] != cmd[0]) return -1;
+	return 0;	
+}
+
+int USBmX_WriteB(USBmX_DeviceRef dev, u_char value)
+{
+	int r;
+	u_char res[8];
+	u_char cmd[8] = {
+		0x02,
+		value,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00
+	};
+	r = USBmX_RawCommand(dev, cmd, res);
+	if(r < 0) return r;
+	if(res[0] != cmd[0]) return -1;
+	return 0;	
+}
+
+int USBmX_WriteABit(USBmX_DeviceRef dev, u_char v_and, u_char v_or)
+{
+	int r;
+	u_char res[8];
+	u_char cmd[8] = {
+		0x03,
+		v_and,
+		v_or,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00
+	};
+	r = USBmX_RawCommand(dev, cmd, res);
+	if(r < 0) return r;
+	if(res[0] != cmd[0]) return -1;
+	return 0;	
+}
+
+int USBmX_WriteBBit(USBmX_DeviceRef dev, u_char v_and, u_char v_or)
+{
+	int r;
+	u_char res[8];
+	u_char cmd[8] = {
+		0x04,
+		v_and,
+		v_or,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00
+	};
+	r = USBmX_RawCommand(dev, cmd, res);
+	if(r < 0) return r;
+	if(res[0] != cmd[0]) return -1;
+	return 0;	
+}
+
+int USBmX_ReadA(USBmX_DeviceRef dev, u_char *value_out)
+{
+	int r;
+	u_char res[8];
+	u_char cmd[8] = {
+		0x05,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00
+	};
+	r = USBmX_RawCommand(dev, cmd, res);
+	if(value_out) *value_out = res[1];
+	if(r < 0) return r;
+	if(res[0] != cmd[0]) return -1;
+	return 0;
+}
+
+int USBmX_ReadB(USBmX_DeviceRef dev, u_char *value_out)
+{
+	int r;
+	u_char res[8];
+	u_char cmd[8] = {
+		0x06,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00
+	};
+	r = USBmX_RawCommand(dev, cmd, res);
+	if(value_out) *value_out = res[1];
+	if(r < 0) return r;
+	if(res[0] != cmd[0]) return -1;
+	return 0;
+}
+
+int USBmX_SetBit(USBmX_DeviceRef dev, u_char line)
+{
+	int r;
+	u_char res[8];
+	u_char cmd[8] = {
+		0x07,
+		line,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00
+	};
+	r = USBmX_RawCommand(dev, cmd, res);
+	if(r < 0) return r;
+	if(res[0] != cmd[0]) return -1;
+	return 0;
+}
+
+int USBmX_ResetBit(USBmX_DeviceRef dev, u_char line)
+{
+	int r;
+	u_char res[8];
+	u_char cmd[8] = {
+		0x08,
+		line,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00
+	};
+	r = USBmX_RawCommand(dev, cmd, res);
+	if(r < 0) return r;
+	if(res[0] != cmd[0]) return -1;
+	return 0;
+}
+
+int USBmX_DirectionA(USBmX_DeviceRef dev, u_char dir0, u_char dir1)
+{
+	int r;
+	u_char res[8];
+	u_char cmd[8] = {
+		0x09,
+		dir0,
+		dir1,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00
+	};
+	r = USBmX_RawCommand(dev, cmd, res);
+	if(r < 0) return r;
+	if(res[0] != cmd[0]) return -1;
+	return 0;
+}
+
+int USBmX_DirectionB(USBmX_DeviceRef dev, u_char dir0, u_char dir1)
+{
+	int r;
+	u_char res[8];
+	u_char cmd[8] = {
+		0x0A,
+		dir0,
+		dir1,
+		0x00,
+		0x00,
+		0x00,
+		0x00,
+		0x00
+	};
+	r = USBmX_RawCommand(dev, cmd, res);
+	if(r < 0) return r;
+	if(res[0] != cmd[0]) return -1;
+	return 0;
+}
+
+int USBmX_StrobeWrite(USBmX_DeviceRef dev, 
+	u_char data, 
+	u_char port_select, 
+	u_char strobe_line,
+	u_char pulse_length)
+{
+	return -1;
+}
+
+int USBmX_StrobeRead(USBmX_DeviceRef dev, 
+	u_char *data_out, 
+	u_char port_select, 
+	u_char strobe_line,
+	u_char pulse_length)
+{
+	return -1;
+}
+
+int USBmX_StrobeWrites(USBmX_DeviceRef dev, 
+	u_char n_bytes, 
+	u_char bytes[])
+{
+	return -1;
+}
+
+int USBmX_InitLCD(USBmX_DeviceRef dev,
+	u_char rw_select,
+	u_char rs_select,
+	u_char port_select,
+	u_char e_select)
+{
+	return -1;
+}
+
+int USBmX_LCDCmd(USBmX_DeviceRef dev, u_char cmd)
+{
+	return -1;
+}
+
+int USBmX_LCDData(USBmX_DeviceRef dev, u_char data)
+{
+	return -1;
+}

+ 350 - 0
motion/USBMicroX.h

@@ -0,0 +1,350 @@
+#ifndef _USBMICROX_H_
+#define _USBMICROX_H_
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <libgen.h>
+#include <mach/mach.h>
+#include <sys/types.h>
+#include <CoreFoundation/CFNumber.h>
+#include <CoreFoundation/CoreFoundation.h>
+#include <IOKit/IOKitLib.h>
+#include <IOKit/IOCFPlugIn.h>
+#include <IOKit/usb/IOUSBLib.h>
+#include <IOKit/hid/IOHIDLib.h>
+#include <IOKit/hid/IOHIDKeys.h>
+
+#define USBMX_LOG(S, ...) \
+	({ fprintf(stderr, "/%s/%s/%d >>> " S "\n", basename(__FILE__) , \
+	__FUNCTION__ , __LINE__ , ## __VA_ARGS__ ); })
+#define USBMX_CMD(...) (u_char[8]){ __VA_ARGS__ }
+#define USBMX_ALLOC(TYPE) (TYPE*)malloc(sizeof(TYPE))
+
+#define USBMX_DEBUG_LOG 0
+#define USBMX_RESPONSE_BUFFER_SIZE 8
+
+struct _USBmX_Context;
+
+typedef struct _USBmX_Device {
+	struct _USBmX_Context *ctx;
+	IOHIDDeviceInterface **interface;
+	CFMutableDictionaryRef properties;
+	const char *serialNumber;
+	const char *deviceName;
+	const char *manufacturer;
+	int response_ready;
+	u_char response_buffer[USBMX_RESPONSE_BUFFER_SIZE];
+	struct _USBmX_Device *next;
+	void *userData;
+} USBmX_Device;
+typedef USBmX_Device* USBmX_DeviceRef;
+
+typedef void (*USBmX_DeviceAddedProc)(USBmX_DeviceRef dev, void *cookie);
+typedef void (*USBmX_DeviceRemovedProc)(USBmX_DeviceRef dev, void *cookie);
+
+typedef struct _USBmX_Context {
+	USBmX_DeviceRef deviceList;
+	CFMutableDictionaryRef matchingDict;
+	USBmX_DeviceAddedProc deviceAdded;
+	USBmX_DeviceRemovedProc deviceRemoved;
+	void *deviceAdded_cookie;
+	void *deviceRemoved_cookie;
+} USBmX_Context;
+typedef USBmX_Context* USBmX_ContextRef;
+
+/*
+ *	USBmX_Create:
+ *	Creates a USBmX_ContextRef used in all other
+ *	routines.  Should return a valid USBmX_ContextRef
+ *	even if no devices are conected.
+ */
+extern USBmX_ContextRef USBmX_Create(void);
+
+extern int USBmX_DeviceAddedHandler(
+			USBmX_ContextRef ctx, 
+			USBmX_DeviceAddedProc proc, 
+			void *cookie);
+extern int USBmX_DeviceRemovedHandler(
+			USBmX_ContextRef ctx, 
+			USBmX_DeviceRemovedProc proc, 
+			void *cookie);
+
+/*
+ *	USBmX_Begin:
+ *	You must call USBmX_Begin to init the USB
+ *	subsystem.  Call before sending any commands.
+ *	Returns < 0 on error.
+ */
+extern int USBmX_Begin(USBmX_ContextRef ctx);
+
+/*
+ *	USBmX_ScanForDevices:
+ *	Call to manually re-scan the USB bus for newly
+ *	connected devices.  You should never have to
+ *	call this yourself (there are internal
+ *	notifications that should call this automatically
+ *	when a device is plugged in.
+ *	Returns < 0 on error.
+ */
+extern int USBmX_ScanForDevices(USBmX_ContextRef ctx);
+
+/*
+ *	USBmX_Destroy:
+ *	Call when you are done messing with all devices.
+ */
+extern int USBmX_Destroy(USBmX_ContextRef ctx);
+
+/*
+ *	USBmX_DeviceCount:
+ *	Get the number of connected devices.
+ */
+extern int USBmX_DeviceCount(USBmX_ContextRef ctx);
+
+/*
+ *	USBmX_DefaultDevice:
+ *	Get the default devico (generally the last device
+ *	that was plugged in.  Useful if you only have 1
+ *	device plugged in and don't want to mess with the
+ *	serial number.  Returns NULL if there are no
+ *	devices connected.
+ */
+extern USBmX_DeviceRef USBmX_DefaultDevice(USBmX_ContextRef ctx);
+
+/*
+ *	USBmX_DeviceWithSerial:
+ *	Get the device matching the provided serial
+ *	number.  Returns NULL if the device cannot be
+ *	found.
+ */
+extern USBmX_DeviceRef USBmX_DeviceWithSerial(USBmX_ContextRef ctx, const char *serial);
+
+/*
+ *	USBmX_DumpCommand:
+ *	Small utility function that prints out an 8 byte
+ *	buffer in human readable form.  Useful for
+ *	printing commands and responses.
+ */
+extern void USBmX_DumpCommand(u_char buf[]);
+
+/*
+ *	USBmX_DeviceInfo:
+ *	Outputs as much infomation as possible on a
+ *	given device.
+ */
+extern int USBmX_DeviceInfo(USBmX_DeviceRef dev);
+
+
+/*
+*****************************************************
+*****************  Commands  ************************
+*****************************************************
+The following command functions return 0 on success
+and < 0 on error.
+
+Where the command demands that you chose a 'line'
+(A0..A7, or B0..B7), the table below may be helpful:
+Value	Line
+-----	----
+0x00	A0
+0x01	A1
+0x02	A2
+0x03	A3
+0x04	A4
+0x05	A5
+0x06	A6
+0x07	A7
+0x08	B0
+0x09	B1
+0x0A	B2
+0x0B	B3
+0x0C	B4
+0x0D	B5
+0x0E	B6
+0x0F	B7
+*/
+
+
+/*
+ *	USBmX_RawCommand:
+ *	Performs a raw command on the specifie device.
+ *	More info can be found here:
+ *	http://usbmicro.com/odn/index.html
+ *	under "Raw Command Summary"
+ */
+extern int USBmX_RawCommand(USBmX_DeviceRef dev, 
+	const u_char command[8], 
+	u_char response[8]);
+
+/*
+The initial state of the ports on power up is that all
+of the 16 lines are set to be inputs. This command 
+duplicates the power up conditions of setting all lines 
+to input.
+*/
+extern int USBmX_InitPorts(USBmX_DeviceRef dev);
+
+/*
+Write a byte value to specified port. The possible values 
+range from 0-255 (0x00..0xFF) (passed in value).
+*/
+extern int USBmX_WriteA(USBmX_DeviceRef dev, u_char value);
+extern int USBmX_WriteB(USBmX_DeviceRef dev, u_char value);
+
+/*
+Write masked values to specified port. The net result of 
+writing masked values is that only the specified bits will 
+be written. The resulting port condition is the logic 
+combination of the current port state ANDed with the 
+first term (v_and) and then ORed with the second (v_or). 
+This command can affect any number of lines on the port.
+*/
+extern int USBmX_WriteABit(USBmX_DeviceRef dev, u_char v_and, u_char v_or);
+extern int USBmX_WriteBBit(USBmX_DeviceRef dev, u_char v_and, u_char v_or);
+
+/*
+Read a byte value from specified port. The returned value
+(returned in *value_out) is the state of the port lines that 
+an external device has set, if the line is an input. 
+The lines that might be configured as outputs return the 
+output state.
+*/
+extern int USBmX_ReadA(USBmX_DeviceRef dev, u_char *value_out);
+extern int USBmX_ReadB(USBmX_DeviceRef dev, u_char *value_out);
+
+/*
+Set a single bit/line high. (0..15)
+*/
+extern int USBmX_SetBit(USBmX_DeviceRef dev, u_char line);
+
+/*
+Set a single bit/line low. (0..15)
+*/
+extern int USBmX_ResetBit(USBmX_DeviceRef dev, u_char line);
+
+/*
+Set the i/o direction of specified port.
+dir0 is written to the direction0 control register, dir1 
+is written to the direction1 control register appropriate 
+for the port.
+The individual pins of the ports can be set to input, or 
+output. Input is set when both bits associated with a 
+particular line are set to 0. There are different types 
+of outputs available for the board, but generally setting 
+both of the direction bits to 1 will suffice.
+High order bits of dir0 and dir1 correspond to high order
+lines.
+*/
+extern int USBmX_DirectionA(USBmX_DeviceRef dev, u_char dir0, u_char dir1);
+extern int USBmX_DirectionB(USBmX_DeviceRef dev, u_char dir0, u_char dir1);
+
+/*
+Strobe write of a byte value to a port. This command 
+selects port A (port_select=0x00) or B (port_select=0x01) 
+for the written byte, as well as a polarity (negative or 
+positive) and a line (A.0 - B.7 (strobe_line=0..15)) to 
+toggle. The byte is written and then the line toggled.
+pulse_length can be anywhere between 0x00..0xFF.
+Default is to have the strobe negative-going 
+(high->low->high).  Add 0x10 to strobe_line to have the
+pulse be positive-going (low->high->low).
+*/
+extern int USBmX_StrobeWrite(USBmX_DeviceRef dev, 
+	u_char data, 
+	u_char port_select, 
+	u_char strobe_line,
+	u_char pulse_length);
+
+/*
+Strobe read of a byte value from a port. This command 
+selects port A (port_select=0x00) or B (port_select=0x01) 
+for the read byte, as well as a polarity (negative or 
+positive) and a line (A.0 - B.7 (strobe_line=0..15)) to 
+toggle. The line is toggled and the byte is read.
+pulse_length can be anywhere between 0x00..0xFF.
+Default is to have the strobe negative-going 
+(high->low->high).  Add 0x10 to strobe_line to have the
+pulse be positive-going (low->high->low).
+*data_out is set to the byte read.
+*/
+extern int USBmX_StrobeRead(USBmX_DeviceRef dev, 
+	u_char *data_out, 
+	u_char port_select, 
+	u_char strobe_line,
+	u_char pulse_length);
+
+/*
+Strobe write of a 1 to 6 byte value to a port. This 
+command uses port A or B for the written byte, as well 
+as a polarity (negative or positive) and a line 
+(A.0 - B.7) to toggle based on the previously executed 
+USBmX_StrobeWrite or USBmX_StrobeRead.
+Control this command's port for the written byte, 
+polarity and strobe line by using the USBmX_StrobeWrite 
+or USBmX_StrobeRead. This command will then use the 
+same selected port, polarity, and strobe line to write 
+up to 6 bytes.
+n_bytes is the number of bytes to write.
+bytes[] is an array of bytes, the first n_bytes values
+will be written.  It must be at least of size n_bytes.
+*/
+extern int USBmX_StrobeWrites(USBmX_DeviceRef dev, 
+	u_char n_bytes, 
+	u_char bytes[]);
+
+/*
+Strobe read of a 1 to 6 byte value from a port. This 
+command uses port A or B for the read byte, as well 
+as a polarity (negative or positive) and a line 
+(A.0 - B.7) to toggle based on the previously executed 
+USBmX_StrobeWrite or USBmX_StrobeRead.
+Control this command's port for the written byte, 
+polarity and strobe line by using the USBmX_StrobeWrite 
+or USBmX_StrobeRead. This command will then use the 
+same selected port, polarity, and strobe line to read 
+up to 6 bytes.
+n_bytes is the number of bytes to read.
+bytes[] is an array of bytes that the read values will be 
+copied into.  It must be at least of size n_bytes.
+*/
+extern int USBmX_StrobeReads(USBmX_DeviceRef dev, 
+	u_char n_bytes, 
+	u_char bytes[]);
+
+/*
+Initialize LCD variables. This includes the selection 
+of the lines used for RW, RS, E and the port used for 
+data. RW, RS, and E are reset. These commands are 
+appropriate for HD44780 devices and devices that are 
+compatible.
+port_select = 0 for port A, or 1 for port B.
+See table (way) above for line->number mappings.
+*/
+extern int USBmX_InitLCD(USBmX_DeviceRef dev,
+	u_char rw_select,
+	u_char rs_select,
+	u_char port_select,
+	u_char e_select);
+	
+/*
+Write a command to the LCD. The RS, RW, E lines and the 
+data port are selected by the USBmX_InitLCD. The data 
+byte is written to the selected port and the control 
+lines are set appropriately. The E line is toggled for 
+five to eight microseconds.
+*/
+extern int USBmX_LCDCmd(USBmX_DeviceRef dev, u_char cmd);
+
+/*
+Write a character to the LCD. The RS, RW, E lines and 
+the data port are selected by the USBmX_InitLCD. The data 
+byte is written to the selected port and the control 
+lines are set appropriately. The E line is toggled for 
+five to eight microseconds.
+*/
+extern int USBmX_LCDData(USBmX_DeviceRef dev, u_char data);
+
+// TODO: SPI + Stepper
+
+#endif /* _USBMICROX_H_ */

+ 25 - 0
motion/launchd/com.uberzach.motion.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.motion</string>
+	<key>KeepAlive</key>
+	<dict>
+		<key>NetworkState</key>
+		<true/>
+	</dict>
+	<key>ProgramArguments</key>
+	<array>
+		<string>/Users/tv/bin/video/motion/USBMicroX</string>
+	</array>
+</dict>
+</plist>

+ 173 - 0
motion/main.c

@@ -0,0 +1,173 @@
+#include "USBMicroX.h"
+#include <fcntl.h>
+#include <sys/stat.h>
+
+#define DATA_DIR "plexMonitor/"
+#define OUT_FILE "MOTION"
+#define NUM_DEVICES 2
+#define SERIALS { "101128160542", "101128155210" }
+#define SLEEP_DELAY 250000
+#define TIMEOUT 2
+
+//#define DEBUG
+
+// Typedef
+typedef unsigned char u8;
+
+// Prototypes
+void timeout(int sig);
+USBmX_DeviceRef init();
+u8 readDev(USBmX_DeviceRef dev);
+char * initPaths();
+
+// Globals
+USBmX_ContextRef ctx;
+
+int main(int argc, const char * argv[])
+{
+	char *outfile;
+	unsigned int i, motion;
+	USBmX_DeviceRef device[NUM_DEVICES];
+	const char *serials[NUM_DEVICES] = SERIALS;
+
+	// Setup the output file
+	outfile = initPaths();
+
+	// Monitor for alarms
+	signal(SIGALRM, timeout);
+
+	// Setup the USB devices
+	for (i = 0; i < NUM_DEVICES; i++) {
+		device[i] = init(serials[i]);
+	}
+	
+	// Read forever
+	while (1) {
+		// Check for motion in any device
+		motion = 0;
+		for (i = 0; i < NUM_DEVICES; i++) {
+			if (readDev(device[i])) {
+				motion = 1;
+				i = NUM_DEVICES;
+			}
+		}
+
+		// If we detected motion
+		if (motion > 0) {
+			#ifdef DEBUG
+			printf("Motion detected\n");
+			#endif
+			if (utimes(outfile, NULL) != 0) {
+				if (errno == ENOENT) {
+				} else {
+					fprintf(stderr, "Error touching output file (%s): %s\n", strerror(errno), outfile);
+				}
+			}
+		} else {
+			#ifdef DEBUG
+			printf("\n");
+			#endif
+		}	
+
+		// Delay and loop
+		usleep(SLEEP_DELAY);
+	}
+	
+	// Cleanup (we never get here)
+	USBmX_Destroy(ctx);
+	return 0;
+}
+
+// Catch the timeout
+void timeout(int sig) {
+	signal(sig, SIG_IGN);
+	fprintf(stderr, "Timeout waiting for USB read\n");
+	exit(1);
+}
+
+// Setup the USBMicro context and build refs to our specified devices
+USBmX_DeviceRef init(const char *serial) {
+	USBmX_DeviceRef device;
+
+	// Create a new USBmicroX context as needed
+	if (!ctx) {
+		ctx = USBmX_Create();
+		USBmX_Begin(ctx);
+	}
+
+	// Find the specified device
+	device = USBmX_DeviceWithSerial(ctx, serial);
+	if (!device) {
+		fprintf(stderr, "Device not found: %s\n", serial);
+		exit(1);
+	}
+
+	// Return device handle
+	return device;
+}
+
+// Read the specified USBMicro device
+u8 readDev(USBmX_DeviceRef dev) {
+	unsigned char data = 0x00;
+
+	// Enable the timeout
+	alarm(TIMEOUT);
+
+	// Read from the device
+	if (USBmX_ReadA(dev, &data) != 0) {
+		fprintf(stderr, "Device read error\n");
+		exit(1);
+	}
+
+	// Clear the timeout
+	alarm(0);
+
+	// Return pin A5
+	return (data & 0x20);
+}
+
+// Setup the data directory and output file
+char * initPaths() {
+	int fd;
+	size_t len;
+	char *datadir, *outfile;
+	struct stat statbuf;
+
+	// Construct the data directory and output file paths
+	len = confstr(_CS_DARWIN_USER_TEMP_DIR, NULL, (size_t) 0);
+	len += sizeof(DATA_DIR);
+	datadir = malloc(len);
+	outfile = malloc(len + sizeof(OUT_FILE));
+	if (datadir == NULL || outfile == NULL) {
+		fprintf(stderr, "Out of memory\n");
+		exit(1);
+	}
+	confstr(_CS_DARWIN_USER_TEMP_DIR, datadir, len);
+	strlcat(datadir, DATA_DIR, len);
+	strlcpy(outfile, datadir, len);
+	strlcat(outfile, OUT_FILE, len + sizeof(OUT_FILE));
+
+	// Create the datadir as needed
+	stat(datadir, &statbuf);
+	if (!S_ISDIR(statbuf.st_mode)) {
+		fprintf(stderr, "Creating data directory: %s\n", datadir);
+		if (mkdir(datadir, S_IRUSR | S_IWUSR | S_IXUSR) != 0) {
+			fprintf(stderr, "Error creating data directory (%s): %s\n", strerror(errno), outfile);
+			exit(1);
+		}
+	}
+
+	// Create the output file as needed
+	stat(outfile, &statbuf);
+	if(!S_ISREG(statbuf.st_mode)) {
+		fprintf(stderr, "Creating output file: %s\n", outfile);
+		fd = open(outfile, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
+		if (fd <= 0 || close(fd) != 0) {
+			fprintf(stderr, "Error creating output file (%s): %s\n", strerror(errno), outfile);
+			exit(1);
+		}
+	}
+
+	// Return the output file path
+	return outfile;
+}

+ 13 - 0
motion/makefile

@@ -0,0 +1,13 @@
+default: USBMicroX
+
+USBMicroX: USBMicroX.o main.o
+	gcc -framework CoreFoundation -framework IOKit -o USBMicroX USBMicroX.o main.o
+
+main.o: main.c
+	gcc -c main.c
+
+USBMicroX.o: USBMicroX.c USBMicroX.h
+	gcc -c USBMicroX.c
+
+clean:
+	rm -f main.o USBMicroX.o USBMicroX

+ 56 - 0
plexCmd/plex

@@ -0,0 +1,56 @@
+#!/bin/bash
+
+# Parameters
+USER="plex"
+PASS="bobola22"
+HOST="localhost"
+PORT="3000"
+
+# Figure out what we should do
+CMD="`basename "${0}" | sed 's%^plex\-%%' | tr a-z A-Z`"
+
+# Reduce typing
+CURL="curl --silent --get --user ${USER}:${PASS}"
+RPATH="/xbmcCmds/xbmcHttp"
+URL="http://${HOST}:${PORT}${RPATH}"
+OUTPUT=0
+
+# Pick a command
+if [ "${CMD}" == "UPDATE" ]; then
+	URL="${URL}/?command=ExecBuiltIn&parameter=XBMC.updatelibrary(video)"
+elif [ "${CMD}" == "CLEAN" ]; then
+	URL="${URL}?command=ExecBuiltIn&parameter=XBMC.CleanLibrary(video)"
+elif [ "${CMD}" == "FILES" ]; then
+	URL="${URL}?command=queryvideodatabase(SELECT%20path.strPath,%20files.strFilename%20FROM%20path%20JOIN%20files%20ON%20path.idPath%20=%20files.idPath%20JOIN%20episode%20ON%20files.idFile%20=%20episode.idFile)"
+	OUTPUT=1
+elif [ "${CMD}" == "BUTTON" ]; then
+	URL="${URL}?command=SendKey(${1})"
+elif [ "${CMD}" == "PLAYING" ]; then
+	URL="${URL}?command=GetCurrentlyPlaying"
+	OUTPUT=1
+elif [ "${CMD}" == "RESOLUTION" ]; then
+	URL="${URL}?command=GetGuiDescription"
+	OUTPUT=1
+elif [ "${CMD}" == "GUI" ]; then
+	URL="${URL}?command=GetGuiStatus"
+	OUTPUT=1
+else
+	echo "Unknown command: ${CMD}" 1>&2
+	exit 1
+fi
+
+# Bail if Plex is not running
+if ! ps awx -o comm= | grep -v 'Media Server' | grep -q 'MacOS/Plex'; then
+	if [ $OUTPUT -gt 0 ]; then
+		echo "Plex is not running" 1>&2
+		exit 1
+	fi
+	exit 0
+fi
+
+# Run the command
+if [ $OUTPUT -gt 0 ]; then
+	$CURL "${URL}"
+else
+	$CURL "${URL}" >/dev/null
+fi

+ 1 - 0
plexCmd/plex-button

@@ -0,0 +1 @@
+plex

+ 23 - 0
plexCmd/plex-checkTV

@@ -0,0 +1,23 @@
+#!/bin/bash
+
+# Path must be the default /Volumes and not /mnt -- Plex stores the former in its DB
+TV_PATH="/Volumes/media/TV"
+
+# Get the list of TV files in the library
+LIB_TEMP="`mktemp -t 'checkTV.XXXXXXXX'`"
+~/bin/video/plex-files | ~/bin/video/plex-filter | grep "^${TV_PATH}/" | sort -f > "${LIB_TEMP}"
+
+# Bail if we did not find anything
+if [ `cat "${LIB_TEMP}" | wc -l` -lt 1 ]; then
+	exit 1
+fi
+
+# Get the list of all files on the disk
+DISK_TEMP="`mktemp -t 'checkTV.XXXXXXXX'`"
+find "${TV_PATH}" -type f -name '*.m[op4k][4v]' | grep -v '\/\._' | sort -f > "${DISK_TEMP}"
+
+# Compare
+diff -i -U 0 "${DISK_TEMP}" "${LIB_TEMP}" | grep -v '^---' | grep -v '^+++' | grep -v '@@' | grep -v '^+'
+
+# Cleanup
+rm -rf "${LIB_TEMP}" "${DISK_TEMP}"

+ 1 - 0
plexCmd/plex-clean

@@ -0,0 +1 @@
+plex

+ 1 - 0
plexCmd/plex-files

@@ -0,0 +1 @@
+plex

+ 18 - 0
plexCmd/plex-filter

@@ -0,0 +1,18 @@
+#!/usr/bin/perl -w
+use strict;
+
+my $prefix = '';
+while (<STDIN>) {
+	foreach my $field (split(/\>/, $_)) {
+		$field =~ s/\>/\>\n/g;
+		$field =~ s/\<\/?\w+$//g;
+		if ($field =~ /^\s*$/) {
+			next;
+		}
+		if ($field =~ /^\//) {
+			$prefix = $field;
+			next;
+		}
+		print $prefix . $field . "\n";
+	}
+}

+ 1 - 0
plexCmd/plex-gui

@@ -0,0 +1 @@
+plex

+ 1 - 0
plexCmd/plex-playing

@@ -0,0 +1 @@
+plex

+ 1 - 0
plexCmd/plex-resolution

@@ -0,0 +1 @@
+plex

+ 1 - 0
plexCmd/plex-update

@@ -0,0 +1 @@
+plex

+ 27 - 0
plexMonitor/launchd/com.uberzach.plexMonitor.gui.plist

@@ -0,0 +1,27 @@
+<?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>beddy.uberzach.com</string>
+		<string>heady.uberzach.com</string>
+	</array>
+	<key>RunAtLoad</key>
+	<true/>
+	<key>Label</key>
+	<string>com.uberzach.plexMonitor.gui</string>
+	<key>KeepAlive</key>
+	<dict>
+		<key>NetworkState</key>
+		<true/>
+	</dict>
+	<key>ProgramArguments</key>
+	<array>
+		<string>/Users/tv/bin/video/plexMonitor/plexMonitor-gui</string>
+		<string>15</string>
+	</array>
+</dict>
+</plist>

+ 27 - 0
plexMonitor/launchd/com.uberzach.plexMonitor.playing.plist

@@ -0,0 +1,27 @@
+<?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>beddy.uberzach.com</string>
+		<string>heady.uberzach.com</string>
+	</array>
+	<key>RunAtLoad</key>
+	<true/>
+	<key>Label</key>
+	<string>com.uberzach.plexMonitor.playing</string>
+	<key>KeepAlive</key>
+	<dict>
+		<key>NetworkState</key>
+		<true/>
+	</dict>
+	<key>ProgramArguments</key>
+	<array>
+		<string>/Users/tv/bin/video/plexMonitor/plexMonitor-playing</string>
+		<string>0.25</string>
+	</array>
+</dict>
+</plist>

+ 1 - 0
plexMonitor/plexMonitor-gui

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

+ 1 - 0
plexMonitor/plexMonitor-playing

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

+ 104 - 0
plexMonitor/plexMonitor.pl

@@ -0,0 +1,104 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use LWP::Simple;
+use File::Basename;
+use Time::HiRes qw( usleep );
+use File::Temp qw( tempfile );
+
+# Prototypes
+sub xbmcHTTP($);
+
+# App config
+my $TEMP_DIR = `getconf DARWIN_USER_TEMP_DIR`;
+chomp($TEMP_DIR);
+my $DATA_DIR = $TEMP_DIR . '/plexMonitor';
+
+# xbmcHTTP commands
+my %CMDS = (
+	'PLAYING' => 'GetCurrentlyPlaying',
+	'GUI'     => 'GetGuiStatus'
+);
+
+# Debug
+my $DEBUG = 0;
+if ($ENV{'DEBUG'}) {
+	$DEBUG = 1;
+}
+
+# Command-line arguments
+my ($DELAY) = @ARGV;
+if (!$DELAY) {
+	$DELAY = 0;
+}
+$DELAY *= 1000000;    # Microseconds;
+
+# Sanity check
+if (!-d $TEMP_DIR) {
+	die("Bad config\n");
+}
+
+# Mode
+my $MODE = 'PLAYING';
+if (basename($0) =~ /GUI/i) {
+	$MODE = 'GUI';
+}
+
+# Add the data directory as needed
+if (!-d $DATA_DIR) {
+	mkdir($DATA_DIR);
+}
+
+# Loop forever (unless no delay is set)
+my $data     = '';
+my $dataLast = '';
+do {
+
+	# Run the monitor command
+	my $changed = 0;
+	$dataLast = $data;
+	$data     = xbmcHTTP($CMDS{$MODE});
+
+	# Compare this data set to the last
+	if ($data ne $dataLast) {
+		$changed = 1;
+		if ($DEBUG) {
+			print STDERR "Change detected in data:\n" . $data . "\n";
+		}
+	}
+
+	# If anything changed, save the data to disk
+	if ($changed) {
+		my ($fh, $tmp) = tempfile($DATA_DIR . '/' . $MODE . '.XXXXXXXX', 'UNLINK' => 0);
+		print $fh $data;
+		close($fh);
+		rename($tmp, $DATA_DIR . '/' . $MODE);
+	}
+
+	# Special handling for PLAYING mode -- it's a bit of a hack, but it saves a lot of code other places
+	if ($changed && $MODE eq 'PLAYING') {
+		my $playing = 0;
+		if ($data =~ /PlayStatus\:Playing/) {
+			$playing = 1;
+		}
+		my ($fh, $tmp) = tempfile($DATA_DIR . '/PLAY_STATUS.XXXXXXXX', 'UNLINK' => 0);
+		print $fh $playing . "\n";
+		close($fh);
+		rename($tmp, $DATA_DIR . '/PLAY_STATUS');
+	}
+
+	# Delay and loop
+	usleep($DELAY);
+} until ($DELAY == 0);
+
+# Exit cleanly
+exit(0);
+
+sub xbmcHTTP($) {
+	my ($cmd) = @_;
+	my $data = get('http://localhost:3000/xbmcCmds/xbmcHttp?command=' . $cmd);
+	if (!defined($data)) {
+		$data = '';
+	}
+	return $data;
+}

+ 29 - 0
projector/launchd/com.uberzach.projector.plist

@@ -0,0 +1,29 @@
+<?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.projector</string>
+	<key>KeepAlive</key>
+	<dict>
+		<key>PathState</key>
+		<dict>
+			<key>/dev/tty.Projector-DevB</key>
+			<true/>
+		</dict>
+	</dict>
+	<key>ProgramArguments</key>
+	<array>
+		<string>/Users/tv/bin/video/projector/projector.pl</string>
+		<string>15</string>
+	</array>
+</dict>
+</plist>

+ 28 - 0
projector/launchd/com.uberzach.projector.power.plist

@@ -0,0 +1,28 @@
+<?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.projector.power</string>
+	<key>KeepAlive</key>
+	<dict>
+		<key>OtherJobEnabled</key>
+		<dict>
+			<key>com.uberzach.projector</key>
+			<true/>
+		</dict>
+	</dict>
+	<key>ProgramArguments</key>
+	<array>
+		<string>/Users/tv/bin/video/projector/projectorPower.pl</string>
+	</array>
+</dict>
+</plist>

+ 1 - 0
projector/projector.pl

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

+ 153 - 0
projector/projectorPower.pl

@@ -0,0 +1,153 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use IO::Socket::UNIX;
+use POSIX qw(ceil floor);
+
+# Prototypes
+sub mtime($);
+
+# Config
+my $TIMEOUT   = 900;
+my $COUNTDOWN = 300;
+
+# App config
+my $DELAY       = 60;
+my $CMD_TIMEOUT = 5;
+my $TEMP_DIR    = `getconf DARWIN_USER_TEMP_DIR`;
+chomp($TEMP_DIR);
+my $DATA_DIR = $TEMP_DIR . 'plexMonitor/';
+my $CMD_FILE = $DATA_DIR . 'PROJECTOR.socket';
+
+# Debug
+my $DEBUG = 0;
+if ($ENV{'DEBUG'}) {
+	$DEBUG = 1;
+}
+
+# Sanity check
+if (!-d $DATA_DIR || !-S $CMD_FILE) {
+	die("Bad config\n");
+}
+
+# Socket init
+my $sock = IO::Socket::UNIX->new(
+	'Peer'    => $CMD_FILE,
+	'Type'    => SOCK_DGRAM,
+	'Timeout' => $CMD_TIMEOUT
+) or die('Unable to open socket: ' . $CMD_FILE . ": ${@}\n");
+
+# State
+my $state      = 'INIT';
+my $stateLast  = $state;
+my $updateLast = time();
+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 and changes
+	{
+		$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";
+		}
+
+		my $mtime = mtime($DATA_DIR . 'PROJECTOR');
+		if ($mtime > $updateLast) {
+			$updateLast = $mtime;
+		}
+	}
+
+	# 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 = 'COUNTDOWN';
+		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
+			$sock->send($state)
+			  or die('Unable to write command to socket: ' . $CMD_FILE . ': ' . $state . ": ${!}\n");
+		} 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);
+}
+
+# Cleanup
+$sock->close();
+undef($sock);
+exit(0);
+
+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;
+}

+ 42 - 0
serial/cmd.pl

@@ -0,0 +1,42 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use IO::Socket::UNIX;
+use POSIX qw(ceil floor);
+
+# App config
+my $TIMEOUT  = 5;
+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;
+}
+
+# Command-line parameters
+my ($DEV, $CMD) = @ARGV;
+my $CMD_FILE = $DATA_DIR . uc($DEV) . '.socket';
+
+# Sanity check
+if (!-d $DATA_DIR || !-S $CMD_FILE) {
+	die("Bad config\n");
+}
+
+# Socket init
+my $sock = IO::Socket::UNIX->new(
+	'Peer'    => $CMD_FILE,
+	'Type'    => SOCK_DGRAM,
+	'Timeout' => $TIMEOUT
+) or die('Unable to open socket: ' . $CMD_FILE . ": ${@}\n");
+
+# Send the command
+$sock->send($CMD)
+  or die('Unable to write command to socket: ' . $CMD_FILE . ': ' . $CMD . ": ${!}\n");
+
+# Cleanup
+$sock->close();
+undef($sock);
+exit(0);

+ 299 - 0
serial/serial.pl

@@ -0,0 +1,299 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use File::Basename;
+use File::Temp qw( tempfile );
+use IO::Select;
+use IO::Socket::UNIX;
+use Device::SerialPort;
+
+# 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.' . $DEV . '-DevB';
+	$CRLF      = "\r\n";
+	$DELIMITER = ':';
+	%CMDS      = (
+		'INIT'   => '',
+		'ON'     => 'PWR ON',
+		'OFF'    => 'PWR OFF',
+		'STATUS' => 'PWR?'
+	);
+	$STATUS_ON = 'PWR=01';
+} elsif (basename($0) =~ /AMPLIFIER/i) {
+	$DEV       = 'Amplifier';
+	$PORT      = '/dev/tty.' . $DEV . '-DevB';
+	$CRLF      = "\r";
+	$DELIMITER = "\r";
+	%CMDS      = (
+		'INIT'     => '',
+		'ON'       => 'PWON',
+		'OFF'      => 'PWSTANDBY',
+		'STATUS'   => 'PW?',
+		'VOL+'     => 'MVUP',
+		'VOL-'     => 'MVDOWN',
+		'MUTE'     => 'MUON',
+		'UNMUTE'   => 'MUOFF',
+		'TV'       => 'SITV',
+		'DVD'      => 'SIDVD',
+		'SURROUND' => 'MSDOLBY DIGITAL',
+		'STEREO'   => 'MS7CH STEREO',
+
+	);
+	$STATUS_ON = 'PWON';
+} elsif (basename($0) =~ /TV/i) {
+	$DEV       = 'TV';
+	$PORT      = '/dev/tty.' . $DEV . '-DevB';
+	$CRLF      = "\r";
+	$DELIMITER = "\r";
+	%CMDS      = (
+		'INIT'      => 'RSPW1',
+		'ON'        => 'POWR1',
+		'OFF'       => 'POWR0',
+		'STATUS'    => 'POWR?',
+		'VOL+'      => 'MVUP',
+		'VOL-'      => 'MVDOWN',
+		'MUTE'      => 'MUTE1',
+		'UNMUTE'    => 'MUTE2',
+		'TV'        => 'IAVD0',
+		'PLEX'      => 'IAVD7',
+		'VOL_CHECK' => 'VOLM?',
+		'VOL6'      => 'VOLM6',
+		'VOL12'     => 'VOLM12',
+		'VOL24'     => 'VOLM24',
+		'VOL+'      => 'VOLM',
+		'VOL-'      => 'VOLM'
+	);
+	$STATUS_ON = '1';
+} else {
+	die("No device specified\n");
+}
+
+# App config
+my $DELAY_STATUS    = 5;
+my $BYTE_TIMEOUT    = 50;
+my $SILENCE_TIMEOUT = $BYTE_TIMEOUT * 10;
+my $MAX_CMD_LEN     = 1024;
+my $BT_CHECK        = $ENV{'HOME'} . '/bin/btcheck';
+my $TEMP_DIR        = `getconf DARWIN_USER_TEMP_DIR`;
+chomp($TEMP_DIR);
+my $DATA_DIR = $TEMP_DIR . 'plexMonitor/';
+my $CMD_FILE = $DATA_DIR . uc($DEV) . '.socket';
+
+# 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");
+}
+
+# Wait for the serial port to become available
+system($BT_CHECK, $DEV);
+if ($? != 0) {
+	sleep($DELAY_STATUS);
+	die('Bluetooth device "' . $DEV . "\" not available\n");
+}
+
+# Socket init
+if (-e $CMD_FILE) {
+	unlink($CMD_FILE);
+}
+my $sock = IO::Socket::UNIX->new(
+	'Local' => $CMD_FILE,
+	'Type'  => SOCK_DGRAM
+) or die('Unable to open socket: ' . $CMD_FILE . ": ${@}\n");
+if (!-S $CMD_FILE) {
+	die('Failed to create socket: ' . $CMD_FILE . "\n");
+}
+my $select = IO::Select->new($sock)
+  or die('Unable to select socket: ' . $CMD_FILE . ": ${!}\n");
+
+# Port init
+my $port = new Device::SerialPort($PORT)
+  or die('Unable to open serial connection: ' . $PORT . ": ${!}\n");
+$port->read_const_time($BYTE_TIMEOUT);
+
+# Init (clear any previous state)
+sendQuery($port, $CMDS{'INIT'});
+
+# Track the power state
+# Check for new commands
+my $power      = -1;
+my $powerLast  = $power;
+my $lastStatus = 0;
+while (1) {
+
+	# Calculate our next timeout
+	# Hold on select() but not more than $DELAY_STATUS after our last update
+	# Plus 1 because we aren't using hi-res time
+	my $timeout = ($lastStatus + $DELAY_STATUS + 1) - time();
+	if ($timeout < 1) {
+		$timeout = 0;
+	}
+	if ($DEBUG) {
+		print STDERR 'Waiting for commands with timeout: ' . $timeout . "\n";
+	}
+
+	# Check for queued commands
+	my @ready_clients = $select->can_read($timeout);
+	foreach my $fh (@ready_clients) {
+
+		# Grab the inbound text
+		my $text = undef();
+		$fh->recv($text, $MAX_CMD_LEN);
+		$text =~ s/^\s+//;
+		$text =~ s/\s+$//;
+		if ($DEBUG) {
+			print STDERR 'Got command: ' . $text . "\n";
+		}
+
+		# Only accept valid commands
+		my $cmd = undef();
+		foreach my $name (keys(%CMDS)) {
+			if ($name eq $text) {
+				$cmd = $name;
+				last;
+			}
+		}
+
+		# Send command to serial device
+		if ($cmd) {
+			if ($DEBUG) {
+				print STDERR 'Sending command: ' . $cmd . "\n";
+			}
+			my $result = sendQuery($port, $CMDS{$cmd});
+			if ($DEBUG && $result) {
+				print STDERR "\tGot result: " . $result . "\n";
+			}
+		}
+	}
+
+	# Check the power state, but not too frequently
+	if (time() > $lastStatus + $DELAY_STATUS) {
+		$lastStatus = time();
+		$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 ' . uc($DEV) . ' power state: ' . $power . "\n";
+			}
+			my ($fh, $tmp) = tempfile($DATA_DIR . uc($DEV) . '.XXXXXXXX', 'UNLINK' => 0);
+			print $fh $power . "\n";
+			close($fh);
+			rename($tmp, $DATA_DIR . uc($DEV));
+		}
+	}
+}
+
+# Cleanup
+undef($select);
+close($sock);
+undef($sock);
+$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();
+	}
+
+	# Translate CRLF and CR to LF
+	$string =~ s/\r\n/\n/g;
+	$string =~ s/\r/\n/g;
+
+	# Strip the trailing delimiter
+	$string =~ s/${char}$//;
+
+	# Strip leading or trailing whitespace
+	$string =~ s/^\s+//;
+	$string =~ s/\s+$//;
+
+	# Return our clean string
+	if ($DEBUG) {
+		print STDERR 'Read: ' . $string . "\n";
+	}
+	return $string;
+}