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(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 > 75 || $temp < -75) {
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);
424 if ($dayno > $ld->{last_day}) {
425 $ld->{Temp_Out_Max} = $ld->{Temp_Out_Min} = $temp;
426 $ld->{Temp_Out_Max_T} = $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
427 $ld->{last_day} = $dayno;
429 cycle_loop_data_files();
431 if ($temp > $ld->{Temp_Out_Max}) {
432 $ld->{Temp_Out_Max} = $temp;
433 $ld->{Temp_Out_Max_T} = clocktime($ts, 0);
436 if ($temp < $ld->{Temp_Out_Min}) {
437 $ld->{Temp_Out_Min} = $temp;
438 $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
442 if ($ts >= $ld->{last_hour} + 1800) {
443 $h{Pressure_Trend} = unpack("C", substr $blk,3,1);
444 $h{Pressure_Trend_txt} = $bar_trend{$h{Pressure_Trend}};
445 $h{Batt_TX_OK} = (unpack("C", substr $blk,86,1)+0) ^ 1;
446 $h{Batt_Console} = nearest(0.01, unpack("s", substr $blk,87,2) * 0.005859375);
447 $h{Forecast_Icon} = unpack("C", substr $blk,89,1);
448 $h{Forecast_Rule} = unpack("C", substr $blk,90,1);
449 $h{Sunrise} = sprintf( "%04d", unpack("S", substr $blk,91,2) );
450 $h{Sunrise} =~ s/(\d{2})(\d{2})/$1:$2/;
451 $h{Sunset} = sprintf( "%04d", unpack("S", substr $blk,93,2) );
452 $h{Sunset} =~ s/(\d{2})(\d{2})/$1:$2/;
453 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
454 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
455 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
456 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
459 if ($loop_count) { # i.e not the first
460 my $a = wind_average(scalar @{$ld->{wind_hour}} ? @{$ld->{wind_hour}} : {w => $h{Wind}, d => $h{Dir}});
462 $h{Wind_1h} = nearest(0.1, $a->{w});
463 $h{Dir_1h} = nearest(0.1, $a->{d});
465 $a = wind_average(@{$ld->{wind_min}});
466 $h{Wind_1m} = nearest(0.1, $a->{w});
467 $h{Dir_1m} = nearest(1, $a->{d});
469 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
471 $ld->{last_rain_min} = $ld->{last_rain_hour} = $rain;
474 $s = genstr($ts, 'h', \%h);
475 $ld->{lasthour_h} = $s;
477 $ld->{last_hour} = int($ts/1800)*1800;
478 $ld->{last_min} = int($ts/60)*60;
479 @{$ld->{wind_hour}} = ();
480 @{$ld->{wind_min}} = ();
484 push @last5daysh, $s;
485 shift @last5daysh if @last5daysh > 5*24;
489 } elsif ($ts >= $ld->{last_min} + 60) {
490 my $a = wind_average(@{$ld->{wind_min}});
493 push @{$ld->{wind_hour}}, $a;
495 if ($loop_count) { # i.e not the first
498 $h{Wind_1m} = nearest(0.1, $a->{w});
499 $h{Dir_1m} = nearest(1, $a->{d});
500 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
502 $ld->{last_rain_min} = $rain;
504 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
505 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
506 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
507 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
510 $s = genstr($ts, 'm', \%h);
511 $ld->{lastmin_h} = $s;
513 $ld->{last_min} = int($ts/60)*60;
514 @{$ld->{wind_min}} = ();
516 output_str($s, 1) if $s;
520 my $o = gen_hash_diff($ld->{last_h}, \%h);
522 $o->{Dir} ||= $h{Dir};
523 $o->{Wind} ||= $h{Wind};
526 $s = genstr($ts, 'r', $o);
527 push @last10minsr, $s;
528 shift @last10minsr while @last10minsr > ($windmins * $updatepermin);
531 dbg "loop rec not changed" if isdbg 'chan';
533 output_str($s, 0) if $s;
538 dbg "CRC check failed for LOOP data!";
549 my $j = $json->encode($h);
550 my $tm = clocktime($ts, 1);
551 return qq|{"tm":"$tm","t":$ts,"$let":$j}|;
558 my ($sec,$min,$hr) = (gmtime $ts)[0,1,2];
561 $s = sprintf "%02d:%02d:%02d", $hr, $min, $sec;
563 $s = sprintf "%02d:%02d", $hr, $min;
575 $dlog->writenow($s) if $logit;
576 foreach my $ws (keys $WS) {
593 while (my ($k, $v) = each %$now) {
594 if (!exists $last->{$k} || $last->{$k} ne $now->{$k}) {
599 return $count ? \%o : undef;
607 # Using the simplified approximation for dew point
608 # Accurate to 1 degree C for humidities > 50 %
609 # http://en.wikipedia.org/wiki/Dew_point
611 my $dewpoint = $temp - ((100 - $rh) / 5);
613 # this is the more complete one (which doesn't work)
617 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
618 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
625 # Expects packed data...
626 my $data_str = shift @_;
629 my @lst = split //, $data_str;
630 foreach my $data (@lst) {
631 my $data = unpack("c",$data);
634 my $index = $crc >> 8 ^ $data;
635 my $lhs = $crc_table[$index];
636 #print "lhs=$lhs, crc=$crc\n";
637 my $rhs = ($crc << 8) & 0xFFFF;
648 return ($_[0] - 32) * 5/9;
653 return $_[0] * 0.44704;
658 return $_[0] * 33.8637526;
663 my ($sindir, $cosdir, $wind);
668 $sindir += sin(d2r($r->{d})) * $r->{w};
669 $cosdir += cos(d2r($r->{d})) * $r->{w};
673 my $avhdg = r2d(atan2($sindir, $cosdir));
674 $avhdg += 360 if $avhdg < 0;
675 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
682 return ($n / pi) * 180;
689 return ($n / 180) * pi;
696 $ld->{rain24} ||= [];
698 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
699 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
700 my $Rain_1m = nearest(0.1, $rm);
701 push @{$ld->{rain24}}, $Rain_1m;
702 $ld->{rain_24} += $rm;
703 while (@{$ld->{rain24}} > 24*60) {
704 $ld->{rain_24} -= shift @{$ld->{rain24}};
706 my $Rain_24h = nearest(0.1, $ld->{rain_24});
707 return ($Rain_1m, $Rain_1h, $Rain_24h);
713 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
714 $dataf->autoflush(1);
720 dbg "read loop data: $s" if isdbg 'json';
721 $ld = $json->decode($s) if length $s;
723 # sort out rain stats
725 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
726 my $diff = 24*60 - $c;
727 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
732 $rain += $_ for @{$ld->{rain24}};
735 $ld->{rain_24} = nearest(0.1, $rain);
743 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
744 $dataf->autoflush(1);
750 my $s = $json->encode($ld);
751 dbg "write loop data: $s" if isdbg 'json';
755 sub cycle_loop_data_files
757 $dataf->close if $dataf;
760 rename "$datafn.oooo", "$datafn.ooooo";
761 rename "$datafn.ooo", "$datafn.oooo";
762 rename "$datafn.oo", "$datafn.ooo";
763 rename "$datafn.o", "$datafn.oo";
764 copy $datafn, "$datafn.o";
771 my $start = shift || time - 86400;
775 if ($lg->open($dayno, 'r+')) {
776 while (my $l = $lg->read) {
777 next unless $l =~ /,"$let":/;
778 my ($t) = $l =~ /"t":(\d+)/;
779 if ($t && $t >= $start) {