#!/usr/bin/perl
#
# polldavis.pl  (c) copyleft 2003 Tom Southerland
#
# run in the background to poll station data from
# a Davis Vantage Pro.  Logs to a MySQL database.
#
# see cpan.org
use Device::SerialPort;
use DBI;
use Sys::Syslog;
use Net::EasyTCP;

# customize for your setup
$station = "LionsGate";
# serial port info
$port = "/dev/ttyS0";
$baud = "19200";
# enable debugging?
# debug = 0   silence is golden
# debug = 1   log to syslog
# debug = 2   log to syslog and stdout
$debug = 0;
# set syslog facility
$logfacility = "local0";
# db connection info
$dbhost = "your-db-host";
$dbname = "your-db-name";
$dbuser = "your-db-user";
$dbpass = "your-db-user-pass";
$dbtable = "your-db-table";

# what port should we listen on
$serverport = 4321;
# how often to log to the db (in seconds)
$logfreq = 60;
# how many loops at a time
$LOOP = 29;
#
# ---------  end of tunables  -----------
#

# log our pid
open(foo,">/var/run/polldavis.pid")||die;
print foo "$$\n";
close foo;
# announce our starting
&logerr("starting up");
# create device constructor
$ob = Device::SerialPort->new ($port,1) || die "Can't open $port: $!";
$ob->baudrate($baud)       || die "fail setting baudrate";
$ob->parity("none")       || die "fail setting parity";
$ob->databits(8)   || die "fail setting databits";
$ob->stopbits(1)   || die "fail setting stopbits";
$ob->handshake("none") || die "fail setting handshake";
# timeouts
$ob->read_char_time(50);     # time between chars
$ob->read_const_time(3000); # timeout after 3 seconds / LOOP is sent every 2.5 sec
$ob->write_settings || die "no settings";

# create db connection
$dsn = "DBI:mysql:database=$dbname;host=$dbhost";
$dbh = DBI->connect($dsn, $dbuser, $dbpass) || die "DBI connect failed";

# create our server listener
$server = new Net::EasyTCP(
  mode => "server",
  port => $serverport,
) || die;

# bind in the server callbacks
$server->setcallback(
  data       => \&gotdata,
  connect    => \&connected,
  disconnect => \&disconnected
) || die;

# -- program start --

# set vars used for data calculations to known state
$dloop=$tempoutT=$tempinT=$barometerT=$humoutT=$huminT=$wspeedT=$winddirT=0;
$rainrateT=$uvT=$solarT=$windgust=$windgustdir=0;
$lastdbwrite = 0;
$loopcount = $LOOP;

# wake up the device
&wakeup;
# start our server listener
$server->start( \&polldavis )||die;

# -- program end --

sub polldavis {
  # start the loop cycle
  if(&readloop){
    &dbrecord;
    &feedclients;
  }else{
    while(&sendloop == 0){
      &wakeup;
    }
  }
}

sub feedclients {
  my $key;
  foreach $key(keys %ClientsState){
    if($ClientsState{$key} eq "start"){
      $weatherdata{'action'}="real_time_data";
      my $client = $ClientsConnected{$key};
      $client->send(\%weatherdata) || die "ERROR SENDING TO CLIENT: $@\n";
    }
  }
}

sub gotdata {
  my $client = shift;
  my $serial = $client->serial();
  my $data = $client->data();
  ($data, $args) = split (/ /,$data,2);
  logerr("Client $serial sent me: $data");
  # $client->send($data) || die "ERROR SENDING TO CLIENT: $@\n";
  if ($data eq "exit") {
    $ClientsState{$serial}='connect';
    $client->close() || die "ERROR CLOSING CLIENT: $@\n";
  }
  elsif ($data eq "start_rtdata") {
    $ClientsState{$serial}='start';
  }
  elsif ($data eq "stop_rtdata") {
    logerr("Client $serial sent STOP");
    $ClientsState{$serial}='connect';
  }
  elsif ($data =~ /rrq/) {
    my %hashmsg = ("action" => "rcf");
    $client->send(\%hashmsg);
  }
  elsif ($data =~ /arq/) {
    my %hashmsg = ("action" => "acf", "level" => "0");
    $client->send(\%hashmsg);
  }
  elsif ($data eq "DIE") {
    logerr("Client $serial sent DIE");
    $server->stop() || die "ERROR STOPPING SERVER: $@\n";
  }
}

