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->{Temp_Out_Max} = $ld->{Temp_Out_Min} = $ld->{Wind_Max} = $temp;
429 $ld->{Temp_Out_Max_T} = $ld->{Temp_Out_Min_T} = $ld->{Wind_Max_T} = clocktime($ts, 0);
430 $ld->{last_day} = $dayno;
434 if ($temp > $ld->{Temp_Out_Max}) {
435 $h{Temp_Out_Max} = $ld->{Temp_Out_Max} = $temp;
436 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T} = clocktime($ts, 0);
439 if ($temp < $ld->{Temp_Out_Min}) {
440 $h{Temp_Out_Min} = $ld->{Temp_Out_Min} = $temp;
441 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
445 if ($wind->{w} > $ld->{Wind_Max}) {
446 $h{Wind_Max} = $ld->{Wind_Max} = $wind->{w};
447 $h{Wind_Max_T} = $ld->{Wind_Max_T} = clocktime($ts, 0);
451 my $wkph = $wind->{w} * 3.6;
452 $h{WindChill} = nearest(0.1, $wind->{w} >= 1.2 ? 13.12 + 0.6215 * $temp - 11.37 * $wkph ** 0.16 + 0.3965 * $temp * $wkph ** 0.16 : $temp);
454 if ($ts >= $ld->{last_hour} + 1800) {
455 $h{Pressure_Trend} = unpack("C", substr $blk,3,1);
456 $h{Pressure_Trend_txt} = $bar_trend{$h{Pressure_Trend}};
457 $h{Batt_TX_OK} = (unpack("C", substr $blk,86,1)+0) ^ 1;
458 $h{Batt_Console} = nearest(0.01, unpack("s", substr $blk,87,2) * 0.005859375);
459 $h{Forecast_Icon} = unpack("C", substr $blk,89,1);
460 $h{Forecast_Rule} = unpack("C", substr $blk,90,1);
461 $h{Sunrise} = sprintf( "%04d", unpack("S", substr $blk,91,2) );
462 $h{Sunrise} =~ s/(\d{2})(\d{2})/$1:$2/;
463 $h{Sunset} = sprintf( "%04d", unpack("S", substr $blk,93,2) );
464 $h{Sunset} =~ s/(\d{2})(\d{2})/$1:$2/;
466 if ($loop_count) { # i.e not the first
467 my $a = wind_average(scalar @{$ld->{wind_hour}} ? @{$ld->{wind_hour}} : {w => $h{Wind}, d => $h{Dir}});
469 $h{Wind_1h} = nearest(0.1, $a->{w});
470 $h{Dir_1h} = nearest(0.1, $a->{d});
472 $a = wind_average(@{$ld->{wind_min}});
473 $h{Wind_1m} = nearest(0.1, $a->{w});
474 $h{Dir_1m} = nearest(1, $a->{d});
476 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
479 $ld->{last_rain_min} = $ld->{last_rain_hour} = $rain;
480 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
481 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
482 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
483 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
484 $h{Wind_Max} = $ld->{Wind_Max};
485 $h{Wind_Max_T} = $ld->{Wind_Max_T};
488 $s = genstr($ts, 'h', \%h);
489 $ld->{lasthour_h} = $s;
491 $ld->{last_hour} = int($ts/1800)*1800;
492 $ld->{last_min} = int($ts/60)*60;
493 @{$ld->{wind_hour}} = ();
494 @{$ld->{wind_min}} = ();
498 push @last5daysh, $s;
499 shift @last5daysh if @last5daysh > 5*24;
503 } elsif ($ts >= $ld->{last_min} + 60) {
504 my $a = wind_average(@{$ld->{wind_min}});
507 push @{$ld->{wind_hour}}, $a;
509 if ($loop_count) { # i.e not the first
512 $h{Wind_1m} = nearest(0.1, $a->{w});
513 $h{Dir_1m} = nearest(1, $a->{d});
514 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
516 $ld->{last_rain_min} = $rain;
517 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
518 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
519 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
520 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
521 $h{Wind_Max} = $ld->{Wind_Max};
522 $h{Wind_Max_T} = $ld->{Wind_Max_T};
525 $s = genstr($ts, 'm', \%h);
526 $ld->{lastmin_h} = $s;
528 $ld->{last_min} = int($ts/60)*60;
529 @{$ld->{wind_min}} = ();
531 output_str($s, 1) if $s;
535 my $o = gen_hash_diff($ld->{last_h}, \%h);
537 $o->{Dir} ||= $h{Dir};
538 $o->{Wind} ||= $h{Wind};
541 $s = genstr($ts, 'r', $o);
542 push @last10minsr, $s;
543 shift @last10minsr while @last10minsr > ($windmins * $updatepermin);
546 dbg "loop rec not changed" if isdbg 'chan';
548 output_str($s, 0) if $s;
551 write_ld() if $writeld;
552 cycle_loop_data_files() if $cycledata;
555 dbg "CRC check failed for LOOP data!";
566 my $j = $json->encode($h);
567 my $tm = clocktime($ts, 1);
568 return qq|{"tm":"$tm","t":$ts,"$let":$j}|;
575 my ($sec,$min,$hr) = (gmtime $ts)[0,1,2];
578 $s = sprintf "%02d:%02d:%02d", $hr, $min, $sec;
580 $s = sprintf "%02d:%02d", $hr, $min;
592 $dlog->writenow($s) if $logit;
593 foreach my $ws (keys $WS) {
610 while (my ($k, $v) = each %$now) {
611 if (!exists $last->{$k} || $last->{$k} ne $now->{$k}) {
616 return $count ? \%o : undef;
624 # Using the simplified approximation for dew point
625 # Accurate to 1 degree C for humidities > 50 %
626 # http://en.wikipedia.org/wiki/Dew_point
628 my $dewpoint = $temp - ((100 - $rh) / 5);
630 # this is the more complete one (which doesn't work)
634 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
635 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
642 # Expects packed data...
643 my $data_str = shift @_;
646 my @lst = split //, $data_str;
647 foreach my $data (@lst) {
648 my $data = unpack("c",$data);
651 my $index = $crc >> 8 ^ $data;
652 my $lhs = $crc_table[$index];
653 #print "lhs=$lhs, crc=$crc\n";
654 my $rhs = ($crc << 8) & 0xFFFF;
665 return ($_[0] - 32) * 5/9;
670 return $_[0] * 0.44704;
675 return $_[0] * 33.8637526;
680 my ($sindir, $cosdir, $wind);
685 $sindir += sin(d2r($r->{d})) * $r->{w};
686 $cosdir += cos(d2r($r->{d})) * $r->{w};
690 my $avhdg = r2d(atan2($sindir, $cosdir));
691 $avhdg += 360 if $avhdg < 0;
692 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
699 return ($n / pi) * 180;
706 return ($n / 180) * pi;
713 $ld->{rain24} ||= [];
715 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
716 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
717 my $Rain_1m = nearest(0.1, $rm);
718 push @{$ld->{rain24}}, $Rain_1m;
719 $ld->{rain_24} += $rm;
720 while (@{$ld->{rain24}} > 24*60) {
721 $ld->{rain_24} -= shift @{$ld->{rain24}};
723 my $Rain_24h = nearest(0.1, $ld->{rain_24});
724 return ($Rain_1m, $Rain_1h, $Rain_24h);
730 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
731 $dataf->autoflush(1);
737 dbg "read loop data: $s" if isdbg 'json';
738 $ld = $json->decode($s) if length $s;
740 # sort out rain stats
742 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
743 my $diff = 24*60 - $c;
744 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
749 $rain += $_ for @{$ld->{rain24}};
752 $ld->{rain_24} = nearest(0.1, $rain);
760 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
761 $dataf->autoflush(1);
767 my $s = $json->encode($ld);
768 dbg "write loop data: $s" if isdbg 'json';
772 sub cycle_loop_data_files
774 $dataf->close if $dataf;
777 rename "$datafn.oooo", "$datafn.ooooo";
778 rename "$datafn.ooo", "$datafn.oooo";
779 rename "$datafn.oo", "$datafn.ooo";
780 rename "$datafn.o", "$datafn.oo";
781 copy $datafn, "$datafn.o";
788 my $start = shift || time - 86400;
792 if ($lg->open($dayno, 'r+')) {
793 while (my $l = $lg->read) {
794 next unless $l =~ /,"$let":/;
795 my ($t) = $l =~ /"t":(\d+)/;
796 if ($t && $t >= $start) {