9 use Mojo::IOLoop::Stream;
10 use Mojo::Transaction::WebSocket;
11 #use Mojo::JSON qw(decode_json encode_json);
15 use Math::Round qw(nearest);
17 use Data::Random qw(rand_chars);
20 use constant pi => 3.14159265358979;
22 my $randomfn = '/dev/urandom';
23 my $devname = "/dev/davis";
24 my $datafn = ".loop_data";
27 my $poll_interval = 2.5;
28 my $rain_mult = 0.2; # 0.1 or 0.2 mm or 0.01 inches
36 our $ser; # the serial port Mojo::IOLoop::Stream
37 our $ob; # the Serial Port filehandle
41 our $json = JSON->new->canonical(1);
42 our $WS = {}; # websocket connections
45 our @last10minsr = ();
47 our $windmins = 2; # no of minutes of wind data for the windrose
48 our $histdays = 5; # no of days of (half)hour data to search for main graph
49 our $updatepermin = 60 / 2.5; # no of updates per minute
51 our $loop_count; # how many LOOPs we have done, used as start indicator
54 0x0, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
55 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
56 0x1231, 0x210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
57 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
58 0x2462, 0x3443, 0x420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
59 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
60 0x3653, 0x2672, 0x1611, 0x630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
61 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
62 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x840, 0x1861, 0x2802, 0x3823,
63 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
64 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0xa50, 0x3a33, 0x2a12,
65 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
66 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0xc60, 0x1c41,
67 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
68 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0xe70,
69 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
70 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
71 0x1080, 0xa1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
72 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
73 0x2b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
74 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
75 0x34e2, 0x24c3, 0x14a0, 0x481, 0x7466, 0x6447, 0x5424, 0x4405,
76 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
77 0x26d3, 0x36f2, 0x691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
78 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
79 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x8e1, 0x3882, 0x28a3,
80 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
81 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0xaf1, 0x1ad0, 0x2ab3, 0x3a92,
82 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
83 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0xcc1,
84 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
85 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0xed1, 0x1ef0
90 $bar_trend{-60} = "Falling Rapidly";
91 $bar_trend{196} = "Falling Rapidly";
92 $bar_trend{-20} = "Falling Slowly";
93 $bar_trend{236} = "Falling Slowly";
94 $bar_trend{0} = "Steady";
95 $bar_trend{20} = "Rising Slowly";
96 $bar_trend{60} = "Rising Rapidly";
100 $SIG{TERM} = $SIG{INT} = sub {$ending = 1; Mojo::IOLoop->stop;};
101 $SIG{HUP} = 'IGNORE';
104 # WebSocket weather service
105 websocket '/weather' => sub {
111 app->log->debug('WebSocket opened.');
112 dbg 'WebSocket opened' if isdbg 'chan';
115 # send historical data
116 $c->send($ld->{lasthour_h}) if exists $ld->{lasthour_h};
117 $c->send($ld->{lastmin_h}) if exists $ld->{lastmin_h};
120 $c->inactivity_timeout(3615);
126 dbg "websocket: text $msg" if isdbg 'chan';
130 dbg "websocket: json $msg" if isdbg 'chan';
135 $c->on(finish => sub {
136 my ($c, $code, $reason) = @_;
137 app->log->debug("WebSocket closed with status $code.");
138 dbg "websocket closed with status $code" if isdbg 'chan';
143 get '/' => {template => 'index'};
153 dbg "*** starting $0";
159 my $dayno = int ($tnow/86400);
160 for (my $i = 0-$histdays; $i < 0; ++$i ) {
161 push @last5daysh, grab_history(SMGLog->new("day"), "h", $tnow-(86400*$histdays), $dayno+$i+1);
163 @last10minsr = map {my ($t, $js) = split(/\s/, $_, 2); $js} grab_history(SMGLog->new("debug"), "r", $tnow-(60*$windmins), $dayno);
164 dbg sprintf("last5days = %d last10mins = %d", scalar @last5daysh, scalar @last10minsr);
166 sysopen(R, $randomfn, 0) or die "cannot open $randomfn $!\n";
168 sysread(R, $rs, 8) or die "not enough randomness available\n";
171 app->secrets([qw(Here's something that's really seakrett), $rs]);
173 our $dlog = SMGLog->new("day");
174 $did = Mojo::IOLoop->recurring(1 => sub {$dlog->flushall});
176 dbg "before next tick";
177 Mojo::IOLoop->next_tick(sub { loop() });
178 dbg "before app start";
180 dbg "after app start";
185 $dataf->close if $dataf;
189 # move all the files along one
190 cycle_loop_data_files();
193 dbg "*** ending $0 (\$ending = $ending)";
198 ##################################################################################
202 dbg "last_min: " . scalar gmtime($ld->{last_min});
203 dbg "last_hour: " . scalar gmtime($ld->{last_hour});
205 $ser = doopen($devname);
206 start_loop() if $ser;
215 $d =~ s/([\%\x00-\x1f\x7f-\xff])/sprintf("%%%02X", ord($1))/eg;
216 dbg "read added '$d' buf lth=" . length $buf if isdbg 'raw';
217 if ($state eq 'waitnl' && $buf =~ /[\cJ\cM]+/) {
218 dbg "Got \\n" if isdbg 'state';
219 Mojo::IOLoop->remove($tid) if $tid;
223 $ser->write("LPS 1 1\n");
224 chgstate("waitloop");
225 } elsif ($state eq "waitloop") {
226 if ($buf =~ /\x06/) {
227 dbg "Got ACK 0x06" if isdbg 'state';
228 chgstate('waitlooprec');
231 } elsif ($state eq 'waitlooprec') {
232 if (length $buf >= 99) {
233 dbg "got loop record" if isdbg 'chan';
244 dbg "start_loop writing $nlcount \\n" if isdbg 'state';
246 Mojo::IOLoop->remove($tid) if $tid;
248 $tid = Mojo::IOLoop->recurring(0.6 => sub {
249 if (++$nlcount > 10) {
253 dbg "writing $nlcount \\n" if isdbg 'state';
261 dbg "state '$state' -> '$_[0]'" if isdbg 'state';
271 $ob = Serial->new($name, 19200) || die "$name $!\n";
272 dbg "streaming $name fileno(" . fileno($ob) . ")" if isdbg 'chan';
274 my $ser = Mojo::IOLoop::Stream->new($ob);
275 $ser->on(error=>sub {dbg "error serial $_[1]"; doclose();});
276 $ser->on(close=>sub {dbg "event close"; doclose();});
277 $ser->on(timeout=>sub {dbg "event serial timeout"; doclose();});
278 $ser->on(read=>sub {on_read(@_)});
281 $rid = Mojo::IOLoop->recurring($poll_interval => sub {
282 start_loop() if !$state;
293 return if $closing++;
295 dbg "serial port closing" if $ser || $ob;
305 Mojo::IOLoop->remove($tid) if $tid;
307 Mojo::IOLoop->remove($rid) if $rid;
310 if (Mojo::IOLoop->is_running && $ending == 0) {
314 Mojo::IOLoop->timer(5 => $delay->begin);
315 dbg "Waiting 5 seconds before opening serial port";
319 dbg "Opening Serial port";
320 $ser = doopen($devname);
335 my $loo = substr $blk,0,3;
336 unless ( $loo eq 'LOO') {
337 dbg "Block invalid loo -> $loo" if isdbg 'chan'; return;
345 my $crc_calc = CRC_CCITT($blk);
350 $tmp = unpack("s", substr $blk,7,2) / 1000;
351 $h{Pressure} = nearest(0.1, in2mb($tmp));
353 $tmp = unpack("s", substr $blk,9,2) / 10;
354 $h{Temp_In} = nearest(0.1, f2c($tmp));
356 $temp = nearest(0.1, f2c(unpack("s", substr $blk,12,2) / 10));
357 $h{Temp_Out} = $temp;
358 if ($temp > 60 || $temp < -60) {
359 dbg "LOOP Temperature out of range ($temp), record ignored";
363 $tmp = unpack("C", substr $blk,14,1);
364 $h{Wind} = nearest(0.1, mph2mps($tmp));
365 $h{Dir} = unpack("s", substr $blk,16,2)+0;
367 my $wind = {w => $h{Wind}, d => $h{Dir}};
368 $wind = 0 if $wind == 255;
369 push @{$ld->{wind_min}}, $wind;
371 $tmp = int(unpack("C", substr $blk,33,1)+0);
373 dbg "LOOP Outside Humidity out of range ($tmp), record ignored";
376 $h{Humidity_Out} = $tmp;
377 $tmp = int(unpack("C", substr $blk,11,1)+0);
379 dbg "LOOP Inside Humidity out of range ($tmp), record ignored";
382 $h{Humidity_In} = $tmp;
385 $tmp = unpack("C", substr $blk,43,1)+0;
386 $h{UV} = $tmp unless $tmp >= 255;
387 $tmp = unpack("s", substr $blk,44,2)+0; # watt/m**2
388 $h{Solar} = $tmp unless $tmp >= 32767;
390 # $h{Rain_Rate} = nearest(0.1,unpack("s", substr $blk,41,2) * $rain_mult);
391 $rain = $h{Rain_Day} = nearest(0.1, unpack("s", substr $blk,50,2) * $rain_mult);
392 my $delta_rain = $h{Rain} = nearest(0.1, ($rain >= $ld->{last_rain} ? $rain - $ld->{last_rain} : $rain)) if $loop_count;
393 $ld->{last_rain} = $rain;
395 # what sort of packet is it?
396 my $sort = unpack("C", substr $blk,4,1);
400 $tmp = unpack("C", substr $blk,18,2);
401 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp/10));
402 $tmp = unpack("C", substr $blk,20,2);
403 # $h{Wind_Avg_2} = nearest(0.1,mph2mps($tmp/10));
404 $tmp = unpack("C", substr $blk,22,2);
405 # $h{Wind_Gust_10} = nearest(0.1,mph2mps($tmp/10));
407 # $h{Dir_Avg_10} = unpack("C", substr $blk,24,2)+0;
408 $tmp = unpack("C", substr $blk,30,2);
409 $h{Dew_Point} = nearest(0.1, f2c($tmp));
414 $tmp = unpack("C", substr $blk,15,1);
415 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp));
416 $h{Dew_Point} = nearest(0.1, dew_point($h{Temp_Out}, $h{Humidity_Out}));
417 $h{Rain_Month} = nearest(0.1, unpack("s", substr $blk,52,2) * $rain_mult);
418 $h{Rain_Year} = nearest(0.1, unpack("s", substr $blk,54,2) * $rain_mult);
423 my $dayno = int($ts/86400);
427 if ($dayno > $ld->{last_day}) {
428 $ld->{Wind_Max} = $wind->{w};
429 $ld->{Temp_Out_Max} = $ld->{Temp_Out_Min} = $temp;
430 $ld->{Temp_Out_Max_T} = $ld->{Temp_Out_Min_T} = $ld->{Wind_Max_T} = clocktime($ts, 0);
431 $ld->{last_day} = $dayno;
435 if ($temp > $ld->{Temp_Out_Max}) {
436 $h{Temp_Out_Max} = $ld->{Temp_Out_Max} = $temp;
437 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T} = clocktime($ts, 0);
440 if ($temp < $ld->{Temp_Out_Min}) {
441 $h{Temp_Out_Min} = $ld->{Temp_Out_Min} = $temp;
442 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
446 if ($wind->{w} > $ld->{Wind_Max}) {
447 $h{Wind_Max} = $ld->{Wind_Max} = $wind->{w};
448 $h{Wind_Max_T} = $ld->{Wind_Max_T} = clocktime($ts, 0);
453 if ($ts >= $ld->{last_hour} + 1800) {
454 $h{Pressure_Trend} = unpack("C", substr $blk,3,1);
455 $h{Pressure_Trend_txt} = $bar_trend{$h{Pressure_Trend}};
456 $h{Batt_TX_OK} = (unpack("C", substr $blk,86,1)+0) ^ 1;
457 $h{Batt_Console} = nearest(0.01, unpack("s", substr $blk,87,2) * 0.005859375);
458 $h{Forecast_Icon} = unpack("C", substr $blk,89,1);
459 $h{Forecast_Rule} = unpack("C", substr $blk,90,1);
460 $h{Sunrise} = sprintf( "%04d", unpack("S", substr $blk,91,2) );
461 $h{Sunrise} =~ s/(\d{2})(\d{2})/$1:$2/;
462 $h{Sunset} = sprintf( "%04d", unpack("S", substr $blk,93,2) );
463 $h{Sunset} =~ s/(\d{2})(\d{2})/$1:$2/;
465 if ($loop_count) { # i.e not the first
466 my $a = wind_average(scalar @{$ld->{wind_hour}} ? @{$ld->{wind_hour}} : {w => $h{Wind}, d => $h{Dir}});
468 $h{Wind_1h} = nearest(0.1, $a->{w});
469 $h{Dir_1h} = nearest(0.1, $a->{d});
471 $a = wind_average(@{$ld->{wind_min}});
472 $h{Wind_1m} = nearest(0.1, $a->{w});
473 $h{Dir_1m} = nearest(1, $a->{d});
475 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
478 $ld->{last_rain_min} = $ld->{last_rain_hour} = $rain;
479 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
480 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
481 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
482 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
483 $h{Wind_Max} = $ld->{Wind_Max};
484 $h{Wind_Max_T} = $ld->{Wind_Max_T};
487 $s = genstr($ts, 'h', \%h);
488 $ld->{lasthour_h} = $s;
490 $ld->{last_hour} = int($ts/1800)*1800;
491 $ld->{last_min} = int($ts/60)*60;
492 @{$ld->{wind_hour}} = ();
493 @{$ld->{wind_min}} = ();
497 push @last5daysh, $s;
498 shift @last5daysh if @last5daysh > 5*24;
502 } elsif ($ts >= $ld->{last_min} + 60) {
503 my $a = wind_average(@{$ld->{wind_min}});
506 push @{$ld->{wind_hour}}, $a;
508 if ($loop_count) { # i.e not the first
511 $h{Wind_1m} = nearest(0.1, $a->{w});
512 $h{Dir_1m} = nearest(1, $a->{d});
513 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
515 my $wkph = $a->{w} * 3.6;
516 $h{WindChill} = nearest(0.1, $a->{w} >= 1.2 ? 13.12 + 0.6215 * $temp - 11.37 * $wkph ** 0.16 + 0.3965 * $temp * $wkph ** 0.16 : $temp);
518 $ld->{last_rain_min} = $rain;
519 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
520 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
521 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
522 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
523 $h{Wind_Max} = $ld->{Wind_Max};
524 $h{Wind_Max_T} = $ld->{Wind_Max_T};
527 $s = genstr($ts, 'm', \%h);
528 $ld->{lastmin_h} = $s;
530 $ld->{last_min} = int($ts/60)*60;
531 @{$ld->{wind_min}} = ();
533 output_str($s, 1) if $s;
537 my $o = gen_hash_diff($ld->{last_h}, \%h);
539 $o->{Dir} ||= $h{Dir};
540 $o->{Wind} ||= $h{Wind};
543 $s = genstr($ts, 'r', $o);
544 push @last10minsr, $s;
545 shift @last10minsr while @last10minsr > ($windmins * $updatepermin);
548 dbg "loop rec not changed" if isdbg 'chan';
550 output_str($s, 0) if $s;
553 write_ld() if $writeld;
554 cycle_loop_data_files() if $cycledata;
557 dbg "CRC check failed for LOOP data!";
568 my $j = $json->encode($h);
569 my $tm = clocktime($ts, 1);
570 return qq|{"tm":"$tm","t":$ts,"$let":$j}|;
577 my ($sec,$min,$hr) = (gmtime $ts)[0,1,2];
580 $s = sprintf "%02d:%02d:%02d", $hr, $min, $sec;
582 $s = sprintf "%02d:%02d", $hr, $min;
594 $dlog->writenow($s) if $logit;
595 foreach my $ws (keys $WS) {
612 while (my ($k, $v) = each %$now) {
613 if (!exists $last->{$k} || $last->{$k} ne $now->{$k}) {
618 return $count ? \%o : undef;
626 # Using the simplified approximation for dew point
627 # Accurate to 1 degree C for humidities > 50 %
628 # http://en.wikipedia.org/wiki/Dew_point
630 my $dewpoint = $temp - ((100 - $rh) / 5);
632 # this is the more complete one (which doesn't work)
636 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
637 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
644 # Expects packed data...
645 my $data_str = shift @_;
648 my @lst = split //, $data_str;
649 foreach my $data (@lst) {
650 my $data = unpack("c",$data);
653 my $index = $crc >> 8 ^ $data;
654 my $lhs = $crc_table[$index];
655 #print "lhs=$lhs, crc=$crc\n";
656 my $rhs = ($crc << 8) & 0xFFFF;
667 return ($_[0] - 32) * 5/9;
672 return $_[0] * 0.44704;
677 return $_[0] * 33.8637526;
682 my ($sindir, $cosdir, $wind);
687 $sindir += sin(d2r($r->{d})) * $r->{w};
688 $cosdir += cos(d2r($r->{d})) * $r->{w};
692 my $avhdg = r2d(atan2($sindir, $cosdir));
693 $avhdg += 360 if $avhdg < 0;
694 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
701 return ($n / pi) * 180;
708 return ($n / 180) * pi;
715 $ld->{rain24} ||= [];
717 my $Rain_1h = nearest(0.1, $rain >= $ld->{last_rain_hour} ? $rain - $ld->{last_rain_hour} : $rain); # this is the rate for this hour, so far
718 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
719 my $Rain_1m = nearest(0.1, $rm);
720 push @{$ld->{rain24}}, $Rain_1m;
721 $ld->{rain_24} += $rm;
722 while (@{$ld->{rain24}} > 24*60) {
723 $ld->{rain_24} -= shift @{$ld->{rain24}};
725 my $Rain_24h = nearest(0.1, $ld->{rain_24});
726 return ($Rain_1m, $Rain_1h, $Rain_24h);
732 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
733 $dataf->autoflush(1);
739 dbg "read loop data: $s" if isdbg 'json';
740 $ld = $json->decode($s) if length $s;
742 # sort out rain stats
744 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
745 my $diff = 24*60 - $c;
746 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
751 $rain += $_ for @{$ld->{rain24}};
754 $ld->{rain_24} = nearest(0.1, $rain);
762 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
763 $dataf->autoflush(1);
769 my $s = $json->encode($ld);
770 dbg "write loop data: $s" if isdbg 'json';
774 sub cycle_loop_data_files
776 $dataf->close if $dataf;
779 rename "$datafn.oooo", "$datafn.ooooo";
780 rename "$datafn.ooo", "$datafn.oooo";
781 rename "$datafn.oo", "$datafn.ooo";
782 rename "$datafn.o", "$datafn.oo";
783 copy $datafn, "$datafn.o";
790 my $start = shift || time - 86400;
794 if ($lg->open($dayno, 'r+')) {
795 while (my $l = $lg->read) {
796 next unless $l =~ /,"$let":/;
797 my ($t) = $l =~ /"t":(\d+)/;
798 if ($t && $t >= $start) {