sub connected {
  my %hashmsg = ("action" => "connection_ind");
  my $client = shift;
  my $serial = $client->serial();
  logerr("Client $serial just connected");
  $client->send(\%hashmsg);
  $ClientsConnected{$serial}=$client;
  $ClientsState{$serial}='connect';
}

sub disconnected {
  my $client = shift;
  my $serial = $client->serial();
  logerr("Client $serial just disconnected");
  delete $ClientsConnected{$serial};
  delete $ClientsState{$serial};
}

sub dbrecord {
  # data sanity checks
  if($tempout > 120 || $tempout < -30){ return; }
  elsif($tempin > 120 || $tempin < -30){ return; }
  elsif($barometer > 60 || $barometer < 5){ return; }
  elsif($humout > 100 || $humout < 0){ return; }
  elsif($humin > 100 || $humin < 0){ return; }
  elsif($avgwind > 150 || $avgwind < 0){ return; }
  elsif($wspeed > 150 || $wspeed < 0){ return; }
  $now = time();
  # init our client data packet
  ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($now);
  $year += 1900; $mon++;
  if(length($mday) == 1){ $mday = "0".$mday; }
  if(length($mon) == 1){ $mon = "0".$mon; }
  if(length($hour) == 1){ $hour = "0".$hour; }
  if(length($min) == 1){ $min = "0".$min; }
  if(length($sec) == 1){ $sec = "0".$sec; }
  %weatherdata = (
   "date" => "$mon:$mday:$year",
   "time" => "$hour:$min:$sec",
   "outtmp" => "$tempout",
   "intmp" => "$tempin",
   "barp" => "$barometer",
   "outrh" => "$humout",
   "inrh" => "$humin",
   "winds" => "$wspeed",
   "windd" => "$winddir",
   "rrate" => "$rainrate",
   "barroc" => "$bartrend",
   "r1" => "$rainday",
   "r2" => "$rainmonth",
   "r3" => "$rainyear"
);
  # set dbwrite timestamp
  if($lastdbwrite == 0){ $lastdbwrite = $now; }
  # check for max wind speed
  if($wspeed > $windgust){ $windgust = $wspeed; $windgustdir = $winddir; }
  # add vars for average
  $tempoutT = ($tempoutT + $tempout);
  $tempinT = ($tempinT + $tempin);
  $barometerT = ($barometerT + $barometer);
  $humoutT = ($humoutT + $humout);
  $huminT = ($huminT + $humin);
  $wspeedT = ($wspeedT + $wspeed);
  $winddirT = ($winddirT + $winddir);
  $rainrateT = ($rainrateT + $rainrate);
  $uvT = ($uvT + $uv);
  $solarT = ($solarT + $solar);
  # incriment the number of samples
  $dloop++;
  if(($now - $lastdbwrite) >= $logfreq){
    # calculate the period averages
    $tempout = sprintf("%.1f", ($tempoutT / $dloop));
    $tempin = sprintf("%.1f", ($tempinT / $dloop));
    $barometer = sprintf("%.3f", ($barometerT / $dloop));
    $humout = sprintf("%.0f", ($humoutT / $dloop));
    $humin = sprintf("%.0f", ($huminT / $dloop));
    $wspeed = sprintf("%.0f", ($wspeedT / $dloop));
    $winddir = sprintf("%.0f", ($winddirT / $dloop));
    $rainrate = sprintf("%.2f", ($rainrateT / $dloop));
    $uv = sprintf("%.2f", ($uvT / $dloop));
    $solar = sprintf("%.2f", ($solarT / $dloop));
    &logerr("dbrecord: update: loops: $dloop now: $now lastw: $lastdbwrite logfreq: $logfreq diff: ".($now-$lastdbwrite));
    $lastdbwrite = ($lastdbwrite + $logfreq);
    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($now);
    $year += 1900; $mon++;
    $dbh->do("INSERT INTO $dbtable (timekey, station, year, month, mday, hour, min, temperature, temperature_inside,
           barometer, humidity, humidity_inside, windspeed, winddir, windgust, windgustdir, bartrend,
	   rainrate, stormrain, stormdate, rainday, rainmonth, rainyear, consbat, transbat,
	   forecasticons, forecastrule, sunrise, sunset, uv, solar)
           VALUES ('$now', '$station', '$year', '$mon', '$mday', '$hour', '$min', '$tempout', '$tempin',
           '$barometer','$humout','$humin','$wspeed','$winddir','$windgust','$windgustdir','$bartrend',
	   '$rainrate','$stormrain','$stormdate','$rainday','$rainmonth','$rainyear','$consbat','$transbat',
	   '$forecasticons','$forecastrule','$sunrise','$sunset','$uv','$solar')");
    # reset vars for next cycle
    $dloop=$tempoutT=$tempinT=$barometerT=$humoutT=$huminT=$wspeedT=$winddirT=0;
    $rainrateT=$uvT=$solarT=$windgust=$windgustdir=0;
  }
  return;
}

