#!/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 @collect = ();
while ($timeout>0) {
my $curr_day = (localtime)[3];
if ($curr_day == 1) {
# NOTE: month changed, change RRD
# FIXME: avoid to check that every second...
$rrd = rrd_init();
}
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 (defined $count and $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) {
my ($counter, $uptime, $humidity, $temperature, $quality, $particles)
= $_ =~ m/^([0-9]+);([0-9]+);([0-9]+.[0-9]+);([0-9]+.[0-9]+);([0-9]+);([0-9]+.[0-9]+)$/;
if (defined $particles) {
#print STDOUT ($time, ";", $_, "\n");
$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;