Datalogger.PL polls TCP, UDP, and serial streams for line delimited data and logs it to a file with optional timestamps and source notation.
Usage
usage: ./Datalogger.PL [options]
options:
-p
--port ports or sources (see below)
default: T192.168.0.19:4000 S/dev/ttyUSB0
-f
--filename default: data.log
-c
--configFile default:
-T
--timestamping turn on timestamping; default 0
-P
--logport turn on port logging; default 0
-v
--verbose turn on verbose output to STDERR
-m
--monolithic specify monolithic log file rather than interval
-i
--interval interval for each log file
default: 3600
-d []
--subdirectory [] use subdirectory organization of log files
default:
-h this help text
Ports (sources) can be any combination of TCP, UDP, and serial devices.
TCP ports are specified by prefacing the IP address:port with 'T' as in:
T192.168.0.101:400 or Tserver_address.net:400
where the number following the colon is the port number, in this case 400.
UDP ports are specified by prefacing the IP address:port with 'U' as in:
U192.168.0.101:400 or Userver_address.net:401
where the number following the colon is the port number, in this case, 401.
Serial devices are specified by prefacing the device name with a 'S' and
appending the communications parameters as in:
S/dev/ttyUSB0:9600,8,1,N or Scom1:9600,8,1,N
where the communications parameters are, in this case, 9600 baud, 8 bits,
1 stop bit, no parity
Multiple ports may be listed as separate options, as in:
-p Tdata_server.net:400 -p S/dev/ttyS0
or as a single, quoted, space delimited option, as in:
-p "Tdata_server.net:400 S/dev/ttyS0"
Perl Code
#!/usr/bin/perl
# $Id: DataLogger.PL,v 1.5 2007/07/02 08:43:30 ben Exp ben $
# a port scanning client for logging data from B&B Vlinx ports
# and serial ports (added November, 2006)
# copywrite Ben Smith, S/V Mother of Perl, 2007
# designed specifically for the Linkstation but may work
# elsewhere
# 28 May, 2006 - added subdirectory log organization
#########################################################
use strict;
use warnings;
use IO::Socket::INET;
use IO::Select;
use Getopt::Long;
use Time::HiRes qw(gettimeofday);
# defaults
my $program_name = $0;
my $fileStartTime = 0;
my $logFileName = "data.log";
my @inputs;
my @defaultInputs = qw(T192.168.0.19:4000 S/dev/ttyUSB0);
my $timeStamp = 0;
my $portLog = 0;
my $verbose = 0;
my $fileInterval = 60 * 60;
my $monolithic = 0;
my $help = 0;
my $subDir = ''; # use subdirectory organization of log files
my $address = '';
my $configFile = '';
my $debug = 0;
if(-r $configFile) {
# read in previous settings
if(open SETTINGS, "<$configFile") {
my @settings = <SETTINGS>;
if(! eval @settings) {
warn("could not evaluate $configFile: $!\n");
}
}
}
GetOptions("port|source|input|p=s" => \@inputs, # the list of command line
# specified inputs to log
"filename|f=s" => \$logFileName,
"configFile|c=s" => \$configFile,
"timestamp|T" => \$timeStamp,
"logport|P" => \$portLog,
"verbose|v" => \$verbose,
"monolithic|m" => \$monolithic,
"interval|i=n" => \$fileInterval,
"subdirectory|d=s" => \$subDir,
"x=n" => \$debug,
"help|h" => \$help,
);
# set default inputs if none specified
print STDERR join(" ",@inputs),"\n" if $debug & 1;
@inputs = @defaultInputs unless @inputs;
# process command line arguments
my $selector = IO::Select->new();
if($help) {
print STDERR <<"EOT";
usage: $program_name [options]
options:
-p <portlist>
--port <portlist> ports or sources (see below)
default: @inputs
-f <logFileName>
--filename <logFileName> default: $logFileName
-c <configFileName>
--configFile <configFileName> default: $configFile
-T
--timestamping turn on timestamping; default $timeStamp
-P
--logport turn on port logging; default $portLog
-v
--verbose turn on verbose output to STDERR
-m
--monolithic specify monolithic log file rather than interval
-i <seconds>
--interval <seconds> interval for each log file
default: $fileInterval
-d [<subdir>]
--subdirectory [<subdir>] use subdirectory organization of log files
default: $subDir
-h this help text
Ports (sources) can be any combination of TCP, UDP, and serial devices.
TCP ports are specified by prefacing the IP address:port with 'T' as in:
T192.168.0.101:400 or Tserver_address.net:400
where the number following the colon is the port number, in this case 400.
UDP ports are specified by prefacing the IP address:port with 'U' as in:
U192.168.0.101:400 or Userver_address.net:401
where the number following the colon is the port number, in this case, 401.
Serial devices are specified by prefacing the device name with a 'S' and
appending the communications parameters as in:
S/dev/ttyUSB0:9600,8,1,N or Scom1:9600,8,1,N
where the communications parameters are, in this case, 9600 baud, 8 bits,
1 stop bit, no parity
Multiple ports may be listed as separate options, as in:
-p Tdata_server.net:400 -p S/dev/ttyS0
or as a single, quoted, space delimited option, as in:
-p "Tdata_server.net:400 S/dev/ttyS0"
EOT
exit;
}
# figure the port list
# break down the multiple input port specs into a list of individual inputs
my @portList;
for my $input (@inputs){ # look at each input element to see if it might
# consist of a space seperated list that needs
# to be split into parts
@portList = (@portList, split(/\s+/,$input)) ;
}
my %inputStructs; # a structure of info about the input ports
# try and open each of the inputs in the input port list
foreach my $port (@portList){
$port =~ tr/a-z/A-Z/; # make uppercase
if (my($type,$address,$settings) = ($port =~ m/(^.)([^:]+):(.*)/)) {
print STDERR "$port: $type $address $settings\n" if $debug & 1;
# TCP
if($type eq 'T'){ # tcp socket port
my $socket = IO::Socket::INET->new(
PeerAddr => $address,
PeerPort => $settings,
Proto => "tcp",
Type => SOCK_STREAM);
if($socket) {
$selector->add($socket); # add to IO::Select list
$inputStructs{$socket} = $settings; # build an association
}
}
#UDP
if($type eq 'U'){ # udp socket port
my $socket = IO::Socket::INET->new(
PeerAddr => $address,
PeerPort => $settings,
Proto => "udp",
Type => SOCK_STREAM);
if($socket) {
$selector->add($socket); # add to IO::Select list
$inputStructs{$socket} = $settings; # build an association
}
}
#SERIAL
if($type eq 'S'){ # serial port
if(-r $address) {
my($baud,$bits,$stop,$parity) = split(/\,/,$settings);
my($parityStr, $stopStr);
# assuming we have a **IX OS with stty
if(open( my $serialFH,"<$address") ) {
if($bits == 8) {
if($parity eq 'E') {
$parityStr = "-parodd";
} else {
$parityStr = "parodd";
}
} else { # not parity bit
$parityStr = "parenb";
}
if($stop == 1) {
$stopStr = "-cstopb";
} else {
$stopStr = "cstopb";
}
system("stty -F $address $baud $parityStr $stopStr");
$selector->add($serialFH);
$inputStructs{$serialFH} = $address;
}
}
}
}
} # end of inputs setup
if (! $selector->count() ){
my $noPortStr = "\t" . join("\n\t",@portList) . "\n";
die "cannot make any connections to specified input:\n${noPortStr}";
}
if($monolithic) { # just open the logfile this once, if it is to be monolithic
OpenLogFile();
}
# handle interupt and quit signals
$SIG{INT} = \&CloseAll;
$SIG{QUIT} = \&CloseAll;
# poll the (succesful and ready) connections for input
while(1) {
# check to see if we should open a new log file
my $wholeSecs = time();
if(! $monolithic && (( $wholeSecs - $fileStartTime) > $fileInterval) ) {
OpenLogFile($wholeSecs);
$fileStartTime = $wholeSecs;
}
# read the port
foreach my $port ($selector->can_read) {
# read from the socket
my $data = readline($port);
chomp $data;
my $output = "";
#prepend portNo if requested
if($portLog) {
$output .= "$inputStructs{$port}\t";
}
#prepend timestamp if requessted
if($timeStamp) {
my $secs = gettimeofday();
my $timestr = sprintf("%013.3f",$secs);
$output .= "$timestr\t";
}
# concat actual string
$output .= "$data\n";
print LOGFILE $output;
print STDERR $output if $verbose;
}
}
sub OpenLogFile { # names and open the LOGFILE
my $secs = shift;
my $filename = ($monolithic) ? $logFileName :
MakeFileName($logFileName,$secs);
close LOGFILE;
open(LOGFILE, ">$filename") ||
die "cannot open \"$filename\" for logging: $!\n";
}
sub MakeFileName {
my ($filename,$insert) = @_;
my $path;
my $pathFilename;
if($subDir){
my($sec,$min,$hour,$mday,$month,$year) = localtime(time());
++$month; # UNIX month starts at zero
$year %= 100; # use only last two digits of year
# make strings with elements
$year = sprintf("year-%02d",$year);
$month = sprintf("month-%02d",$month);
$mday = sprintf("day-%02d",$mday);
#create path
$path = ($subDir) ? "$subDir/" : "./"; # start with specified
# subdir if specified
print STDERR "subDir: $subDir\n" if $debug & 2;
print STDERR "logging path: $path\n" if $debug & 2;
foreach my $dir($year,$month,$mday) {
$path .= "$dir/";
mkdir $path unless -d $path;
}
$pathFilename = "${path}${filename}";
} else {
$pathFilename = $filename;
}
# break the filename into path+prefix and suffix
my($prefix,$suffix)= ($pathFilename =~ m/(^.*)\.(.*$)/ ); #)
# recombine using the insert
return "${prefix}-${insert}.${suffix}";
}
sub CloseAll {
my $filehandle = shift;
my @ports = @_;
print STDERR "Closing all sockets and log file\n" if $verbose;
# close all the input
foreach my $port (@ports) {
close($port);
}
#close the output
close($filehandle);
exit;
}
#######################################################################
# $Log: DataLogger.PL,v $
# Revision 1.5 2007/07/02 08:43:30 ben
# cleaned up subdirectory logging
#
# Revision 1.4 2007/07/02 08:02:06 ben
# A little clean-up of variables and their initialization
#
# Revision 1.3 2007/07/01 22:33:26 ben
# simplified specification of ports and got the darn thing working again.