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 # we always send wind even if it hasn't changed in order to update the wind rose.
540 $o->{Dir} ||= ($h{Dir} + 0);
541 $o->{Wind} ||= ($h{Wind} + 0);
542 $s = genstr($ts, 'r', $o);
543 push @last10minsr, $s;
544 shift @last10minsr while @last10minsr > ($windmins * $updatepermin);
545 output_str($s, 0) if $s;
548 write_ld() if $writeld;
549 cycle_loop_data_files() if $cycledata;
552 dbg "CRC check failed for LOOP data!";
563 my $j = $json->encode($h);
564 my $tm = clocktime($ts, 1);
565 return qq|{"tm":"$tm","t":$ts,"$let":$j}|;
572 my ($sec,$min,$hr) = (gmtime $ts)[0,1,2];
575 $s = sprintf "%02d:%02d:%02d", $hr, $min, $sec;
577 $s = sprintf "%02d:%02d", $hr, $min;
589 $dlog->writenow($s) if $logit;
590 foreach my $ws (keys $WS) {
607 while (my ($k, $v) = each %$now) {
608 if (!exists $last->{$k} || $last->{$k} ne $now->{$k}) {
613 return $count ? \%o : undef;
621 # Using the simplified approximation for dew point
622 # Accurate to 1 degree C for humidities > 50 %
623 # http://en.wikipedia.org/wiki/Dew_point
625 my $dewpoint = $temp - ((100 - $rh) / 5);
627 # this is the more complete one (which doesn't work)
631 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
632 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
639 # Expects packed data...
640 my $data_str = shift @_;
643 my @lst = split //, $data_str;
644 foreach my $data (@lst) {
645 my $data = unpack("c",$data);
648 my $index = $crc >> 8 ^ $data;
649 my $lhs = $crc_table[$index];
650 #print "lhs=$lhs, crc=$crc\n";
651 my $rhs = ($crc << 8) & 0xFFFF;
662 return ($_[0] - 32) * 5/9;
667 return $_[0] * 0.44704;
672 return $_[0] * 33.8637526;
677 my ($sindir, $cosdir, $wind);
682 $sindir += sin(d2r($r->{d})) * $r->{w};
683 $cosdir += cos(d2r($r->{d})) * $r->{w};
687 my $avhdg = r2d(atan2($sindir, $cosdir));
688 $avhdg += 360 if $avhdg < 0;
689 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
696 return ($n / pi) * 180;
703 return ($n / 180) * pi;
710 $ld->{rain24} ||= [];
712 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
713 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
714 my $Rain_1m = nearest(0.1, $rm);
715 push @{$ld->{rain24}}, $Rain_1m;
716 $ld->{rain_24} += $rm;
717 while (@{$ld->{rain24}} > 24*60) {
718 $ld->{rain_24} -= shift @{$ld->{rain24}};
720 my $Rain_24h = nearest(0.1, $ld->{rain_24});
721 return ($Rain_1m, $Rain_1h, $Rain_24h);
727 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
728 $dataf->autoflush(1);
734 dbg "read loop data: $s" if isdbg 'json';
735 $ld = $json->decode($s) if length $s;
737 # sort out rain stats
739 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
740 my $diff = 24*60 - $c;
741 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
746 $rain += $_ for @{$ld->{rain24}};
749 $ld->{rain_24} = nearest(0.1, $rain);
757 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
758 $dataf->autoflush(1);
764 my $s = $json->encode($ld);
765 dbg "write loop data: $s" if isdbg 'json';
769 sub cycle_loop_data_files
771 $dataf->close if $dataf;
774 rename "$datafn.oooo", "$datafn.ooooo";
775 rename "$datafn.ooo", "$datafn.oooo";
776 rename "$datafn.oo", "$datafn.ooo";
777 rename "$datafn.o", "$datafn.oo";
778 copy $datafn, "$datafn.o";
785 my $start = shift || time - 86400;
789 if ($lg->open($dayno, 'r+')) {
790 while (my $l = $lg->read) {
791 next unless $l =~ /,"$let":/;
792 my ($t) = $l =~ /"t":(\d+)/;
793 if ($t && $t >= $start) {