| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- #!/usr/bin/perl
- use strict;
- use warnings;
- use File::Temp;
- use IO::Select;
- use IO::Socket::UNIX;
- # Package name
- package DMX;
- # Debug
- my $DEBUG = 0;
- if ($ENV{'DEBUG'}) {
- $DEBUG = 1;
- }
- # Config
- my $SOCK_TIMEOUT = 5;
- my $MAX_CMD_LEN = 1024;
- my $TEMP_DIR = `getconf DARWIN_USER_TEMP_DIR`;
- chomp($TEMP_DIR);
- my $DATA_DIR = $TEMP_DIR . 'plexMonitor/';
- my $DMX_SOCK = $DATA_DIR . 'DMX.socket';
- my $SUB_SOCK = $DATA_DIR . 'STATE.socket';
- my %CHANNEL_ADJ = (
- '13' => 1.00,
- '14' => 1.14,
- '15' => 1.19,
- );
- # Sanity check
- if (!-d $DATA_DIR) {
- die('Data directory not available: ' . $DATA_DIR . "\n");
- }
- # State
- my $DMX_FH = undef();
- my $SELECT = undef();
- # Data directory
- sub dataDir() {
- return $DATA_DIR;
- }
- # Maximum command length
- sub maxCmdLen() {
- return $MAX_CMD_LEN;
- }
- # Generic client socket
- sub clientSock($) {
- my ($path) = @_;
- my $sock = IO::Socket::UNIX->new(
- 'Peer' => $path,
- 'Type' => IO::Socket::UNIX::SOCK_DGRAM,
- 'Timeout' => $SOCK_TIMEOUT,
- ) or die('Unable to open client socket: ' . $path . ": ${@}\n");
- return $sock;
- }
- # Generic server socket
- sub serverSock($) {
- my ($path) = @_;
- if (-e $path) {
- unlink($path);
- }
- my $sock = IO::Socket::UNIX->new(
- 'Local' => $path,
- 'Type' => IO::Socket::UNIX::SOCK_DGRAM,
- ) or die('Unable to open server socket: ' . $path . ": ${@}\n");
- return $sock;
- }
- # Select on a server socket
- sub selectSock($) {
- my ($path) = @_;
- my $sock = serverSock($path);
- my $sel = IO::Select->new($sock)
- or die('Unable to select socket: ' . $path . ": ${!}\n");
- return $sel;
- }
- # DMX socket init
- sub dmxSock() {
- if (!defined($DMX_FH)) {
- $DMX_FH = clientSock($DMX_SOCK);
- }
- return $DMX_FH;
- }
- # Subscribe to state updates
- sub stateSubscribe($) {
- my ($STATE_SOCK) = @_;
- my $sub_fh = clientSock($SUB_SOCK);
- $sub_fh->send($STATE_SOCK)
- or die('Unable to subscribe: ' . $! . "\n");
- shutdown($sub_fh, 2);
- undef($sub_fh);
- }
- # Alias for selectSock() that also sets the global $SELECT pointer
- sub stateSocket($) {
- my ($STATE_SOCK) = @_;
- $SELECT = selectSock($STATE_SOCK);
- return $SELECT;
- }
- # Parse the state->client comm string
- sub parseState($$$) {
- my ($text, $exists, $mtime) = @_;
- my $cmdState = undef();
- my $exists_text = undef();
- if (!defined($exists)) {
- my %tmp = ();
- $exists = \%tmp;
- }
- if (!defined($mtime)) {
- my %tmp = ();
- $mtime = \%tmp;
- }
- # Parse the string
- %{$exists} = ();
- %{$mtime} = ();
- ($cmdState, $exists_text) = $text =~ /^(\w+)(?:\s+\(([^\)]+)\))?/;
- if (!defined($cmdState)) {
- print STDERR 'State parse error: ' . $text . "\n";
- next;
- }
- if (defined($exists_text)) {
- foreach my $exists_val (split(/\s*,\s*/, $exists_text)) {
- my ($name, $value, $time) = $exists_val =~ /(\w+)\:([\w\.\- ]*)\:(\d*)/;
- if (!defined($name) || !defined($value) || !defined($time)) {
- print STDERR 'State parse error (exists): ' . $text . "\n";
- next;
- }
- $exists->{$name} = $value ? $value : 0;
- $mtime->{$name} = $time ? $time : 0;
- }
- }
- if ($DEBUG) {
- my @exists_tmp = ();
- foreach my $key (keys(%{$exists})) {
- push(@exists_tmp, $key . ':' . $exists->{$key} . ':' . $mtime->{$key});
- }
- print STDERR 'Got state: ' . $cmdState . ' (' . join(', ', @exists_tmp) . ")\n";
- }
- # Translate INIT to OFF
- if ($cmdState eq 'INIT') {
- $cmdState = 'OFF';
- }
- # Return
- return $cmdState;
- }
- sub dim($) {
- my ($args) = @_;
- if (!defined($args->{'delay'})) {
- $args->{'delay'} = 0;
- }
- if (!defined($args->{'channel'}) || !defined($args->{'time'}) || !defined($args->{'value'})) {
- die('Invalid command for socket: ' . join(', ', keys(%{$args})) . ': ' . join(', ', values(%{$args})) . "\n");
- }
- # Silently skip invalid channels
- if ($args->{'channel'} < 0) {
- return;
- }
- # Adjust for the color curve (for channels where we have such data)
- my $value = $args->{'value'};
- if ($CHANNEL_ADJ{ $args->{'channel'} }) {
- $value *= $CHANNEL_ADJ{ $args->{'channel'} };
- }
- # 8-bit values
- $value = int($value);
- if ($value > 255) {
- $value = 255;
- } elsif ($value < 0) {
- $value = 0;
- }
- # We only deal in ints
- my $time = int($args->{'time'});
- my $delay = int($args->{'delay'});
- my $cmd = join(':', $args->{'channel'}, $time, $value, $delay);
- dmxSock();
- $DMX_FH->send($cmd)
- or die('Unable to write command to DMX socket: ' . $cmd . ": ${!}\n");
- }
- sub printDataset($) {
- my ($data_set) = @_;
- my $sum = 0;
- foreach my $data (@{$data_set}) {
- $sum += $data->{'value'};
- my $str = $data->{'channel'} . ' => ' . int($data->{'value'}) . ' @ ' . $data->{'time'};
- if ($data->{'delay'}) {
- $str .= ' (Delay: ' . $data->{'delay'} . ')';
- }
- print STDERR "\t" . $str . "\n";
- }
- print STDERR "\tTotal: " . int($sum) . "\n";
- }
- sub applyDataset($$$) {
- my ($data_set, $state, $file) = @_;
- if (!defined($state)) {
- $state = 'NULL';
- }
- # Debug
- if ($DEBUG) {
- print STDERR 'State: ' . $state . "\n";
- printDataset($data_set);
- }
- # Send the dim command
- my @values = ();
- foreach my $data (@{$data_set}) {
- dim($data);
- my $str = $data->{'channel'} . ' => ' . $data->{'value'} . ' @ ' . $data->{'time'};
- if ($data->{'delay'}) {
- $str .= ' (Delay: ' . $data->{'delay'} . ')';
- }
- push(@values, $str);
- }
- # Save the state and value to disk
- if (length($file)) {
- my ($fh, $tmp) = File::Temp::tempfile($file . '.XXXXXXXX', 'UNLINK' => 0);
- print $fh 'State: ' . $state . "\n" . join("\n", @values) . "\n";
- close($fh);
- rename($tmp, $file);
- }
- }
- sub readState($$$$) {
- my ($delay, $exists, $mtime, $valid) = @_;
- my $cmdState = undef();
- # Wait for state updates
- my @ready_clients = $SELECT->can_read($delay);
- foreach my $fh (@ready_clients) {
- # Ensure we won't block on recv()
- $fh->blocking(0);
- # Grab the inbound text
- while (defined($fh->recv(my $text, $MAX_CMD_LEN))) {
- # Parse the string
- my $state = parseState($text, $exists, $mtime);
- # Ignore invalid states
- if (defined($valid)) {
- if (!defined($valid->{$state})) {
- if ($DEBUG) {
- print STDERR 'Invalid state: ' . $state . "\n";
- }
- next;
- }
- }
- # Propogate valid states
- if ($state) {
- $cmdState = $state;
- }
- }
- }
- # Return the final valid state
- return $cmdState;
- }
- # Always return true
- 1;
|