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 my $ser; # the serial port Mojo::IOLoop::Stream
40 our $json = JSON->new->canonical(1);
41 our $WS = {}; # websocket connections
44 our @last10minsr = ();
46 our $windmins = 2; # no of minutes of wind data for the windrose
47 our $histdays = 5; # no of days of (half)hour data to search for main graph
48 our $updatepermin = 60 / 2.5; # no of updates per minute
50 our $loop_count; # how many LOOPs we have done, used as start indicator
53 0x0, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
54 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
55 0x1231, 0x210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
56 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
57 0x2462, 0x3443, 0x420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
58 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
59 0x3653, 0x2672, 0x1611, 0x630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
60 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
61 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x840, 0x1861, 0x2802, 0x3823,
62 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
63 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0xa50, 0x3a33, 0x2a12,
64 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
65 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0xc60, 0x1c41,
66 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
67 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0xe70,
68 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
69 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
70 0x1080, 0xa1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
71 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
72 0x2b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
73 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
74 0x34e2, 0x24c3, 0x14a0, 0x481, 0x7466, 0x6447, 0x5424, 0x4405,
75 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
76 0x26d3, 0x36f2, 0x691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
77 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
78 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x8e1, 0x3882, 0x28a3,
79 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
80 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0xaf1, 0x1ad0, 0x2ab3, 0x3a92,
81 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
82 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0xcc1,
83 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
84 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0xed1, 0x1ef0
89 $bar_trend{-60} = "Falling Rapidly";
90 $bar_trend{196} = "Falling Rapidly";
91 $bar_trend{-20} = "Falling Slowly";
92 $bar_trend{236} = "Falling Slowly";
93 $bar_trend{0} = "Steady";
94 $bar_trend{20} = "Rising Slowly";
95 $bar_trend{60} = "Rising Rapidly";
99 $SIG{TERM} = $SIG{INT} = sub {++$ending; Mojo::IOLoop->stop;};
100 $SIG{HUP} = 'IGNORE';
103 # WebSocket weather service
104 websocket '/weather' => sub {
110 app->log->debug('WebSocket opened.');
111 dbg 'WebSocket opened' if isdbg 'chan';
114 # send historical data
115 $c->send($ld->{lasthour_h}) if exists $ld->{lasthour_h};
116 $c->send($ld->{lastmin_h}) if exists $ld->{lastmin_h};
119 $c->inactivity_timeout(3615);
125 dbg "websocket: text $msg" if isdbg 'chan';
129 dbg "websocket: json $msg" if isdbg 'chan';
134 $c->on(finish => sub {
135 my ($c, $code, $reason) = @_;
136 app->log->debug("WebSocket closed with status $code.");
137 dbg "websocket closed with status $code" if isdbg 'chan';
142 get '/' => {template => 'index'};
152 dbg "*** starting $0";
158 my $dayno = int ($tnow/86400);
159 for (my $i = 0-$histdays; $i < 0; ++$i ) {
160 push @last5daysh, grab_history(SMGLog->new("day"), "h", $tnow-(86400*$histdays), $dayno+$i+1);
162 @last10minsr = map {my ($t, $js) = split(/\s/, $_, 2); $js} grab_history(SMGLog->new("debug"), "r", $tnow-(60*$windmins), $dayno);
163 dbg sprintf("last5days = %d last10mins = %d", scalar @last5daysh, scalar @last10minsr);
165 sysopen(R, $randomfn, 0) or die "cannot open $randomfn $!\n";
167 sysread(R, $rs, 32) or die "not enough randomness available\n";
170 app->secrets([qw(Here's something that's really seakrett), $rs]);
172 our $dlog = SMGLog->new("day");
173 dbg "before next tick";
174 Mojo::IOLoop->next_tick(sub { loop() });
175 dbg "before app start";
177 dbg "after app start";
180 $dataf->close if $dataf;
184 # move all the files along one
185 cycle_loop_data_files();
193 ##################################################################################
197 dbg "last_min: " . scalar gmtime($ld->{last_min});
198 dbg "last_hour: " . scalar gmtime($ld->{last_hour});
200 $did = Mojo::IOLoop->recurring(1 => sub {$dlog->flushall});
211 $d =~ s/([\%\x00-\x1f\x7f-\xff])/sprintf("%%%02X", ord($1))/eg;
212 dbg "read added '$d' buf lth=" . length $buf if isdbg 'raw';
213 if ($state eq 'waitnl' && $buf =~ /[\cJ\cM]+/) {
214 dbg "Got \\n" if isdbg 'state';
215 Mojo::IOLoop->remove($tid) if $tid;
219 $ser->write("LPS 1 1\n");
220 chgstate("waitloop");
221 } elsif ($state eq "waitloop") {
222 if ($buf =~ /\x06/) {
223 dbg "Got ACK 0x06" if isdbg 'state';
224 chgstate('waitlooprec');
227 } elsif ($state eq 'waitlooprec') {
228 if (length $buf >= 99) {
229 dbg "got loop record" if isdbg 'chan';
240 dbg "start_loop writing $nlcount \\n" if isdbg 'state';
242 Mojo::IOLoop->remove($tid) if $tid;
244 $tid = Mojo::IOLoop->recurring(0.6 => sub {
245 if (++$nlcount > 10) {
246 dbg "\\n count > 10, closing connection" if isdbg 'chan';
250 dbg "writing $nlcount \\n" if isdbg 'state';
258 dbg "state '$state' -> '$_[0]'" if isdbg 'state';
265 dbg "do reopen on '$name' ending $ending";
267 $ser = do_open($name);
271 Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
282 my $ob = Serial->new($name, 19200) || die "$name $!\n";
283 dbg "streaming $name fileno(" . fileno($ob) . ")" if isdbg 'chan';
285 my $ser = Mojo::IOLoop::Stream->new($ob);
286 $ser->on(error=>sub {dbg "serial $_[1]"; do_reopen($name) unless $ending});
287 $ser->on(close=>sub {dbg "serial closing"; do_reopen($name) unless $ending});
288 $ser->on(timeout=>sub {dbg "serial timeout";});
289 $ser->on(read=>sub {on_read(@_)});
292 Mojo::IOLoop->remove($tid) if $tid;
294 Mojo::IOLoop->remove($rid) if $rid;
296 $rid = Mojo::IOLoop->recurring($poll_interval => sub {
297 start_loop() if !$state;
311 my $loo = substr $blk,0,3;
312 unless ( $loo eq 'LOO') {
313 dbg "Block invalid loo -> $loo" if isdbg 'chan'; return;
321 my $crc_calc = CRC_CCITT($blk);
326 $tmp = unpack("s", substr $blk,7,2) / 1000;
327 $h{Pressure} = nearest(1, in2mb($tmp));
329 $tmp = unpack("s", substr $blk,9,2) / 10;
330 $h{Temp_In} = nearest(0.1, f2c($tmp));
332 $temp = nearest(0.1, f2c(unpack("s", substr $blk,12,2) / 10));
333 $h{Temp_Out} = $temp;
334 if ($temp > 75 || $temp < -75) {
335 dbg "LOOP Temperature out of range ($temp), record ignored";
339 $tmp = unpack("C", substr $blk,14,1);
340 $h{Wind} = nearest(0.1, mph2mps($tmp));
341 $h{Dir} = unpack("s", substr $blk,16,2)+0;
343 my $wind = {w => $h{Wind}, d => $h{Dir}};
344 $wind = 0 if $wind == 255;
345 push @{$ld->{wind_min}}, $wind;
347 $tmp = int(unpack("C", substr $blk,33,1)+0);
349 dbg "LOOP Outside Humidity out of range ($tmp), record ignored";
352 $h{Humidity_Out} = $tmp;
353 $tmp = int(unpack("C", substr $blk,11,1)+0);
355 dbg "LOOP Inside Humidity out of range ($tmp), record ignored";
358 $h{Humidity_In} = $tmp;
361 $tmp = unpack("C", substr $blk,43,1)+0;
362 $h{UV} = $tmp unless $tmp >= 255;
363 $tmp = unpack("s", substr $blk,44,2)+0; # watt/m**2
364 $h{Solar} = $tmp unless $tmp >= 32767;
366 # $h{Rain_Rate} = nearest(0.1,unpack("s", substr $blk,41,2) * $rain_mult);
367 $rain = $h{Rain_Day} = nearest(0.1, unpack("s", substr $blk,50,2) * $rain_mult);
368 my $delta_rain = $h{Rain} = nearest(0.1, ($rain >= $ld->{last_rain} ? $rain - $ld->{last_rain} : $rain)) if $loop_count;
369 $ld->{last_rain} = $rain;
371 # what sort of packet is it?
372 my $sort = unpack("C", substr $blk,4,1);
376 $tmp = unpack("C", substr $blk,18,2);
377 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp/10));
378 $tmp = unpack("C", substr $blk,20,2);
379 # $h{Wind_Avg_2} = nearest(0.1,mph2mps($tmp/10));
380 $tmp = unpack("C", substr $blk,22,2);
381 # $h{Wind_Gust_10} = nearest(0.1,mph2mps($tmp/10));
383 # $h{Dir_Avg_10} = unpack("C", substr $blk,24,2)+0;
384 $tmp = unpack("C", substr $blk,30,2);
385 $h{Dew_Point} = nearest(0.1, f2c($tmp));
390 $tmp = unpack("C", substr $blk,15,1);
391 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp));
392 $h{Dew_Point} = nearest(0.1, dew_point($h{Temp_Out}, $h{Humidity_Out}));
393 $h{Rain_Month} = nearest(0.1, unpack("s", substr $blk,52,2) * $rain_mult);
394 $h{Rain_Year} = nearest(0.1, unpack("s", substr $blk,54,2) * $rain_mult);
399 my $dayno = int($ts/86400);
400 if ($dayno > $ld->{last_day}) {
401 $ld->{Temp_Out_Max} = $ld->{Temp_Out_Min} = $temp;
402 $ld->{Temp_Out_Max_T} = $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
403 $ld->{last_day} = $dayno;
405 cycle_loop_data_files();
407 if ($temp > $ld->{Temp_Out_Max}) {
408 $ld->{Temp_Out_Max} = $temp;
409 $ld->{Temp_Out_Max_T} = clocktime($ts, 0);
412 if ($temp < $ld->{Temp_Out_Min}) {
413 $ld->{Temp_Out_Min} = $temp;
414 $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
418 if ($ts >= $ld->{last_hour} + 1800) {
419 $h{Pressure_Trend} = unpack("C", substr $blk,3,1);
420 $h{Pressure_Trend_txt} = $bar_trend{$h{Pressure_Trend}};
421 $h{Batt_TX_OK} = (unpack("C", substr $blk,86,1)+0) ^ 1;
422 $h{Batt_Console} = nearest(0.01, unpack("s", substr $blk,87,2) * 0.005859375);
423 $h{Forecast_Icon} = unpack("C", substr $blk,89,1);
424 $h{Forecast_Rule} = unpack("C", substr $blk,90,1);
425 $h{Sunrise} = sprintf( "%04d", unpack("S", substr $blk,91,2) );
426 $h{Sunrise} =~ s/(\d{2})(\d{2})/$1:$2/;
427 $h{Sunset} = sprintf( "%04d", unpack("S", substr $blk,93,2) );
428 $h{Sunset} =~ s/(\d{2})(\d{2})/$1:$2/;
429 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
430 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
431 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
432 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
435 if ($loop_count) { # i.e not the first
436 my $a = wind_average(scalar @{$ld->{wind_hour}} ? @{$ld->{wind_hour}} : {w => $h{Wind}, d => $h{Dir}});
438 $h{Wind_1h} = nearest(0.1, $a->{w});
439 $h{Dir_1h} = nearest(0.1, $a->{d});
441 $a = wind_average(@{$ld->{wind_min}});
442 $h{Wind_1m} = nearest(0.1, $a->{w});
443 $h{Dir_1m} = nearest(1, $a->{d});
445 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
447 $ld->{last_rain_min} = $ld->{last_rain_hour} = $rain;
450 $s = genstr($ts, 'h', \%h);
451 $ld->{lasthour_h} = $s;
453 $ld->{last_hour} = int($ts/1800)*1800;
454 $ld->{last_min} = int($ts/60)*60;
455 @{$ld->{wind_hour}} = ();
456 @{$ld->{wind_min}} = ();
460 push @last5daysh, $s;
461 shift @last5daysh if @last5daysh > 5*24;
465 } elsif ($ts >= $ld->{last_min} + 60) {
466 my $a = wind_average(@{$ld->{wind_min}});
469 push @{$ld->{wind_hour}}, $a;
471 if ($loop_count) { # i.e not the first
474 $h{Wind_1m} = nearest(0.1, $a->{w});
475 $h{Dir_1m} = nearest(1, $a->{d});
476 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
478 $ld->{last_rain_min} = $rain;
480 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
481 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
482 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
483 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
486 $s = genstr($ts, 'm', \%h);
487 $ld->{lastmin_h} = $s;
489 $ld->{last_min} = int($ts/60)*60;
490 @{$ld->{wind_min}} = ();
492 output_str($s, 1) if $s;
496 my $o = gen_hash_diff($ld->{last_h}, \%h);
498 $o->{Dir} ||= $h{Dir};
499 $o->{Wind} ||= $h{Wind};
502 $s = genstr($ts, 'r', $o);
503 push @last10minsr, $s;
504 shift @last10minsr while @last10minsr > ($windmins * $updatepermin);
507 dbg "loop rec not changed" if isdbg 'chan';
509 output_str($s, 0) if $s;
514 dbg "CRC check failed for LOOP data!";
525 my $j = $json->encode($h);
526 my $tm = clocktime($ts, 1);
527 return qq|{"tm":"$tm","t":$ts,"$let":$j}|;
534 my ($sec,$min,$hr) = (gmtime $ts)[0,1,2];
537 $s = sprintf "%02d:%02d:%02d", $hr, $min, $sec;
539 $s = sprintf "%02d:%02d", $hr, $min;
551 $dlog->writenow($s) if $logit;
552 foreach my $ws (keys $WS) {
569 while (my ($k, $v) = each %$now) {
570 if (!exists $last->{$k} || $last->{$k} ne $now->{$k}) {
575 return $count ? \%o : undef;
583 # Using the simplified approximation for dew point
584 # Accurate to 1 degree C for humidities > 50 %
585 # http://en.wikipedia.org/wiki/Dew_point
587 my $dewpoint = $temp - ((100 - $rh) / 5);
589 # this is the more complete one (which doesn't work)
593 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
594 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
601 # Expects packed data...
602 my $data_str = shift @_;
605 my @lst = split //, $data_str;
606 foreach my $data (@lst) {
607 my $data = unpack("c",$data);
610 my $index = $crc >> 8 ^ $data;
611 my $lhs = $crc_table[$index];
612 #print "lhs=$lhs, crc=$crc\n";
613 my $rhs = ($crc << 8) & 0xFFFF;
624 return ($_[0] - 32) * 5/9;
629 return $_[0] * 0.44704;
634 return $_[0] * 33.8637526;
639 my ($sindir, $cosdir, $wind);
644 $sindir += sin(d2r($r->{d})) * $r->{w};
645 $cosdir += cos(d2r($r->{d})) * $r->{w};
649 my $avhdg = r2d(atan2($sindir, $cosdir));
650 $avhdg += 360 if $avhdg < 0;
651 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
658 return ($n / pi) * 180;
665 return ($n / 180) * pi;
672 $ld->{rain24} ||= [];
674 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
675 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
676 my $Rain_1m = nearest(0.1, $rm);
677 push @{$ld->{rain24}}, $Rain_1m;
678 $ld->{rain_24} += $rm;
679 while (@{$ld->{rain24}} > 24*60) {
680 $ld->{rain_24} -= shift @{$ld->{rain24}};
682 my $Rain_24h = nearest(0.1, $ld->{rain_24});
683 return ($Rain_1m, $Rain_1h, $Rain_24h);
689 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
690 $dataf->autoflush(1);
696 dbg "read loop data: $s" if isdbg 'json';
697 $ld = $json->decode($s) if length $s;
699 # sort out rain stats
701 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
702 my $diff = 24*60 - $c;
703 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
708 $rain += $_ for @{$ld->{rain24}};
711 $ld->{rain_24} = nearest(0.1, $rain);
719 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
720 $dataf->autoflush(1);
726 my $s = $json->encode($ld);
727 dbg "write loop data: $s" if isdbg 'json';
731 sub cycle_loop_data_files
733 $dataf->close if $dataf;
736 rename "$datafn.oooo", "$datafn.ooooo";
737 rename "$datafn.ooo", "$datafn.oooo";
738 rename "$datafn.oo", "$datafn.ooo";
739 rename "$datafn.o", "$datafn.oo";
740 copy $datafn, "$datafn.o";
747 my $start = shift || time - 86400;
751 if ($lg->open($dayno, 'r+')) {
752 while (my $l = $lg->read) {
753 next unless $l =~ /,"$let":/;
754 my ($t) = $l =~ /"t":(\d+)/;
755 if ($t && $t >= $start) {