sub sendloop {
  $ob->write("LOOP $LOOP\r");
  &logerr("sendloop: sent: LOOP $LOOP\x0d");
  ($c,$in) = $ob->read(1);
  $in = unpack("H*",$in);
  if( $c == 1 && $in == 06){
    &logerr("sendloop: $c bytes: got ACK: $in");
    $loopcount = $LOOP;
    return(1); # success
  }else{
    &logerr("sendloop: $c bytes: failed ACK: $in");
    return(0); # failure
  }
}

sub readloop {
  return if($loopcount == 0);
  # read current data
  ($c, $in) = $ob->read(99);
  $tmp = unpack("H*", $in);
  &logerr("loop: rawdata: $tmp");
  $crc = &crc16($in);
  if($c != 99 || $crc != 0){
    &logerr("loop: failed at loopcount: $loopcount bytes read: $c crc: $crc");
    return (0); # failed to read LOOP packet
  }else{
    &logerr("loop: loopcount: $loopcount");
    $loopcount--;
  }
  @packet=split(//,$in);
  # parse the packet
  $bartrend = unpack('C*', @packet[3]);
  if($bartrend == 196){ $bartrend = "Falling Rapidly"; }
  elsif($bartrend == 236){ $bartrend = "Falling Slowly"; }
  elsif($bartrend == 0){ $bartrend = "Steady"; }
  elsif($bartrend == 20){ $bartrend = "Rising Slowly"; }
  elsif($bartrend == 60){ $bartrend = "Rising Rapidly"; }
  else{ $bartrend = "No data"; }
  $barometer = unpack('S*', join('',@packet[7],@packet[8]))/1000;
  $tempin = unpack('S*', join('',@packet[9],@packet[10]))/10;
  $humin = unpack('C*', @packet[11]);
  $tempout = unpack('S*', join('',@packet[12],@packet[13]))/10;
  $wspeed = unpack('C*', @packet[14]);
  $avgwind = unpack('C*', @packet[15]);
  $winddir = unpack('S*', join('',@packet[16],@packet[17]));
  $humout = unpack('C*', @packet[33]);
  $rainrate = unpack('S*', join('',@packet[41],@packet[42]))/100;
  $uv = unpack('C*', @packet[43]);
  $solar = unpack('S*', join('',@packet[44],@packet[45]));
  $stormrain = unpack('S*', join('',@packet[46],@packet[47]))/100;
  # //        month | day | year
  # // Bits  15  12  11..7  6..0
  # borrowed from Joe C. Eaton - http://jce.dnsalias.com/Weather/scripts.tar.gz
  $StormStartDatetmp = hex scalar reverse unpack('h*', join('',@packet[48],@packet[49]));
  $StormStartYear = $StormStartDatetmp & 0x7f;
  $StormStartDatetmp = $StormStartDatetmp >> 7;
  $StormStartDay = $StormStartDatetmp & 0x1f;
  $StormStartDatetmp = $StormStartDatetmp >> 5;
  $StormStartMonth = $StormStartDatetmp;
  $StormStartYear = 2000 + $StormStartYear;
  $stormdate = "$StormStartYear-$StormStartMonth-$StormStartDay";
  $rainday = unpack('S*', join('',@packet[50],@packet[51]))/100;
  $rainmonth = unpack('S*', join('',@packet[52],@packet[53]))/100;
  $rainyear= unpack('S*', join('',@packet[54],@packet[55]))/100;
  $transbat= unpack('C*', @packet[86]);
  $consbat= (unpack('S*', join('',@packet[87],@packet[88]))*300)/512/100;
  $forecasticons= unpack('C*', @packet[89]);
  $forecastrule= unpack('C*', @packet[90]);
  $sunrise = &dodate(unpack('S*', join('',@packet[91],@packet[92])));
  $sunset = &dodate(unpack('S*', join('',@packet[93],@packet[94])));
  if(0){
    print "bartrend: $bartrend\n";
    print "barometer: $barometer\n";
    print "tempin: $tempin\n";
    print "humin: $humin\n";
    print "tempout: $tempout\n";
    print "wspeed: $wspeed\n";
    print "avgwind: $avgwind\n";
    print "winddir: $winddir\n";
    print "humout: $humout\n";
    print "rainrate: $rainrate\n";
    print "uv: $uv\n";
    print "solar: $solar\n";
    print "stormrain: $stormrain\n";
    print "stormdate: $stormdate\n";
    print "rainday: $rainday\n";
    print "rainmonth: $rainmonth\n";
    print "rainyear: $rainyear\n";
    print "consbat: $consbat\n";
    print "transbat: $transbat\n";
    print "forecasticons: $forecasticons\n";
    print "forecastrule: $forecastrule\n";
    print "sunrise: $sunrise\n";
    print "sunset: $sunset\n";
  }
  return(1); # success
}

sub wakeup {
  # try to wake up the device
  $ob->read_const_time(1000); # crank down the timeout so reset works
  for($i=0; $i<33; $i++){
    $ob->write("\x0d");
    ($c, $in) = $ob->read(2);
    if( "$in" eq "\x0a\x0d" ){
      $tmp = unpack("H*", $in);
      &logerr("wakeup: got $c bytes: $tmp");
      $ob->read_const_time(3000); # timeout after 3 seconds / LOOP is sent every 2.5 sec
      return(0); # success
    }elsif($c){
      $tmp = unpack("H*", $in);
      &logerr("wakeup: got invalid response: $tmp");
      # drain the line
      while($c){ ($c, $in) = $ob->read(1); }
    }
  }
  return(1); # faulure
}

sub dodate {
  ($d) = @_;
  my $date;
  if( $d =~ /\d\d\d\d\d/ ){ $date = "\x00" }
  elsif( $d =~ /(\d\d)(\d\d)/ ){ $date = "${1}:${2}"; }
  elsif( $d =~ /(\d)(\d\d)/ ){ $date = "0${1}:${2}"; }
  return("$date");
}

#
# check crc of entire data packet
#
# taken from:
# http://www.perlmonks.org/index.pl?node_id=110752
sub crc16 {   # Evan's crc16 transmuted from crc16f.c from kernel-dev files of RH 7.x
    my @crctab = (
    0x0000,  0x1021,  0x2042,  0x3063,  0x4084,  0x50a5,  0x60c6,  0x70e7,
    0x8108,  0x9129,  0xa14a,  0xb16b,  0xc18c,  0xd1ad,  0xe1ce,  0xf1ef,
    0x1231,  0x0210,  0x3273,  0x2252,  0x52b5,  0x4294,  0x72f7,  0x62d6,
    0x9339,  0x8318,  0xb37b,  0xa35a,  0xd3bd,  0xc39c,  0xf3ff,  0xe3de,
    0x2462,  0x3443,  0x0420,  0x1401,  0x64e6,  0x74c7,  0x44a4,  0x5485,
    0xa56a,  0xb54b,  0x8528,  0x9509,  0xe5ee,  0xf5cf,  0xc5ac,  0xd58d,
    0x3653,  0x2672,  0x1611,  0x0630,  0x76d7,  0x66f6,  0x5695,  0x46b4,
    0xb75b,  0xa77a,  0x9719,  0x8738,  0xf7df,  0xe7fe,  0xd79d,  0xc7bc,
    0x48c4,  0x58e5,  0x6886,  0x78a7,  0x0840,  0x1861,  0x2802,  0x3823,
    0xc9cc,  0xd9ed,  0xe98e,  0xf9af,  0x8948,  0x9969,  0xa90a,  0xb92b,
    0x5af5,  0x4ad4,  0x7ab7,  0x6a96,  0x1a71,  0x0a50,  0x3a33,  0x2a12,
    0xdbfd,  0xcbdc,  0xfbbf,  0xeb9e,  0x9b79,  0x8b58,  0xbb3b,  0xab1a,
    0x6ca6,  0x7c87,  0x4ce4,  0x5cc5,  0x2c22,  0x3c03,  0x0c60,  0x1c41,
    0xedae,  0xfd8f,  0xcdec,  0xddcd,  0xad2a,  0xbd0b,  0x8d68,  0x9d49,
    0x7e97,  0x6eb6,  0x5ed5,  0x4ef4,  0x3e13,  0x2e32,  0x1e51,  0x0e70,
    0xff9f,  0xefbe,  0xdfdd,  0xcffc,  0xbf1b,  0xaf3a,  0x9f59,  0x8f78,
    0x9188,  0x81a9,  0xb1ca,  0xa1eb,  0xd10c,  0xc12d,  0xf14e,  0xe16f,
    0x1080,  0x00a1,  0x30c2,  0x20e3,  0x5004,  0x4025,  0x7046,  0x6067,
    0x83b9,  0x9398,  0xa3fb,  0xb3da,  0xc33d,  0xd31c,  0xe37f,  0xf35e,
    0x02b1,  0x1290,  0x22f3,  0x32d2,  0x4235,  0x5214,  0x6277,  0x7256,
    0xb5ea,  0xa5cb,  0x95a8,  0x8589,  0xf56e,  0xe54f,  0xd52c,  0xc50d,
    0x34e2,  0x24c3,  0x14a0,  0x0481,  0x7466,  0x6447,  0x5424,  0x4405,
    0xa7db,  0xb7fa,  0x8799,  0x97b8,  0xe75f,  0xf77e,  0xc71d,  0xd73c,
    0x26d3,  0x36f2,  0x0691,  0x16b0,  0x6657,  0x7676,  0x4615,  0x5634,
    0xd94c,  0xc96d,  0xf90e,  0xe92f,  0x99c8,  0x89e9,  0xb98a,  0xa9ab,
    0x5844,  0x4865,  0x7806,  0x6827,  0x18c0,  0x08e1,  0x3882,  0x28a3,
    0xcb7d,  0xdb5c,  0xeb3f,  0xfb1e,  0x8bf9,  0x9bd8,  0xabbb,  0xbb9a,
    0x4a75,  0x5a54,  0x6a37,  0x7a16,  0x0af1,  0x1ad0,  0x2ab3,  0x3a92,
    0xfd2e,  0xed0f,  0xdd6c,  0xcd4d,  0xbdaa,  0xad8b,  0x9de8,  0x8dc9,
    0x7c26,  0x6c07,  0x5c64,  0x4c45,  0x3ca2,  0x2c83,  0x1ce0,  0x0cc1,
    0xef1f,  0xff3e,  0xcf5d,  0xdf7c,  0xaf9b,  0xbfba,  0x8fd9,  0x9ff8,
    0x6e17,  0x7e36,  0x4e55,  0x5e74,  0x2e93,  0x3eb2,  0x0ed1,  0x1ef0
    );
    my $crc = 0; my $charidx = 0;
    my $nchars = length $_[0];            # $nchars is the number of characters to process
    my @chararry = unpack("c*",$_[0]);    # an array of chars
    my $crcsl = 0; my $crcsr = 0;         # these strings to capture "crc-shift-left" and "crc-shift-right"
    while( $nchars-- )
    {
	$crc = $crc & 65535;     # need short ints - this is how I do it.
	$crcsl = $crc << 8;
	$crcsr = $crc >> 8;
	$crc = ($crcsl) ^ $crctab[($crcsr) ^ $chararry[$charidx++]];   # I never fully understood this.
	$crc = $crc & 65535;     # need short ints - this is how I do it.
    }
    return $crc;
}

sub logerr {
  return if($debug == 0);
  my $msg;
  ($msg) = @_;
  if($debug == 1){
    openlog('polldavis', 'pid', "$logfacility");
    syslog('debug', "$msg");
    closelog();
  }
  if($debug == 2){
    $|=1;
    print "$msg\n";
  }
  return(0);
}
