#!/usr/bin/perl
# 
# Programme "GetInputFromTemperatureKit145"
#
# Author Stephen Kingham, Stephen.Kingham<AT>kingtech.com.au
#
# https://www.kingtech.com.au
#
# License and distributed under the GNU General Public License (GPL)
#
# Copyright (C) 2009 Stephen Kingham
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.


# This programme:
# Gets data from 4 temperature probes using RS232 serial interface.  The tempurature probes come from www.kitsrus.com Kit 145.
# Checks that the data it get s from the RS232 is reasonable, ie it ignors massive changes in temperature.
# It applies some calibration to take into account systematic errors in the probes,
# The programme then creates an RRDTOOL database for each temperature probe if the database does not already exist, and
# then saves the data to the datbase.
#
# There is a start up script that can load this as a daemon.
# There is another programme that creates the HTML pages with graphs of the temperatures.
#
# Note that RRDTOOL is archetecture specific.  An RRDTOOL database created in 64bit can not be read using 32bit and visa versa.
#  However you can convert 32bit to 64bit.  I found this out while upgrading the server that gathers the temperatures from 
#  32bit to 64bit, and that my web server with the CGI that reads the RRDTOOL was on 32bit.


# Ver 1.0 - Tested during 2009 and it is very stable, records negative numbers.
# Ver 1.1 - General tidy up and documentation completed


# I use strict to enforce good PERL programming
use strict;
#
# We need Device::SerialPort so that PERL can talk to the serial (tty) ports
use Device::SerialPort 0.12;
#
# The English PERL Module allows us to write a PERL script that is more readable.
# According to Perldoc the "qw( -no_match_vars )" makes this module work a little more efficiently.
use English qw( -no_match_vars ) ;


# $Debug is "DEBUG", or "DEBUG2" something else.
# DEBUG will send general information to the log, very useful for calibrating the temperature probes
# DEBUG2 this generates a whole lot of programming traps to the log 
my $Debug = "";

# Args is used to process the parameters passed from the command line.
my @Args=();


# CalibrateProbeByA and B are used to apply a linear systematic error to the temperature probes.
# For example the probe aways reads 5 degrees below the real temperature.
# The CorrectTemperature=B * ProbeReading + A
my @CalibrateProbeByA;
my @CalibrateProbeByB;
#
# Probe 1
$CalibrateProbeByA[1] =   0;
$CalibrateProbeByB[1] =   1;
# Probe 2
$CalibrateProbeByA[2] =  0;
$CalibrateProbeByB[2] =  1;
# Probe 3
$CalibrateProbeByA[3] =  0;
$CalibrateProbeByB[3] =  1;
# Probe 3
$CalibrateProbeByA[3] =  0;
$CalibrateProbeByB[3] =  1;
# Probe 4
$CalibrateProbeByA[4] =  4.43509729682135;
$CalibrateProbeByB[4] =  0.853570485163335;
# Probe 5
$CalibrateProbeByA[5] =  0;
$CalibrateProbeByB[5] =  1;
# Probe 6
$CalibrateProbeByA[6] =  0;
$CalibrateProbeByB[6] =  1;
# Probe 7
$CalibrateProbeByA[7] =  0;
$CalibrateProbeByB[7] =  1;
# Probe 8
$CalibrateProbeByA[8] =  0;
$CalibrateProbeByB[8] =  1;
# TempuratureProbeName is an array that has the user readable names of the probes.
# They are initialised here to numbers but the command line switches allows the user
# to change them to things like Bedroom1.  Do not use spaces.
my @TempuratureProbeName;
$TempuratureProbeName[1]="1";
$TempuratureProbeName[2]="2";
$TempuratureProbeName[3]="3";
$TempuratureProbeName[4]="4";
$TempuratureProbeName[5]="5";
$TempuratureProbeName[6]="6";
$TempuratureProbeName[7]="7";
$TempuratureProbeName[8]="8";


#
# Serial Settings
#
my $SerialInterfaceTTYName      = "/dev/ttyS0";          # port to watch
# I have also tested using a USB serial port device. "/dev/ttyUSB0"
#

