11 use Mojo::IOLoop::Stream;
12 use Mojo::Transaction::WebSocket;
13 #use Mojo::JSON qw(decode_json encode_json);
17 use Math::Round qw(nearest);
19 use Data::Random qw(rand_chars);
22 use constant pi => 3.14159265358979;
24 my $devname = "/dev/davis";
25 my $datafn = ".loop_data";
28 our $poll_interval = 2.5;
29 our $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";
100 our $dlog = SMGLog->new("day");
102 my $dayno = int ($tnow/86400);
103 for (my $i = 0-$histdays; $i < 0; ++$i ) {
104 push @last5daysh, grab_history(SMGLog->new("day"), "h", $tnow-(86400*$histdays), $dayno+$i+1);
106 @last10minsr = map {my ($t, $js) = split(/\s/, $_, 2); $js} grab_history(SMGLog->new("debug"), "r", $tnow-(60*$windmins), $dayno);
107 dbg sprintf("last5days = %d last10mins = %d", scalar @last5daysh, scalar @last10minsr);
113 dbg "last_min: " . scalar gmtime($ld->{last_min});
114 dbg "last_hour: " . scalar gmtime($ld->{last_hour});
116 $did = Mojo::IOLoop->recurring(1 => sub {$dlog->flushall});
125 $d =~ s/([\%\x00-\x1f\x7f-\xff])/sprintf("%%%02X", ord($1))/eg;
126 dbg "read added '$d' buf lth=" . length $buf if isdbg 'raw';
127 if ($state eq 'waitnl' && $buf =~ /[\cJ\cM]+/) {
128 dbg "Got \\n" if isdbg 'state';
129 Mojo::IOLoop->remove($tid) if $tid;
133 $ser->write("LPS 1 1\n");
134 chgstate("waitloop");
135 } elsif ($state eq "waitloop") {
136 if ($buf =~ /\x06/) {
137 dbg "Got ACK 0x06" if isdbg 'state';
138 chgstate('waitlooprec');
141 } elsif ($state eq 'waitlooprec') {
142 if (length $buf >= 99) {
143 dbg "got loop record" if isdbg 'chan';
154 dbg "start_loop writing $nlcount \\n" if isdbg 'state';
156 Mojo::IOLoop->remove($tid) if $tid;
158 $tid = Mojo::IOLoop->recurring(0.6 => sub {
159 if (++$nlcount > 10) {
160 dbg "\\n count > 10, closing connection" if isdbg 'chan';
164 dbg "writing $nlcount \\n" if isdbg 'state';
172 dbg "state '$state' -> '$_[0]'" if isdbg 'state';
179 dbg "do reopen on '$name' ending $ending";
181 $ser = do_open($name);
185 Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
196 my $ob = Serial->new($name, 19200) || die "$name $!\n";
197 dbg "streaming $name fileno(" . fileno($ob) . ")" if isdbg 'chan';
199 my $ser = Mojo::IOLoop::Stream->new($ob);
200 $ser->on(error=>sub {dbg "serial $_[1]"; do_reopen($name) unless $ending});
201 $ser->on(close=>sub {dbg "serial closing"; do_reopen($name) unless $ending});
202 $ser->on(timeout=>sub {dbg "serial timeout";});
203 $ser->on(read=>sub {on_read(@_)});
206 Mojo::IOLoop->remove($tid) if $tid;
208 Mojo::IOLoop->remove($rid) if $rid;
210 $rid = Mojo::IOLoop->recurring($poll_interval => sub {
211 start_loop() if !$state;
225 my $loo = substr $blk,0,3;
226 unless ( $loo eq 'LOO') {
227 dbg "Block invalid loo -> $loo" if isdbg 'chan'; return;
235 my $crc_calc = CRC_CCITT($blk);
240 $tmp = unpack("s", substr $blk,7,2) / 1000;
241 $h{Pressure} = nearest(1, in2mb($tmp));
243 $tmp = unpack("s", substr $blk,9,2) / 10;
244 $h{Temp_In} = nearest(0.1, f2c($tmp));
246 $temp = nearest(0.1, f2c(unpack("s", substr $blk,12,2) / 10));
247 $h{Temp_Out} = $temp;
248 if ($temp > 75 || $temp < -75) {
249 dbg "LOOP Temperature out of range ($temp), record ignored";
253 $tmp = unpack("C", substr $blk,14,1);
254 $h{Wind} = nearest(0.1, mph2mps($tmp));
255 $h{Dir} = unpack("s", substr $blk,16,2)+0;
257 my $wind = {w => $h{Wind}, d => $h{Dir}};
258 $wind = 0 if $wind == 255;
259 push @{$ld->{wind_min}}, $wind;
261 $tmp = int(unpack("C", substr $blk,33,1)+0);
263 dbg "LOOP Outside Humidity out of range ($tmp), record ignored";
266 $h{Humidity_Out} = $tmp;
267 $tmp = int(unpack("C", substr $blk,11,1)+0);
269 dbg "LOOP Inside Humidity out of range ($tmp), record ignored";
272 $h{Humidity_In} = $tmp;
275 $tmp = unpack("C", substr $blk,43,1)+0;
276 $h{UV} = $tmp unless $tmp >= 255;
277 $tmp = unpack("s", substr $blk,44,2)+0; # watt/m**2
278 $h{Solar} = $tmp unless $tmp >= 32767;
280 # $h{Rain_Rate} = nearest(0.1,unpack("s", substr $blk,41,2) * $rain_mult);
281 $rain = $h{Rain_Day} = nearest(0.1, unpack("s", substr $blk,50,2) * $rain_mult);
282 my $delta_rain = $h{Rain} = nearest(0.1, ($rain >= $ld->{last_rain} ? $rain - $ld->{last_rain} : $rain)) if $loop_count;
283 $ld->{last_rain} = $rain;
285 # what sort of packet is it?
286 my $sort = unpack("C", substr $blk,4,1);
290 $tmp = unpack("C", substr $blk,18,2);
291 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp/10));
292 $tmp = unpack("C", substr $blk,20,2);
293 # $h{Wind_Avg_2} = nearest(0.1,mph2mps($tmp/10));
294 $tmp = unpack("C", substr $blk,22,2);
295 # $h{Wind_Gust_10} = nearest(0.1,mph2mps($tmp/10));
297 # $h{Dir_Avg_10} = unpack("C", substr $blk,24,2)+0;
298 $tmp = unpack("C", substr $blk,30,2);
299 $h{Dew_Point} = nearest(0.1, f2c($tmp));
304 $tmp = unpack("C", substr $blk,15,1);
305 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp));
306 $h{Dew_Point} = nearest(0.1, dew_point($h{Temp_Out}, $h{Humidity_Out}));
307 $h{Rain_Month} = nearest(0.1, unpack("s", substr $blk,52,2) * $rain_mult);
308 $h{Rain_Year} = nearest(0.1, unpack("s", substr $blk,54,2) * $rain_mult);
313 my $dayno = int($ts/86400);
314 if ($dayno > $ld->{last_day}) {
315 $ld->{Temp_Out_Max} = $ld->{Temp_Out_Min} = $temp;
316 $ld->{Temp_Out_Max_T} = $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
317 $ld->{last_day} = $dayno;
319 cycle_loop_data_files();
321 if ($temp > $ld->{Temp_Out_Max}) {
322 $ld->{Temp_Out_Max} = $temp;
323 $ld->{Temp_Out_Max_T} = clocktime($ts, 0);
326 if ($temp < $ld->{Temp_Out_Min}) {
327 $ld->{Temp_Out_Min} = $temp;
328 $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
332 if ($ts >= $ld->{last_hour} + 1800) {
333 $h{Pressure_Trend} = unpack("C", substr $blk,3,1);
334 $h{Pressure_Trend_txt} = $bar_trend{$h{Pressure_Trend}};
335 $h{Batt_TX_OK} = (unpack("C", substr $blk,86,1)+0) ^ 1;
336 $h{Batt_Console} = nearest(0.01, unpack("s", substr $blk,87,2) * 0.005859375);
337 $h{Forecast_Icon} = unpack("C", substr $blk,89,1);
338 $h{Forecast_Rule} = unpack("C", substr $blk,90,1);
339 $h{Sunrise} = sprintf( "%04d", unpack("S", substr $blk,91,2) );
340 $h{Sunrise} =~ s/(\d{2})(\d{2})/$1:$2/;
341 $h{Sunset} = sprintf( "%04d", unpack("S", substr $blk,93,2) );
342 $h{Sunset} =~ s/(\d{2})(\d{2})/$1:$2/;
343 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
344 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
345 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
346 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
349 if ($loop_count) { # i.e not the first
350 my $a = wind_average(scalar @{$ld->{wind_hour}} ? @{$ld->{wind_hour}} : {w => $h{Wind}, d => $h{Dir}});
352 $h{Wind_1h} = nearest(0.1, $a->{w});
353 $h{Dir_1h} = nearest(0.1, $a->{d});
355 $a = wind_average(@{$ld->{wind_min}});
356 $h{Wind_1m} = nearest(0.1, $a->{w});
357 $h{Dir_1m} = nearest(1, $a->{d});
359 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
361 $ld->{last_rain_min} = $ld->{last_rain_hour} = $rain;
364 $s = genstr($ts, 'h', \%h);
365 $ld->{lasthour_h} = $s;
367 $ld->{last_hour} = int($ts/1800)*1800;
368 $ld->{last_min} = int($ts/60)*60;
369 @{$ld->{wind_hour}} = ();
370 @{$ld->{wind_min}} = ();
374 push @last5daysh, $s;
375 shift @last5daysh if @last5daysh > 5*24;
379 } elsif ($ts >= $ld->{last_min} + 60) {
380 my $a = wind_average(@{$ld->{wind_min}});
383 push @{$ld->{wind_hour}}, $a;
385 if ($loop_count) { # i.e not the first
388 $h{Wind_1m} = nearest(0.1, $a->{w});
389 $h{Dir_1m} = nearest(1, $a->{d});
390 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
392 $ld->{last_rain_min} = $rain;
394 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
395 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
396 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
397 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
400 $s = genstr($ts, 'm', \%h);
401 $ld->{lastmin_h} = $s;
403 $ld->{last_min} = int($ts/60)*60;
404 @{$ld->{wind_min}} = ();
406 output_str($s, 1) if $s;
410 my $o = gen_hash_diff($ld->{last_h}, \%h);
412 $o->{Dir} ||= $h{Dir};
413 $o->{Wind} ||= $h{Wind};
416 $s = genstr($ts, 'r', $o);
417 push @last10minsr, $s;
418 shift @last10minsr while @last10minsr > ($windmins * $updatepermin);
421 dbg "loop rec not changed" if isdbg 'chan';
423 output_str($s, 0) if $s;
428 dbg "CRC check failed for LOOP data!";
439 my $j = $json->encode($h);
440 my $tm = clocktime($ts, 1);
441 return qq|{"tm":"$tm","t":$ts,"$let":$j}|;
448 my ($sec,$min,$hr) = (gmtime $ts)[0,1,2];
451 $s = sprintf "%02d:%02d:%02d", $hr, $min, $sec;
453 $s = sprintf "%02d:%02d", $hr, $min;
465 $dlog->writenow($s) if $logit;
466 foreach my $ws (keys $WS) {
483 while (my ($k, $v) = each %$now) {
484 if (!exists $last->{$k} || $last->{$k} ne $now->{$k}) {
489 return $count ? \%o : undef;
497 # Using the simplified approximation for dew point
498 # Accurate to 1 degree C for humidities > 50 %
499 # http://en.wikipedia.org/wiki/Dew_point
501 my $dewpoint = $temp - ((100 - $rh) / 5);
503 # this is the more complete one (which doesn't work)
507 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
508 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
515 # Expects packed data...
516 my $data_str = shift @_;
519 my @lst = split //, $data_str;
520 foreach my $data (@lst) {
521 my $data = unpack("c",$data);
524 my $index = $crc >> 8 ^ $data;
525 my $lhs = $crc_table[$index];
526 #print "lhs=$lhs, crc=$crc\n";
527 my $rhs = ($crc << 8) & 0xFFFF;
538 return ($_[0] - 32) * 5/9;
543 return $_[0] * 0.44704;
548 return $_[0] * 33.8637526;
553 my ($sindir, $cosdir, $wind);
558 $sindir += sin(d2r($r->{d})) * $r->{w};
559 $cosdir += cos(d2r($r->{d})) * $r->{w};
563 my $avhdg = r2d(atan2($sindir, $cosdir));
564 $avhdg += 360 if $avhdg < 0;
565 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
572 return ($n / pi) * 180;
579 return ($n / 180) * pi;
586 $ld->{rain24} ||= [];
588 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
589 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
590 my $Rain_1m = nearest(0.1, $rm);
591 push @{$ld->{rain24}}, $Rain_1m;
592 $ld->{rain_24} += $rm;
593 while (@{$ld->{rain24}} > 24*60) {
594 $ld->{rain_24} -= shift @{$ld->{rain24}};
596 my $Rain_24h = nearest(0.1, $ld->{rain_24});
597 return ($Rain_1m, $Rain_1h, $Rain_24h);
603 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
604 $dataf->autoflush(1);
610 dbg "read loop data: $s" if isdbg 'json';
611 $ld = $json->decode($s) if length $s;
613 # sort out rain stats
615 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
616 my $diff = 24*60 - $c;
617 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
622 $rain += $_ for @{$ld->{rain24}};
625 $ld->{rain_24} = nearest(0.1, $rain);
633 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
634 $dataf->autoflush(1);
640 my $s = $json->encode($ld);
641 dbg "write loop data: $s" if isdbg 'json';
645 sub cycle_loop_data_files
647 $dataf->close if $dataf;
650 rename "$datafn.oooo", "$datafn.ooooo";
651 rename "$datafn.ooo", "$datafn.oooo";
652 rename "$datafn.oo", "$datafn.ooo";
653 rename "$datafn.o", "$datafn.oo";
654 copy $datafn, "$datafn.o";
661 my $start = shift || time - 86400;
665 if ($lg->open($dayno, 'r+')) {
666 while (my $l = $lg->read) {
667 next unless $l =~ /,"$let":/;
668 my ($t) = $l =~ /"t":(\d+)/;
669 if ($t && $t >= $start) {