#!/usr/bin/perl -w # Licence: WTFPLv2 # Copyright 2015: Julien Moutinho use strict; use warnings; use DateTime::Duration; use DateTime; use Device::SerialPort qw( 0.07 ); use File::Basename; use File::Path; use File::Spec; use Log::Log4perl qw(:easy); use RRDTool::OO; #use Data::Dumper qw(Dumper); #use IO::Handle; my $step = 30; # NOTE: sampling interval in seconds sub rrd_init () { Log::Log4perl->easy_init( { level => $INFO , category => 'rrdtool' , layout => '%m%n' }); my $now = DateTime->now ( time_zone => 'local' #, locale => $config{locale} ); # ->set_time_zone('floating'); my $dir = File::Spec->catdir(dirname($0), 'rrd', (sprintf '%0d', $now->year())); my $file = File::Spec->catfile($dir, (sprintf '%02d.rrd', $now->month())); File::Path::make_path($dir); my $rrd = RRDTool::OO->new(file => $file); if (not (-e $file)) { my $one_month = DateTime::Duration->new(months => 1, end_of_month => 'limit'); my $month_begin = $now->clone->truncate(to => 'month'); my $month_end = $month_begin->clone->add_duration($one_month); my $month_seconds = $month_end->epoch() - $month_begin->epoch(); $rrd->create ( step => $step , data_source => { name => "humidity" , type => "GAUGE" , min => 0 , max => 100 , heartbeat => 2 * $step # seconds } , data_source => { name => "temperature" , type => "GAUGE" , min => -15 , max => 50 , heartbeat => 2 * $step # seconds } , data_source => { name => "quality" , type => "GAUGE" , min => 0 , max => 1000 , heartbeat => 2 * $step # seconds } , data_source => { name => "particles" , type => "GAUGE" , min => 0 , max => 100000 , heartbeat => 2 * $step # seconds } , archive => { cpoints => 1 , cfunc => 'AVERAGE' , rows => $month_seconds / $step , xff => 0.99 # ignore unknowns values (U) } ); } return $rrd; } sub dev_init ($) { my ($file) = @_; my $dev = Device::SerialPort->new($file); $dev->baudrate(9600); # MUST: match *uino Serial.begin() #$dev->buffers(4096, 4096); # NOTE: no-op on POSIX $dev->databits(8); $dev->dtr_active(1); # NOTE: reset the *uino on serial connection $dev->handshake('none'); $dev->parity("none"); $dev->read_char_time(0); # NOTE: don't wait for each character $dev->read_const_time(1000); # NOTE: 1 second per unfulfilled "read" call $dev->stopbits(1); $dev->write_settings; return $dev; } sub main () { my $rrd = rrd_init(); my $dev = dev_init($ARGV[0]); #autoflush STDOUT 1; my $read_timeout = 60; my $timeout = $read_timeout; my $buffer = ""; my $last_time = 0; my $last_day = (localtime)[3]; my @collect = (); while ($timeout>0) { my $curr_day = (localtime)[3]; if ($curr_day < $last_day) { # NOTE: month changed, change RRD $rrd = rrd_init(); $last_day = $curr_day; } my ($count, $saw) = $dev->read(1); # NOTE: this could have read up to 255 chars (max for portability) # but reading only 1 char at a time # enables to add a more accurate timestamp. if ($count > 0) { $buffer .= $saw; my @lines = split /\r\n/, $buffer; if (@lines > 1) { my $time = time; # NOTE: process only ended lines $buffer = pop @lines; foreach (@lines) { if ($_ =~ /^\d+;/) { #print STDOUT ($time, ";", $_, "\n"); my @fields = split /;/, $_; next if @fields != 6; my ($counter, $uptime, $humidity, $temperature, $quality, $particles) = @fields; $collect[0] = $humidity if ($humidity ne ''); $collect[1] = $temperature if ($temperature ne ''); $collect[2] = $quality if ($quality ne ''); $collect[3] = $particles if ($particles ne ''); if ($time >= $last_time + $step && @collect == 4) { $rrd->update ( time => $time , values => [join (':', @collect)] ); @collect = (); $last_time = $time; } } else { print STDERR ($_, "\n"); } } } $timeout = $read_timeout; } else { $timeout--; } } if ($timeout==0) { die "Connection timeout (after ${read_timeout}s)\n"; } undef $dev; } main; 1;