#!/usr/bin/perl
use warnings;
use strict;

# Copyright 2015 Doug Coulter.
# This is a script to read arduino(s), various other environmental variables,
# and stuff them into a local MySQL database for later use.
#
# the BMP180 read/calibrate stuff is modified from code by
# Copyright 2014 by Jason Seymour
# which I assume/hope is GPLV2, making all of this also GPLV2.

#### Database table layout doc
# for now, a single table database on localhost

#Database name OTEnv
# Table: OTData
# Epoch		bigint Linux epoch
# TimeStamp	timestamp
# WaterLevel	unsigned smallint gallons
# WaterInValve	tinyint - 0=closed, 1=open
# WaterOutValve	tinyint  ""	""
# WaterInRate	unsigned int	counts
# WaterOutRate	unsigned int	counts
# IndoorTemp	float	degF (all temps are F)
# IndoorHumid	float	relative humidity
# OutdoorTemp	float	temperature
# OutdoorHumid	float	relhumid
# BaseTemp	float	basement temperature
# BaseHumid	float	rel humid
# ThermoCJ	float   thermocouple cold junction
# Thermocouple  float	net thermocouple temp (woodstove flue)
# BoxTemp	float	Pi box temperature from bmp180
# Barometer	float	hectopascals, BMP-180
# CPUTemp	float	SOC internal temp
# Uptime	bigint	Uptime
#
#
##################### Libraries ############################################
use Device::SerialPort;
use Time::HiRes qw (usleep alarm sleep); # removed time to use integer systime since epoch
use Time::HiRes::Sleep::Until;
use DateTime;
use Device::SMBus;
use DBI;

##################### Constants ###########################################
# Define the BMP-180 sensor registers. Many of these store calibration data
# which is used to calculate temperature compensated pressure readings.

use constant BMP180_CAL_AC1		=> 0xAA;	# Calibration data (16 bit)
use constant BMP180_CAL_AC2		=> 0xAC;	# Calibration data (16 bit) 
use constant BMP180_CAL_AC3		=> 0xAE;	# Calibration data (16 bit)
use constant BMP180_CAL_AC4		=> 0xB0;	# Calibration data (16 bit)
use constant BMP180_CAL_AC5		=> 0xB2;	# Calibration data (16 bit)
use constant BMP180_CAL_AC6		=> 0xB4;	# Calibration data (16 bit)
use constant BMP180_CAL_B1		=> 0xB6;	# Calibration data (16 bit)
use constant BMP180_CAL_B2		=> 0xB8;	# Calibration data (16 bit)
use constant BMP180_CAL_MB		=> 0xBA;	# Calibration data (16 bit)
use constant BMP180_CAL_MC		=> 0xBC;	# Calibration data (16 bit)
use constant BMP180_CAL_MD		=> 0xBE;	# Calibration data (16 bit)
use constant BMP180_CONTROL		=> 0xF4;
use constant BMP180_TEMPDATA		=> 0xF6;
use constant BMP180_PRESSUREDATA	=> 0xF6;
use constant BMP180_READTEMPCMD		=> 0x2E;
use constant BMP180_READPRESSURECMD	=> 0x34;

use constant BMP180_STANDARD		=> 1;
use constant BMP180_HIRES		=> 2;

use constant FILTERI	=> 0.1; # input constant for somple LPF
use constant FILTERA	=> 1.0 - FILTERI; # accumulator constant (must add to 1.0000)

##################### Globals ##############################################
my $debug = 0; # set true to get verbosity
my $port; # serial port to slaves
my $dsn = "DBI:mysql:host=localhost;database=OTEnv"; # data source name
my $dbh;	# database connection handle
my $sth;	# statement handle
my $tablename = "testing"; # testing for resting, OTData for the real thing
my $su;		# timer variable
my $seconds = 5;	# next hit time within minute
my $firsttime = 1;	# for clean init
my $epoch;		# time since epoch


# BMP-180 stuff - create object that connects to the sensor
my $bmp180; #bmp180 in pi chassis