# Within the loop that process each line send from the probe the following variables are used:
#
# probeNumberRead equals the probe number that the measurement came from
my $probeNumberRead = "";
# probeTempuratureRead equals the temperature read from the probe
my $probeTempuratureRead = "";
# probeTempuratureCalibrated equals the once calibration had been applied
my $probeTempuratureCalibrated = "";
#
# Track the last read temperature to detect errors for each probe
# This is needed on account that errors have been seen, ie 1600 rather than 16
my @TempuratureLastRead1;
my @TempuratureLastRead2;
my @TempuratureLastRead3;
my @TempuratureLastRead4;
my @TempuratureLastRead5;
my @TempuratureLastReadAverage;
# As seen from above we only track the last 5 samples.  
# To know how many samples we have we count them for each probe.
my @NumberOfSamplesUpto5;
#
# Track the number of errors for each probe
# eg sudden increase in temerature, errors in the string
# Too many errors should trigger a reset of the serial port
# NumberOfErrorsForAProbe[0] is a special case.  It tracks the overall errors that can not be attributed to a particulare probe.
my @NumberOfErrorsForAProbe;

# LOGFILE is the name of the file to write log messages.
# This can be changed by the command line switches
my $LOGFILE="/var/log/GetInputFromTemperatureKit145.log";

# RootDataDir is where the RDDTOOL database files are saved.
# This is usualy changed by the command line switches
my $RootDataDir="/tmp/";

my $Minutes;
my $Hours;
my $Day;
my $Month;
my $Year;
my $MonthMMM;
my $DayDD;
my $HoursHH;
my $MinutesMM;
# $HoursHH:$MinutesMM DayDD$MonthMMM$Year


# Process the switched passed from the command line
# argnum is only used here.
my $argnum="";
foreach $argnum (0 .. $#ARGV) {
  #
  if ( $ARGV[$argnum] eq "-d" )         { $RootDataDir=$ARGV[$argnum+1] };
  if ( $ARGV[$argnum] eq "-debug" )     { $Debug="DEBUG" };
  if ( $ARGV[$argnum] eq "-1" )         { $TempuratureProbeName[1]=$ARGV[$argnum+1] };
  if ( $ARGV[$argnum] eq "-2" )         { $TempuratureProbeName[2]=$ARGV[$argnum+1] };
  if ( $ARGV[$argnum] eq "-3" )         { $TempuratureProbeName[3]=$ARGV[$argnum+1] };
  if ( $ARGV[$argnum] eq "-4" )         { $TempuratureProbeName[4]=$ARGV[$argnum+1] };
  if ( $ARGV[$argnum] eq "-5" )         { $TempuratureProbeName[5]=$ARGV[$argnum+1] };
  if ( $ARGV[$argnum] eq "-6" )         { $TempuratureProbeName[6]=$ARGV[$argnum+1] };
  if ( $ARGV[$argnum] eq "-7" )         { $TempuratureProbeName[7]=$ARGV[$argnum+1] };
  if ( $ARGV[$argnum] eq "-8" )         { $TempuratureProbeName[8]=$ARGV[$argnum+1] };
  if ( $ARGV[$argnum] eq "-log" )       { $LOGFILE=$ARGV[$argnum+1] };
  if ( $ARGV[$argnum] eq "-h" || $ARGV[$argnum] eq "-help" ) 
  {
    print "usage: GetInputFromTemperatureKit145 <switches>\n";
    print "  valid switches include:\n";
    print "    -d <directory>           = what directory to create and save the RRDTOOL database files (default /tmp/)\n";
    print "    -1 <text with no spaces> = descriptive name of the first probe, eg outside, default is 1\n";
    print "    -1 <text with no spaces> = descriptive name of the first probe, eg serverRoom, default is 2\n";
    print "    -1 <text with no spaces> = descriptive name of the first probe, eg Bed1, default is 3\n";
    print "    -1 <text with no spaces> = descriptive name of the first probe, eg Bed2, default is 4\n";
    print "    -1 <text with no spaces> = descriptive name of the first probe, eg Bed3, default is 5\n";
    print "    -1 <text with no spaces> = descriptive name of the first probe, eg GardenShed, default is 6\n";
    print "    -1 <text with no spaces> = descriptive name of the first probe, eg Garage, default is 7\n";
    print "    -1 <text with no spaces> = descriptive name of the first probe, eg Laundry, default is 8\n";
    print "    -debug                   = sends raw temperature read from probe and value saved to RDDTOOL database\n";
    print "    -log <absulute filename> = name of the file to write log information, default is /var/log/GetInputFromTemperaturekit145.log\n";
    print "  This programme uses RRDTOOL to store the data\n";
    print "Copyright (C) 2009 Stephen Kingham\n";
    print "More info at www.kingtech.com.au\n";
    print "\n";
    print "This program is free software; you can redistribute it and/or\n";
    print "modify it under the terms of the GNU General Public License\n";
    print "as published by the Free Software Foundation; either version 3\n";
    print "of the License, or (at your option) any later version.\n";
    print "\n";
    print "This program is distributed in the hope that it will be useful,\n";
    print "but WITHOUT ANY WARRANTY; without even the implied warranty of\n";
    print "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n";
    print "GNU General Public License for more details.\n";
    sleep 2;
    exit 0;
  };
};

