#!/usr/bin/perl -w ## # Read a 'USB thermal station' from www.d3p.pl. # # This is a USB interface with 8 ports. It responds to commands # through an ACM driver, which presents as a serial device through # /dev/ttyACM0 (or possibly higher numbers if you have multiple). # It requires the Kernel driver 'cdc_acm' to be present on Linux. # # Commands accepted by the device are: # # - report the temperature for sensor in degrees C # V # - report the device version # We only use the sensor commands. # # Readings are provided quickly, although I've used a 2 second # delay here, just to be sure that I get back the data. # # Output depends on the type requested. # # * MRTG output # Use -mrtg . is 1 or 2 sensors in a space, # comma, or / separated list. MRTG only supports up to 2 readings, # so this cannot be any more than this. # # * Munin output # Use -munin OR run with munin-run (ie as the normal munin execution). # Configuration may be supplied in the munin configuration file for # the plugin: # DEVICE Device name # SPEED Device speed # DELAY Delay in reads # SENSORS Number of Sensors to return # SENSORNAME_ # Supplies the name of the sensor # TITLE Title of the graph (default 'Temperatures') # CATEGORY Category for the graph (default 'Sensors') # Munin fetch type may follow, eg: # config Read the configuration # autoconf Detect whether we can run # fetch Read values # When used from within munin, the user 'munin' must have permission # to access the device - you may need to use 'user root' in the # configuration. # Example configuration: # [d3ptemperatures] # user root # env.TITLE Temperatures near the server # env.SENSORNAME_1 Top of server # env.SENSORNAME_2 Back of server # env.SENSORNAME_3 Bottom of door # env.SENSORNAME_4 Top of door # env.SENSORS 8 # env.DEVICE /dev/ttyACM0 # env.DELAY 3 # Note that overriding the number of sensors with SENSORS will use # generic names 'Sensor #'. # # * Readable output # No additional switches; the output will be readable in the format: # Sensor : # # Munin configuration details: # #%# family=auto #%# capabilities=autoconf # use strict; use Device::SerialPort; use Getopt::Long; use Data::Dumper; my $device = "/dev/ttyACM0"; my $speed = 115200; my $nsensors = 8; my $help = 0; my $delay = 2; my $mrtg = undef; my $munin = undef; GetOptions('device=s' => \$device, 'speed=i' => \$speed, 'sensors=i' => \$nsensors, 'delay=i' => \$delay, 'mrtg=s' => \$mrtg, 'munin' => \$munin, 'help' => \$help); if ($help) { die < Options: -device Device to read (default $device) -speed Speed to read at (default $speed) -sensors Number of sensors to read (default $nsensors) -delay Maximum time to wait (default $delay) -mrtg MRTG results, for 1 or 2 sensors (give a list of the sensor numbers) -munin Munin results EOM } my @sensors = (0..$nsensors-1); # Sensor names (used for Munin) my %sensorname = (); # Override the settings if we're using Munin if (defined $ENV{'MUNIN_VERSION'} || $munin) { $munin = 1; # They can specify the sensor names in environment variables # and if they do, only those sensors will be reported. my $sawsensors = 0; for my $n (0..7) { if (defined $ENV{"SENSORNAME_$n"}) { @sensors = () if ($sawsensors == 0); push @sensors, $n; $sensorname{$n} = $ENV{"SENSORNAME_$n"}; } } # Or they can specify a number of sensors to read if (defined $ENV{'SENSORS'}) { @sensors = (0..$ENV{'SENSORS'} - 1); } # Overrides for the main settings $device = $ENV{'DEVICE'} if (defined $ENV{'DEVICE'}); $speed = $ENV{'SPEED'} if (defined $ENV{'SPEED'}); $delay = $ENV{'DELAY'} if (defined $ENV{'DELAY'}); # Munin operation type my $op = shift @ARGV || 'fetch'; if ($op eq 'autoconf') { if (!-w $device) { print "no (cannot access $device)\n"; } else { print "yes\n"; } exit 0; } if ($op eq 'config') { my $title = $ENV{'TITLE'} || 'Temperatures'; my $category = $ENV{'CATEGORY'} || 'Sensors'; print "graph_title $title\n"; print "graph_category $category\n"; print "graph_vlabel Temperature\n"; for my $n (@sensors) { my $name = $sensorname{$n} || "Sensor $n"; print "t$n.label $name\n"; print "t$n.info Temperature at $name in Celcius\n"; } exit 0; } if ($op ne 'fetch') { print STDERR "Do not understand '$op'\n"; exit 1; } } if (defined $mrtg) { # Check the MRTG format if they gave one. # MRTG output only 2 results, so you have to specify which sensors # you are interested in. @sensors = map { int($_) } split /[,\/ ]/, $mrtg; if (@sensors > 2) { die "-mrtg cannot have more than 2 sensors"; } } my $serial = new Device::SerialPort($device, 1) || die "Can't open $device: $!\n"; # Set the port configuration $serial->baudrate($speed); $serial->parity('none'); $serial->databits('8'); $serial->handshake('none'); # Apply the configuration $serial->write_settings(); # Set up timeouts for reading data $serial->read_char_time(0); # don't wait for each character $serial->read_const_time(100); # .1 second per unfulfilled "read" call # Write our request to the port my $command = join "", map { "$_\r" } @sensors; my $wrote = $serial->write($command); if ($wrote != length($command)) { die "Failed to write commands (only wrote $wrote bytes)\n"; } # Our temperature readings my %reading; # Example code taken from Device::SerialPort my $chars=0; my $buffer=""; while ($delay > 0) { my ($count,$saw)=$serial->read(255); # will read _up to_ 255 chars if ($count > 0) { $chars+=$count; $buffer.=$saw; # Strip any leading newlines while (1) { $buffer =~ s/^[\r\n]+//; if ($buffer =~ s/^id(\d+) (-?\d+)[\r\n]+//) { # We obtained a temperature reading $reading{$1} = $2; next; } last; } # Check here to see if what we want is in the $buffer # say "last" if we find it if (scalar(keys %reading) == scalar(@sensors)) { # We have all our readings last; } } else { $delay -= 0.1; } } if (defined $mrtg) { if (defined $sensors[0] && defined $reading{$sensors[0]}) { print "$reading{$sensors[0]}\n"; } else { print "UNDEFINED\n"; } if (defined $sensors[1] && defined $reading{$sensors[1]}) { print "$reading{$sensors[1]}\n"; } else { print "UNDEFINED\n"; } } elsif ($munin) { for my $n (@sensors) { my $value = $reading{$n}; print "t$n.value $value\n"; } } else { # Plain text output print map { "Sensor $_: $reading{$_}\n" } sort keys %reading; }