#BMP variables for calibration etc - a mess, not mine (DC), it was worse before
my $mode = BMP180_HIRES;
my ($cal_AC1,$cal_AC2,$cal_AC3,$cal_AC4,$cal_AC5,$cal_AC6,$cal_B1,$cal_B2,$cal_MB,$cal_MC,$cal_MD);
my ($X1,$X2,$X3,$B3,$B4,$B5,$B6,$B7,$msb,$lsb,$xlsb); # for readBMP(), save per-call allocations
my $UT; # uncompensated temperature
my $UP; # raw pressure

# stuff we'll shove into database

#from pi box
my $boxtemp; # compensated temperature
my $boxtempa; # acquistions pre-filter
my $boxpressurea; # baromentric pressure pre-filter
my $boxpressure; # baromentric pressure
my $uptime; # seconds
my $soctempa; # pi internal temperature, prefilter
my $soctemp; # pi internal temperature
my $ardline; # dataline from arduino1

#aduino line values for acquisition
my ($waterlevela,$waterinratea,$wateroutratea,$indoortempa,$indoorhumida,$outdoortempa,$outdoorhumida,$basetempa,$basehumida,
$thermocja,$thermotempa,$waterinvalvea,$wateroutvalvea);

# for actually putting into database
my ($waterlevel,$waterinrate,$wateroutrate,$indoortemp,$indoorhumid,$outdoortemp,$outdoorhumid,$basetemp,$basehumid,
$thermocj,$thermotemp,$waterinvalve,$wateroutvalve);
 
# stuff for water valve control

my $waterfilepath = "/var/tmp/water"; # file has one char and lf.  I,C,D mean inlet, close all, dump
my $waterfiletime; # last mod time of water file (if exists)
my $waterfilemaxage = 2700; # 2700 seconds is 45 min - a long time to have a valve open
my $waterfiledata;

##################### Subroutines ###########################################

############################ Arduino #########################################
sub setupSerial
{ # note, use sudo raspi-config to remove login terminal from raspi's seral port
      my $status;
      my $portpath = "/dev/ttyAMA0"; # pi's serial port
     $port = new Device::SerialPort ($portpath, 1) || die "Can't open $portpath: $!\n";
     $port->databits(8);
     $port->baudrate(115200); # was default, but set anyway
     $port->parity("none");
     $port->stopbits(1);
     $port->handshake("none");
     $port->datatype('raw');
     $port->stty_icanon(0); #ascii
     $port->are_match("\n");# possible end strings, not used yet here
     $port->lookclear;      # empty the buffer
     $port->read_char_time(20);    #  wait for each character - a little
     $port->read_const_time(100); #  milliseconds per unfulfilled "read" call
     $port->write_settings or undef $port; # die, sort of, if not working
     die "no port at $portpath: $!" unless $port;  # die, really 
#     $status = $port->can_status;
#     print "port can_status: $status\n" if $debug; # automatically detected
     sleep 1;  # give this time
 # should now have a port open to the device we wanted.    	
}
################################################################################
sub readard()
{ # get the data from arduino via raspi's serial interface (not usb)
 my $gotit;
 my $loopcount = 0;
 
 my $match = $port->matchclear;# so we dont keep reading the same line over
 $gotit = "";
   until ("" ne $gotit) {
      $gotit = $port->lookfor;       # poll until data ready
      die "Aborted without match\n" unless (defined $gotit);
      sleep .01;                          # polling sample time
      $loopcount++; 	

      if ($loopcount > 100) { #uh oh, somthing crashed
      $port->write ("1r\n"); # send reset command - might work
      sleep 1; # flag error, this will usually produce a line of zeros
      $loopcount = 0;	
      print "trouble communicating with arduino\n";
     }
 }

#  printf ("%s\n", $gotit");                # input BEFORE the match
 $ardline = $gotit;
#  printf (":%s\n",$ardline) if $debug;    
}
################################################################################
sub parseard
{ # perl almost makes this too easy
 ($waterlevel,$waterinratea,$wateroutratea,$indoortempa,$indoorhumida,$outdoortempa,$outdoorhumida,$basetempa,$basehumida,
$thermocja,$thermotempa,$waterinvalvea,$wateroutvalvea) = split /\s+/,$ardline;

# so, I'll complicate it a bit with some logic and filtering...
 $waterinrate += $waterinratea; # easy, just sum them
 $wateroutrate += $wateroutratea;

 if ($firsttime) { # just assign them, no filtering possible, yet
  $waterinrate = $waterinratea; # I suppose I could have done this with array slices, this is more readable I think
  $wateroutrate = $wateroutratea;
  $indoortemp = $indoortempa;
  $indoorhumid = $indoorhumida;
  $outdoortemp = $outdoortempa;  
  $outdoorhumid = $outdoorhumida;
  $basetemp = $basetempa;
  $basehumid = $basehumida;
  $thermocj = $thermocja;
  $thermotemp = $thermotempa;
  $waterinvalve = $waterinvalvea;
  $wateroutvalve = $wateroutvalvea;

 } else { # apply low pass filter or logic to some values
# logic first - if a valve was open, keep it saying that - we'll catch close next minute if it does close
 if ($waterinvalvea !=0) {$waterinvalve = 1;} # set if true, leave alone otherwise
 if ($wateroutvalvea !=0){$wateroutvalve = 1;}
# now lowpass filters to reduce sensor noise (median smoothing might be better, but a lot more work)
  $indoortemp = $indoortempa * FILTERI + $indoortemp * FILTERA;
  $indoorhumid = $indoorhumida * FILTERI + $indoorhumid * FILTERA;
  $outdoortemp = $outdoortempa * FILTERI + $outdoortemp * FILTERA;
  $outdoorhumid = $outdoorhumida * FILTERI + $outdoorhumid * FILTERA;
  $basetemp = $basetempa * FILTERI + $basetemp * FILTERA;
  $basehumid = $basehumida * FILTERI + $basehumid * FILTERA;
  $thermocj = $thermocja * FILTERI + $thermocj * FILTERA;
  $thermotemp = $thermotempa * FILTERI + $thermotemp * FILTERA;
 }

}