if ( $Debug eq "DEBUG" && $LOGFILE ne "" )
{
  # Open the log file to write messages to
  open(LOG,">/var/log/GetInputFromTemperatureKit145.log")
    ||die "can't open file /var/log/GetInputFromTemperatureKit145.log $!\n";
  print LOG "$HoursHH:$MinutesMM $DayDD$MonthMMM$Year staring\n";
  print LOG "TempuratureProbeName[1]=$TempuratureProbeName[1]\n";
  print LOG "TempuratureProbeName[2]=$TempuratureProbeName[2]\n";
  print LOG "TempuratureProbeName[3]=$TempuratureProbeName[3]\n";
  print LOG "TempuratureProbeName[4]=$TempuratureProbeName[4]\n";
  print LOG "TempuratureProbeName[5]=$TempuratureProbeName[5]\n";
  print LOG "TempuratureProbeName[6]=$TempuratureProbeName[6]\n";
  print LOG "TempuratureProbeName[7]=$TempuratureProbeName[7]\n";
  print LOG "TempuratureProbeName[8]=$TempuratureProbeName[8]\n";
}

($Minutes, $Hours, $Day, $Month, $Year) = (localtime(time))[1,2,3,4,5];
#
# what is the year
$Year=$Year+1900;
#
# what is the month
$MonthMMM=("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")[$Month];
$DayDD     = substr("0$Day",     -2, 2);
#
# what is the hour
$HoursHH   = substr("0$Hours",   -2, 2);
#
# what is the minutes
$MinutesMM = substr("0$Minutes", -2, 2);
#
if ( $LOGFILE ne "" )
{
  print LOG "$HoursHH:$MinutesMM $DayDD$MonthMMM$Year starting\n";
  print LOG "TempuratureProbeName[1]=$TempuratureProbeName[1]\n";
  print LOG "TempuratureProbeName[2]=$TempuratureProbeName[2]\n";
  print LOG "TempuratureProbeName[3]=$TempuratureProbeName[3]\n";
  print LOG "TempuratureProbeName[4]=$TempuratureProbeName[4]\n";
  print LOG "TempuratureProbeName[5]=$TempuratureProbeName[5]\n";
  print LOG "TempuratureProbeName[6]=$TempuratureProbeName[6]\n";
  print LOG "TempuratureProbeName[7]=$TempuratureProbeName[7]\n";
  print LOG "TempuratureProbeName[8]=$TempuratureProbeName[8]\n";
}


# Create a file and store the PID
# If asked the /etc/init.d/ program uses the PID to stop this program
open(PID,">/var/run/GetInputFromTemperatureKit145.pid")
  ||die "can't open file /var/run/GetInputFromTemperaturekit145.pid $!\n";
printf PID "$PID\n";
close (PID);


#
#
my $SerialPort = Device::SerialPort->new ($SerialInterfaceTTYName) || die "Can't Open $SerialInterfaceTTYName: $!";
$SerialPort->baudrate(2400)   || die "failed setting baudrate";
$SerialPort->parity("none")    || die "failed setting parity";
$SerialPort->databits(8)       || die "failed setting databits";
$SerialPort->handshake("none") || die "failed setting handshake";
$SerialPort->write_settings    || die "no settings";
# Open the serial port for the initial read
open(SERIAL, "+>$SerialInterfaceTTYName")
  ||die "can't open serial port $SerialInterfaceTTYName $!\n";



########################################
#
#  MAIN LOOP