############################ BMP-180 #########################################

# The Device::SMBus module provides methods for reading 8 and 16 bit values
# from the sensor, but these methods don't differentiate between signed and
# unsigned values. We need to create our own functions to read signed values
# and handle them correctly.

sub readS8 {
	my $register = shift;;
	$lsb = $bmp180->readByteData($register);
	if($lsb > 127) {
		$lsb -= 256;
	}
	return $lsb;
}
sub readS16 {
	my $register = shift;
	$msb = readS8($register);
	$lsb = $bmp180->readByteData($register+1);
	$msb <<= 8;
	return $msb+$lsb;
}

sub readU16 {
	my $register = shift;
        $msb = $bmp180->readByteData($register);
        $lsb = $bmp180->readByteData($register+1);
	$msb <<= 8;
	return ($msb + $lsb);
}

###################################
sub cal180
{
# Read the calibration data from the sensor's eeprom and store it locally
$cal_AC1 = readS16(BMP180_CAL_AC1);
$cal_AC2 = readS16(BMP180_CAL_AC2);
$cal_AC3 = readS16(BMP180_CAL_AC3);
$cal_AC4 = readU16(BMP180_CAL_AC4);
$cal_AC5 = readU16(BMP180_CAL_AC5);
$cal_AC6 = readU16(BMP180_CAL_AC6);
$cal_B1  = readS16(BMP180_CAL_B1);
$cal_B2  = readS16(BMP180_CAL_B2);
$cal_MB  = readS16(BMP180_CAL_MB);
$cal_MC  = readS16(BMP180_CAL_MC);
$cal_MD  = readS16(BMP180_CAL_MD);
}
##################################
sub readBMP()
{ #cleaned up from originator, but still a mess - at least it works
# temp, raw
	$bmp180->writeByteData(BMP180_CONTROL,BMP180_READTEMPCMD);
	# usleep takes microseconds, so this is 5 milliseconds
	usleep(5000);
	$UT = readU16(BMP180_TEMPDATA);
# compensate temperature
	use integer;
	$X1 = (($UT - $cal_AC6) * $cal_AC5) >> 15;
	$X2 = ($cal_MC << 11) / ($X1 + $cal_MD);
	$B5 = $X1 + $X2;
	no integer;
	$boxtempa = ((($B5 + 8) >> 4) / 10.0); # deg C

# get pressure, raw
	$bmp180->writeByteData(BMP180_CONTROL,BMP180_READPRESSURECMD + ($mode << 6));

	if ($mode == BMP180_HIRES) { usleep(14000);}
	else {usleep(8000);}
	$msb = $bmp180->readByteData(BMP180_PRESSUREDATA);
	$lsb = $bmp180->readByteData(BMP180_PRESSUREDATA+1);
	$xlsb = $bmp180->readByteData(BMP180_PRESSUREDATA+2);
	$UP = ((($msb << 16) + ($lsb << 8) + $xlsb) >> (8 - $mode));

# compensate pressure

	use integer;
	 $B6 = $B5 - 4000;
	 $X1 = ($cal_B2 * ($B6 * $B6) >> 12) >> 11;
	 $X2 = ($cal_AC2 * $B6) >> 11;
	 $X3 = $X1 + $X2;
	 $B3 = ((($cal_AC1 * 4 + $X3) << $mode) + 2) /4;
	 $X1 = ($cal_AC3 * $B6) >> 13;	
	$X2 = ($cal_B1 * (($B6 * $B6)) >> 12 ) >> 16;
	$X3 = (($X1 + $X2) + 2) >> 2;
	$B4 = ($cal_AC4 * ($X3 + 32768)) >> 15;
	$B7 = ($UP - $B3) * (50000 >> $mode);
	if ($B7 < 0x80000000) {$boxpressurea = ($B7 * 2) / $B4;} 
	else {$boxpressurea = ($B7 / $B4) * 2;}
	$X1 = ($boxpressurea >> 8) * ($boxpressurea >> 8);
	$X1 = ($X1 * 3038) >> 16;
	$X2 = (-7357 * $boxpressurea) >> 16;
	$boxpressurea += (($X1 + $X2 + 3791) >> 4);

	no integer;
	$boxpressurea /= 100;
#convert temp to F
	$boxtempa = $boxtempa * 1.8 + 32;

#low pass filter result
	if ($firsttime)
	{
	 $boxpressure = $boxpressurea;
	 $boxtemp = $boxtempa;
	} else {
	 $boxpressure = $boxpressurea * FILTERI + $boxpressure * FILTERA;
	 $boxtemp = $boxtempa * FILTERI + $boxtemp * FILTERA;
	}
}
#######################
sub getuptime()
{ # linux keeps this in a virtual file, almost too easy
 open(Fh,"< /proc/uptime") or die "Couldn't open uptime file: $!";
 ($uptime,undef) = split /\s+/,<Fh>,2;
  close Fh;
}
#######################
sub getsoctemp()
{ # also a virtual file, just get the data, Lowpass it.
 open (Fhc, "< /sys/class/thermal/thermal_zone0/temp") or die "Couldn't open soc temperature file:$!";
 $soctempa = <Fhc>;
 chomp $soctempa;
 $soctempa /= 1000.;
 $soctempa = $soctempa * 1.8 + 32;
# print "Raw SOC:$soctempa\n\n" if $debug;
 if ($firsttime) { $soctemp = $soctempa;}
 else {$soctemp = $soctempa * FILTERI + $soctemp * FILTERA;} # lowpass filter, first order
 close Fhc;
}
##################################
sub debugprint
{ # is *your* code/hardware always perfect?
 printf "Boxtemp: %.2f SOC Temp: %.2f Box Baro: %.2f HectoPascals\n",$boxtemp,$soctemp,$boxpressure;
 printf "Uptime seconds:%.2f\n",$uptime;
 printf "WaterLevel: %u Invalve: %u WaterInRate: %u WaterOutValve: %u WaterOutRate: %u\n",$waterlevel,$waterinvalve,
$waterinrate,$wateroutvalve,$wateroutrate;
 printf "IndoorTemp: %.2f IndoorHumid: %.2f OutdoorTemp: %.2f OutdoorHumid: %.2f BaseTemp: %.2f BaseHumid %.2f\n",
$indoortemp,$indoorhumid,$outdoortemp,$outdoorhumid,$basetemp,$basehumid;
 printf "ThermoCJ: %.2f ThermoTemp: %.2f\n",$thermocj,$thermotemp; 
print "\n";
}
##################################
sub init
{ # start various balls rolling, make connections
 my $logfile = "/home/pi/bin/StuffData.log";
 $epoch = time();
 open LOG, ">> $logfile";
 open ( STDERR, ">>$logfile" );
 open ( STDOUT, ">>$logfile" );
 select ( LOG );
 $| = 1; # autoflush
 select ( STDOUT);
 print "StuffData Started (again?) at $epoch seconds\n"; # to log file
 $bmp180 = Device::SMBus->new(
  I2CBusDevicePath => '/dev/i2c-1',
  I2CDeviceAddress => 0x77,);
 setupSerial();
 $port->write("1!\n"); # do this first since first sample takes some time
 print "init\n" if $debug; # code flow trace
 cal180(); # need this once to get factory calibration data for BMP-180
 print "Connecting to database\n" if $debug;
 $dbh = DBI->connect($dsn,"acq","") or die "Cannot connect to db server\n"; # also, some free delay time
# prepare $sth here too
 $sth = $dbh->prepare("INSERT INTO $tablename (Epoch,Timestamp,WaterLevel,WaterInValve,WaterOutValve,WaterInRate,WaterOutRate,
IndoorTemp,IndoorHumid,OutdoorTemp,OutdoorHumid,BaseTemp,BaseHumid,ThermoCJ,Thermocouple,BoxTemp,Barometer,CPUTemp,Uptime)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)") or die "prepare failed$!\n"; # holy long row, batman!
# bind doesn't work here, uses current variable values.

 $su = Time::HiRes::Sleep::Until->new; # create a "wait until" object
 sleep 3; # let arduino take a sample before we read it (some of the above may or may not take long enough)

}