# Get a line from the tempurature probes
#
while (my $lineRead = <SERIAL>)
{


  #
  # Examine the measurement from the probes and calibrate the value to find the actual temperature
  #

  #
  # Perform initial coversion of epoch
  ($Minutes, $Hours, $Day, $Month, $Year) = (localtime(time))[1,2,3,4,5];
  #
  # what is the year
  $Year=$Year+1900;
  #
  # what is the month
  $MonthMMM=("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")[$Month];
  $DayDD     = substr("0$Day",     -2, 2);
  #
  # what is the hour
  $HoursHH   = substr("0$Hours",   -2, 2);
  #
  # what is the minutes
  $MinutesMM = substr("0$Minutes", -2, 2);
  #

  ($probeNumberRead, $probeTempuratureRead ) = split ( " ", $lineRead);
  $probeTempuratureRead =~ s/^0+(.)/"$1"/e;
  #
  if ( $probeNumberRead =~ /^[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?$/ && $probeTempuratureRead =~ /^[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?$/ )
  {
    if ( int($probeNumberRead) eq $probeNumberRead && $probeNumberRead lt 9 )
    {
      # We have a sensible proble number, it is an integer and less than 9
      #
      # Now calibrate the probe to the actual temperature
      #
      if ( $Debug eq "DEBUG" && $LOGFILE ne "" ) {
        printf LOG ("$HoursHH:$MinutesMM $DayDD$MonthMMM$Year Probe$probeNumberRead=$TempuratureProbeName[$probeNumberRead] Temp=%5.1f * %3.1f + %3.1f", $probeTempuratureRead, $CalibrateProbeByB[$probeNumberRead], $CalibrateProbeByA[$probeNumberRead])
      }
      if ( defined $CalibrateProbeByA[$probeNumberRead] )
      {
        $probeTempuratureCalibrated = $probeTempuratureRead * $CalibrateProbeByB[$probeNumberRead] + $CalibrateProbeByA[$probeNumberRead];
      }
      if ( $Debug eq "DEBUG" && $LOGFILE ne "" ) { printf LOG "=%5.1f, moving average=%5.1f\n",$probeTempuratureCalibrated, $TempuratureLastReadAverage[$probeNumberRead] };
      #
      # Is the temperature a sensible value?
      #
      if ( abs( $TempuratureLastReadAverage[$probeNumberRead]) > abs ( 1.5 * $probeTempuratureCalibrated ) && $TempuratureLastReadAverage[$probeNumberRead] ne 0 )
      {
        # We do not want to report this temperature because it changed by too much
        #
        if ( $Debug eq "DEBUG" && $LOGFILE ne "")
        {
          printf LOG ("$HoursHH:$MinutesMM $DayDD$MonthMMM$Year Increase will be ignored for Probe $probeNumberRead %3.1f > %3.1f %3.1f.\n", abs($TempuratureLastReadAverage[$probeNumberRead]), abs($probeTempuratureCalibrated), abs($probeTempuratureCalibrated * 1.5) );
        }
        #
        # Keep track of the number of errors on this probe, if we get too many we will reset the serial port
        $NumberOfErrorsForAProbe[$probeNumberRead] = $NumberOfErrorsForAProbe[$probeNumberRead] + 1;
        #
      }
      else
      {
        # We must have a sensible tempurature
        #
        # Lets start by resetting the error count
        if ( $NumberOfErrorsForAProbe[$probeNumberRead] gt 0 )
        {
          $NumberOfErrorsForAProbe[$probeNumberRead] = $NumberOfErrorsForAProbe[$probeNumberRead] - 1;
        }
        if ( $NumberOfErrorsForAProbe[0] gt 0 )
        {
          $NumberOfErrorsForAProbe[0] = $NumberOfErrorsForAProbe[0] - 1;
        }
        #
        # Check that the database exists becuase we want to save the reading
        #
        if ( ! -e "$RootDataDir/TemperatureProbe$TempuratureProbeName[$probeNumberRead].rrd" )
        {
          if ( $LOGFILE ne "")
          {
            print LOG "$HoursHH:$MinutesMM $DayDD$MonthMMM$Year $RootDataDir/TemperatureProbe$TempuratureProbeName[$probeNumberRead].rrd does not exist so create it\n";
          }
          #
          @Args = ( "rrdtool create $RootDataDir/TemperatureProbe$TempuratureProbeName[$probeNumberRead].rrd --step 5 DS:Temperature:GAUGE:180:-273:60 RRA:AVERAGE:0.5:1:3200 RRA:AVERAGE:0.5:6:3200 RRA:AVERAGE:0.5:36:3200 RRA:AVERAGE:0.5:144:3200 RRA:AVERAGE:0.5:1008:3200 RRA:AVERAGE:0.5:4320:3200 RRA:AVERAGE:0.5:52560:3200 RRA:AVERAGE:0.5:525600:3200 RRA:MIN:0.5:1:3200 RRA:MIN:0.5:6:3200 RRA:MIN:0.5:36:3200 RRA:MIN:0.5:144:3200 RRA:MIN:0.5:1008:3200 RRA:MIN:0.5:4320:3200 RRA:MIN:0.5:52560:3200 RRA:MIN:0.5:525600:3200 RRA:MAX:0.5:1:3200 RRA:MAX:0.5:6:3200 RRA:MAX:0.5:36:3200 RRA:MAX:0.5:144:3200 RRA:MAX:0.5:1008:3200 RRA:MAX:0.5:4320:3200 RRA:MAX:0.5:52560:3200 RRA:MAX:0.5:525600:3200" );
          system(@Args) == 0 or die "ERROR: system @Args failed: $? $!\n";
          if ($? == -1)
          {
            if ( $LOGFILE ne "" )
            {
              print LOG "ERROR: failed to execute @Args\n $!\n";
            }
          }
          elsif ($? & 127)
          {
            if ( $LOGFILE ne "" )
            {
              printf LOG "ERROR: @Args child died with signal %d, %s $!\n", ($? & 127),  ($? & 128) ? 'with' : 'without';
            }
            sleep 2;
            exit 0;
          }
          else
          {
            if ($Debug eq "DEBUG2") { 
              if ( $LOGFILE ne "" )
              {
                printf LOG ("ERROR: @Args child exited with value %d\n", $? >> 8);
              }
            }
          }
        }
        #
        # Update the database
        #
        # Only if we have at least 5 measurements
        if ( $NumberOfSamplesUpto5[$probeNumberRead] gt 4 )
        {
          @Args = ( "rrdtool update $RootDataDir/TemperatureProbe$TempuratureProbeName[$probeNumberRead].rrd N:$probeTempuratureCalibrated" );
          if ($Debug eq "DEBUG2" && $LOGFILE ne "" )
          {
            print LOG "Going to @Args\n";
          }
          system(@Args) == 0 or die "ERROR: system @Args failed: $? $!\n";
          if ($? == -1)
          {
            if ( $LOGFILE ne "" )
            {
              print LOG "ERROR: failed to execute @Args\n $!\n";
            }
          }
          elsif ($? & 127)
          {
            if ( $LOGFILE ne "" )
            {
              printf LOG ("ERROR: @Args child died with signal %d, %s $!\n", ($? & 127),  ($? & 128) ? 'with' : 'without');
            }
            sleep 2;
            exit 0;
          }
          else
          {
            if ($Debug eq "DEBUG2" && $LOGFILE ne "" )
            {
              printf LOG ("ERROR: @Args child exited with value %d\n", $? >> 8);
            }
          }
          #
          # Now write the existing temperature out for possible display
          #
          open(PID,">$RootDataDir/TemperatureProbe$TempuratureProbeName[$probeNumberRead].txt")
            ||die "can't open file $RootDataDir/TemperatureProbe$TempuratureProbeName[$probeNumberRead].txt $!\n";
          printf PID "$probeTempuratureCalibrated\n";
          close (PID);
        }
      }
      # Keep a track of this temperature to test it with the next one read to find errors
      #
      if ( $NumberOfSamplesUpto5[$probeNumberRead] lt 5 )
      {
        $NumberOfSamplesUpto5[$probeNumberRead]=$NumberOfSamplesUpto5[$probeNumberRead]+1;
      }
      $TempuratureLastRead5[$probeNumberRead]=$TempuratureLastRead4[$probeNumberRead];
      $TempuratureLastRead4[$probeNumberRead]=$TempuratureLastRead3[$probeNumberRead];
      $TempuratureLastRead3[$probeNumberRead]=$TempuratureLastRead2[$probeNumberRead];
      $TempuratureLastRead2[$probeNumberRead]=$TempuratureLastRead1[$probeNumberRead];
      $TempuratureLastRead1[$probeNumberRead]=$probeTempuratureCalibrated;
      $TempuratureLastReadAverage[$probeNumberRead]=($TempuratureLastRead1[$probeNumberRead] + $TempuratureLastRead2[$probeNumberRead] + $TempuratureLastRead3[$probeNumberRead] + $TempuratureLastRead4[$probeNumberRead] + $TempuratureLastRead5[$probeNumberRead] ) / $NumberOfSamplesUpto5[$probeNumberRead] ;
    }
  }
  else 
  {
    # Then probeNumberRead is not an integer OR probeTempuratureRead is not a number.
    #
    if ( $Debug eq "DEBUG" )
    {
      if ( $LOGFILE ne "" )
      {
        print LOG "line read was deemed not to be data from a probe\n";
      }
    }
    $NumberOfErrorsForAProbe[0]=$NumberOfErrorsForAProbe[0]+1;
  }

  foreach $probeNumberRead (0 .. 8)
  {
    # If the Serial port is to be reset:
    if ( $NumberOfErrorsForAProbe[$probeNumberRead] gt 5 )
    {
      close(SERIAL);
      open(SERIAL, "+>$SerialInterfaceTTYName");
      if ( $Debug eq "DEBUG" )
      {
        print LOG "Just reset the serial port $SerialInterfaceTTYName\n";
      }
      $NumberOfErrorsForAProbe[$probeNumberRead]=0;
    }
  }
}
undef $SerialPort;