#############################################################################
##################### Main ##################################################
#############################################################################

print "compile done.  Whew!\n" if $debug;
init(); # initialze hardware and database connection

# main loop forever
do { # hopefully a more time-accurate sample and stuff routine (no creep?)

 $epoch = time();
 print "epoch:$epoch\n" if $debug;

 if ($seconds == 0)
 { # top of the minute is when we stuff the database
  print "even minute\n" if $debug;
# Stuff database here, once a minute
#do the deed, fewer calls than bind per shot
$sth->execute($epoch,undef,$waterlevel,$waterinvalve,$wateroutvalve,$waterinrate,$wateroutrate,
$indoortemp,$indoorhumid,$outdoortemp,$outdoorhumid,$basetemp,$basehumid,$thermocj,$thermotemp,
$boxtemp,$boxpressure,$soctemp,$uptime);

# zero out water counters/solenoid values here
$waterinvalve=$waterinrate=$wateroutvalve=$wateroutrate = 0;
} # if seconds == 0, eg once a minute stuff above

# note all these data aq things use system calls, so, in effect, "yield" to the opsys (which has other things going)
# As we say, beware not noticing the wires that ain't there - they don't need to be.  Unless you change the code(!).
# all this is run once per 5 seconds
 
 $port->write("1r\n"); # ask for a report
 readBMP();
 getsoctemp(); # these do what they say, plus provide some delay between
 getuptime(); # writing the arduino a command and trying to read the response
 readard(); # arduino stuff
 parseard(); # figure out what it said, stuff variables 
# debugprint() if $debug;

# check for water valve control file (manual over-ride via a cgi for the moment)
 if (-e $waterfilepath) # check for exists
 {
  $waterfiletime = (stat($waterfilepath))[9];  # last mod time
  if ($epoch - $waterfiletime > $waterfilemaxage)
  { # too old, close all valves and erase file
   $port->write("1c\n"); # tell arduino to close valves
   unlink($waterfilepath); # delete file
  } else
  { # read file and do what it says
   open (WFH,"< $waterfilepath") or die "couldn't open water file\n";
   $waterfiledata = <WFH>;
   chomp $waterfiledata; # remove line end
   $waterfiledata = lc $waterfiledata; # handy, same format, different cases is all
   $port->write("1$waterfiledata\n"); # unit number, command, EOL
  }
 }


 unless ($firsttime) {$seconds += 5;}
 if ($seconds >=60){$seconds = 0;}
 $firsttime = 0; # been there...we jam LPF values etc first time so we start out ~right instead of ramping to value.
 } while ($su->second($seconds));
#############################################################################
END { # clean up any mess if we're aborted
$dbh->disconnect();
}
# the real end of this program (you could cut/paste this or use the <DATA> filehandle to read it)
# this is the database structure. Two identical tables, one for testing.  To rereate this, you have to manually
# create the database first (either match my $dsn name, user pword, or change to match), then import this SQL.
__END__

