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);
19 use constant pi => 3.14159265358979;
21 my $devname = "/dev/davis";
22 my $datafn = ".loop_data";
25 my $poll_interval = 2.5;
26 my $rain_mult = 0.2; # 0.1 or 0.2 mm or 0.01 inches
34 my $ser; # the serial port Mojo::IOLoop::Stream
38 our $json = JSON->new->canonical(1);
39 our $WS = {}; # websocket connections
43 our $loop_count; # how many LOOPs we have done, used as start indicator
46 0x0, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
47 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
48 0x1231, 0x210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
49 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
50 0x2462, 0x3443, 0x420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
51 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
52 0x3653, 0x2672, 0x1611, 0x630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
53 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
54 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x840, 0x1861, 0x2802, 0x3823,
55 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
56 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0xa50, 0x3a33, 0x2a12,
57 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
58 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0xc60, 0x1c41,
59 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
60 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0xe70,
61 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
62 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
63 0x1080, 0xa1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
64 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
65 0x2b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
66 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
67 0x34e2, 0x24c3, 0x14a0, 0x481, 0x7466, 0x6447, 0x5424, 0x4405,
68 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
69 0x26d3, 0x36f2, 0x691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
70 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
71 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x8e1, 0x3882, 0x28a3,
72 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
73 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0xaf1, 0x1ad0, 0x2ab3, 0x3a92,
74 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
75 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0xcc1,
76 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
77 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0xed1, 0x1ef0
82 $bar_trend{-60} = "Falling Rapidly";
83 $bar_trend{196} = "Falling Rapidly";
84 $bar_trend{-20} = "Falling Slowly";
85 $bar_trend{236} = "Falling Slowly";
86 $bar_trend{0} = "Steady";
87 $bar_trend{20} = "Rising Slowly";
88 $bar_trend{60} = "Rising Rapidly";
92 $SIG{TERM} = $SIG{INT} = sub {++$ending; Mojo::IOLoop->stop;};
96 # WebSocket weather service
97 websocket '/weather' => sub {
103 app->log->debug('WebSocket opened.');
104 dbg 'WebSocket opened' if isdbg 'chan';
107 # send historical data
108 $c->send($ld->{lasthour_h}) if exists $ld->{lasthour_h};
109 $c->send($ld->{lastmin_h}) if exists $ld->{lastmin_h};
112 $c->inactivity_timeout(3615);
118 dbg "websocket: text $msg" if isdbg 'chan';
122 dbg "websocket: json $msg" if isdbg 'chan';
127 $c->on(finish => sub {
128 my ($c, $code, $reason) = @_;
129 app->log->debug("WebSocket closed with status $code.");
130 dbg 'webwocket closed with status $code' if isdbg 'chan';
135 get '/' => {template => 'index'};
145 dbg "*** starting $0";
150 our $dlog = SMGLog->new("day");
151 dbg "before next tick";
152 Mojo::IOLoop->next_tick(sub { loop() });
153 dbg "before app start";
155 dbg "after app start";
158 close $dataf if $dataf;
160 # move all the files along one
161 cycle_loop_data_files();
169 ##################################################################################
173 dbg "last_min: " . scalar gmtime($ld->{last_min});
174 dbg "last_hour: " . scalar gmtime($ld->{last_hour});
176 $did = Mojo::IOLoop->recurring(1 => sub {$dlog->flushall});
187 $d =~ s/([\%\x00-\x1f\x7f-\xff])/sprintf("%%%02X", ord($1))/eg;
188 dbg "read added '$d' buf lth=" . length $buf if isdbg 'raw';
189 if ($state eq 'waitnl' && $buf =~ /[\cJ\cM]+/) {
190 dbg "Got \\n" if isdbg 'state';
191 Mojo::IOLoop->remove($tid) if $tid;
195 $ser->write("LPS 1 1\n");
196 chgstate("waitloop");
197 } elsif ($state eq "waitloop") {
198 if ($buf =~ /\x06/) {
199 dbg "Got ACK 0x06" if isdbg 'state';
200 chgstate('waitlooprec');
203 } elsif ($state eq 'waitlooprec') {
204 if (length $buf >= 99) {
205 dbg "got loop record" if isdbg 'chan';
216 dbg "start_loop writing $nlcount \\n" if isdbg 'state';
218 Mojo::IOLoop->remove($tid) if $tid;
220 $tid = Mojo::IOLoop->recurring(0.6 => sub {
221 if (++$nlcount > 10) {
222 dbg "\\n count > 10, closing connection" if isdbg 'chan';
226 dbg "writing $nlcount \\n" if isdbg 'state';
234 dbg "state '$state' -> '$_[0]'" if isdbg 'state';
241 dbg "do reopen on '$name' ending $ending";
243 $ser = do_open($name);
247 Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
258 my $ob = Serial->new($name, 19200) || die "$name $!\n";
259 dbg "streaming $name fileno(" . fileno($ob) . ")" if isdbg 'chan';
261 my $ser = Mojo::IOLoop::Stream->new($ob);
262 $ser->on(error=>sub {dbg "serial $_[1]"; do_reopen($name) unless $ending});
263 $ser->on(close=>sub {dbg "serial closing"; do_reopen($name) unless $ending});
264 $ser->on(timeout=>sub {dbg "serial timeout";});
265 $ser->on(read=>sub {on_read(@_)});
268 Mojo::IOLoop->remove($tid) if $tid;
270 Mojo::IOLoop->remove($rid) if $rid;
272 $rid = Mojo::IOLoop->recurring($poll_interval => sub {
273 start_loop() if !$state;
287 my $loo = substr $blk,0,3;
288 unless ( $loo eq 'LOO') {
289 dbg "Block invalid loo -> $loo" if isdbg 'chan'; return;
297 my $crc_calc = CRC_CCITT($blk);
302 $tmp = unpack("s", substr $blk,7,2) / 1000;
303 $h{Pressure} = nearest(1, in2mb($tmp));
305 $tmp = unpack("s", substr $blk,9,2) / 10;
306 $h{Temp_In} = nearest(0.1, f2c($tmp));
308 $temp = nearest(0.1, f2c(unpack("s", substr $blk,12,2) / 10));
309 $h{Temp_Out} = $temp;
310 if ($temp > 75 || $temp < -75) {
311 dbg "LOOP Temperature out of range ($temp), record ignored";
315 $tmp = unpack("C", substr $blk,14,1);
316 $h{Wind} = nearest(0.1, mph2mps($tmp));
317 $h{Dir} = unpack("s", substr $blk,16,2)+0;
319 my $wind = {w => $h{Wind}, d => $h{Dir}};
320 $wind = 0 if $wind == 255;
321 push @{$ld->{wind_min}}, $wind;
323 $tmp = int(unpack("C", substr $blk,33,1)+0);
325 dbg "LOOP Outside Humidity out of range ($tmp), record ignored";
328 $h{Humidity_Out} = $tmp;
329 $tmp = int(unpack("C", substr $blk,11,1)+0);
331 dbg "LOOP Inside Humidity out of range ($tmp), record ignored";
334 $h{Humidity_In} = $tmp;
337 $tmp = unpack("C", substr $blk,43,1)+0;
338 $h{UV} = $tmp unless $tmp >= 255;
339 $tmp = unpack("s", substr $blk,44,2)+0; # watt/m**2
340 $h{Solar} = $tmp unless $tmp >= 32767;
342 # $h{Rain_Rate} = nearest(0.1,unpack("s", substr $blk,41,2) * $rain_mult);
343 $rain = $h{Rain_Day} = nearest(0.1, unpack("s", substr $blk,50,2) * $rain_mult);
344 my $delta_rain = $h{Rain} = nearest(0.1, ($rain >= $ld->{last_rain} ? $rain - $ld->{last_rain} : $rain)) if $loop_count;
345 $ld->{last_rain} = $rain;
347 # what sort of packet is it?
348 my $sort = unpack("C", substr $blk,4,1);
352 $tmp = unpack("C", substr $blk,18,2);
353 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp/10));
354 $tmp = unpack("C", substr $blk,20,2);
355 # $h{Wind_Avg_2} = nearest(0.1,mph2mps($tmp/10));
356 $tmp = unpack("C", substr $blk,22,2);
357 # $h{Wind_Gust_10} = nearest(0.1,mph2mps($tmp/10));
359 # $h{Dir_Avg_10} = unpack("C", substr $blk,24,2)+0;
360 $tmp = unpack("C", substr $blk,30,2);
361 $h{Dew_Point} = nearest(0.1, f2c($tmp));
366 $tmp = unpack("C", substr $blk,15,1);
367 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp));
368 $h{Dew_Point} = nearest(0.1, dew_point($h{Temp_Out}, $h{Humidity_Out}));
369 $h{Rain_Month} = nearest(0.1, unpack("s", substr $blk,52,2) * $rain_mult);
370 $h{Rain_Year} = nearest(0.1, unpack("s", substr $blk,54,2) * $rain_mult);
375 my $dayno = int($ts/86400);
376 if ($dayno > $ld->{last_day}) {
377 $ld->{Temp_Out_Max} = $ld->{Temp_Out_Min} = $temp;
378 $ld->{last_day} = $dayno;
380 cycle_loop_data_files();
382 $ld->{Temp_Out_Max} = $temp if $temp > $ld->{Temp_Out_Max};
383 $ld->{Temp_Out_Min} = $temp if $temp < $ld->{Temp_Out_Min};
385 if ($ts >= $ld->{last_hour} + 1800) {
386 $h{Pressure_Trend} = unpack("C", substr $blk,3,1);
387 $h{Pressure_Trend_txt} = $bar_trend{$h{Pressure_Trend}};
388 $h{Batt_TX_OK} = (unpack("C", substr $blk,86,1)+0) ^ 1;
389 $h{Batt_Console} = nearest(0.01, unpack("s", substr $blk,87,2) * 0.005859375);
390 $h{Forecast_Icon} = unpack("C", substr $blk,89,1);
391 $h{Forecast_Rule} = unpack("C", substr $blk,90,1);
392 $h{Sunrise} = sprintf( "%04d", unpack("S", substr $blk,91,2) );
393 $h{Sunrise} =~ s/(\d{2})(\d{2})/$1:$2/;
394 $h{Sunset} = sprintf( "%04d", unpack("S", substr $blk,93,2) );
395 $h{Sunset} =~ s/(\d{2})(\d{2})/$1:$2/;
396 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
397 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
399 if ($loop_count) { # i.e not the first
400 my $a = wind_average(scalar @{$ld->{wind_hour}} ? @{$ld->{wind_hour}} : {w => $h{Wind}, d => $h{Dir}});
402 $h{Wind_1h} = nearest(0.1, $a->{w});
403 $h{Dir_1h} = nearest(0.1, $a->{d});
405 $a = wind_average(@{$ld->{wind_min}});
406 $h{Wind_1m} = nearest(0.1, $a->{w});
407 $h{Dir_1m} = nearest(1, $a->{d});
409 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
411 $ld->{last_rain_min} = $ld->{last_rain_hour} = $rain;
414 $s = genstr($ts, 'h', \%h);
415 $ld->{lasthour_h} = $s;
417 $ld->{last_hour} = int($ts/1800)*1800;
418 $ld->{last_min} = int($ts/60)*60;
419 @{$ld->{wind_hour}} = ();
420 @{$ld->{wind_min}} = ();
422 output_str($s, 1) if $s;
425 } elsif ($ts >= $ld->{last_min} + 60) {
426 my $a = wind_average(@{$ld->{wind_min}});
429 push @{$ld->{wind_hour}}, $a;
431 if ($loop_count) { # i.e not the first
434 $h{Wind_1m} = nearest(0.1, $a->{w});
435 $h{Dir_1m} = nearest(1, $a->{d});
436 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
438 $ld->{last_rain_min} = $rain;
440 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
441 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
444 $s = genstr($ts, 'm', \%h);
445 $ld->{lastmin_h} = $s;
447 $ld->{last_min} = int($ts/60)*60;
448 @{$ld->{wind_min}} = ();
450 output_str($s, 1) if $s;
454 my $o = gen_hash_diff($ld->{last_h}, \%h);
456 $s = genstr($ts, 'r', $o);
459 dbg "loop rec not changed" if isdbg 'chan';
461 output_str($s, 0) if $s;
466 dbg "CRC check failed for LOOP data!";
477 my $j = $json->encode($h);
478 my ($sec,$min,$hr) = (gmtime $ts)[0,1,2];
479 my $tm = sprintf "%02d:%02d:%02d", $hr, $min, $sec;
481 return qq|{"tm":"$tm","t":$ts,"$let":$j}|;
491 $dlog->writenow($s) if $logit;
492 foreach my $ws (keys $WS) {
509 while (my ($k, $v) = each %$now) {
510 if ($last->{$k} ne $now->{$k}) {
515 return $count ? \%o : undef;
523 # Using the simplified approximation for dew point
524 # Accurate to 1 degree C for humidities > 50 %
525 # http://en.wikipedia.org/wiki/Dew_point
527 my $dewpoint = $temp - ((100 - $rh) / 5);
529 # this is the more complete one (which doesn't work)
533 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
534 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
541 # Expects packed data...
542 my $data_str = shift @_;
545 my @lst = split //, $data_str;
546 foreach my $data (@lst) {
547 my $data = unpack("c",$data);
550 my $index = $crc >> 8 ^ $data;
551 my $lhs = $crc_table[$index];
552 #print "lhs=$lhs, crc=$crc\n";
553 my $rhs = ($crc << 8) & 0xFFFF;
564 return ($_[0] - 32) * 5/9;
569 return $_[0] * 0.44704;
574 return $_[0] * 33.8637526;
579 my ($sindir, $cosdir, $wind);
584 $sindir += sin(d2r($r->{d})) * $r->{w};
585 $cosdir += cos(d2r($r->{d})) * $r->{w};
589 my $avhdg = r2d(atan2($sindir, $cosdir));
590 $avhdg += 360 if $avhdg < 0;
591 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
598 return ($n / pi) * 180;
605 return ($n / 180) * pi;
612 $ld->{rain24} ||= [];
614 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
615 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
616 my $Rain_1m = nearest(0.1, $rm);
617 push @{$ld->{rain24}}, $Rain_1m;
618 $ld->{rain_24} += $rm;
619 while (@{$ld->{rain24}} > 24*60) {
620 $ld->{rain_24} -= shift @{$ld->{rain24}};
622 my $Rain_24h = nearest(0.1, $ld->{rain_24});
623 return ($Rain_1m, $Rain_1h, $Rain_24h);
629 open $dataf, "+>>", $datafn or die "cannot open $datafn $!";
630 $dataf->autoflush(1);
636 dbg "read loop data: $s" if isdbg 'json';
637 $ld = $json->decode($s) if length $s;
639 # sort out rain stats
641 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
642 my $diff = 24*60 - $c;
643 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
648 $rain += $_ for @{$ld->{rain24}};
651 $ld->{rain_24} = nearest(0.1, $rain);
659 open $dataf, "+>>", $datafn or die "cannot open $datafn $!";
660 $dataf->autoflush(1);
666 my $s = $json->encode($ld);
667 dbg "write loop data: $s" if isdbg 'json';
671 sub cycle_loop_data_files
673 close $dataf if $dataf;
675 rename "$datafn.oooo", "$datafn.ooooo";
676 rename "$datafn.ooo", "$datafn.oooo";
677 rename "$datafn.oo", "$datafn.ooo";
678 rename "$datafn.o", "$datafn.oo";
679 copy $datafn, "$datafn.o";
685 % my $url = url_for 'weather';
689 <title>DWeather</title>
690 <meta charset="utf-8">
691 <meta http-equiv="X-UA-Compatible" content="IE=edge">
692 <meta name="viewport" content="width=device-width, initial-scale=1">
694 <!-- Latest compiled and minified CSS -->
695 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
697 <!-- Optional theme -->
698 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
702 <center><h1>High View Weather</h1></center>
708 function process(key,value) {
709 var d = document.getElementById(key);
715 function traverse(o) {
719 if (o[i] !== null && typeof(o[i])=="object") {
725 window.onload = function() {
727 ws = new WebSocket('<%= $url->to_abs %>');
729 if (typeof(ws) === 'object') {
730 ws.onmessage = function (event) {
731 var js = JSON.parse(event.data);
732 if (js !== null && typeof(js) === 'object') {
736 ws.onopen = function (event) {
737 document.getElementById("wsconnect").innerHTML = 'ws connected to: <%= $url->to_abs %>';
738 ws.send('WebSocket support works! ♥');
740 ws.onclose = function(event) {
741 document.getElementById("wsconnect").innerHTML = 'ws disconnected, refresh to restart';
744 document.body.innerHTML += 'Webserver only works with Websocket aware browsers';
750 <div id="start-template">
752 <table border=1 width=80% align="center">
754 <th>Time:<td><span id="tm"> </span>
755 <th>Sunrise:<td><span id="Sunrise"> </span>
756 <th>Sunset:<td><span id="Sunset"> </span>
757 <th>Console Volts:<td><span id="Batt_Console"> </span>
758 <th>TX Battery OK:<td><span id="Batt_TX_OK"> </span>
761 <th>Pressure:<td><span id="Pressure"> </span>
762 <th>Trend:<td><span id="Pressure_Trend_txt"> </span>
765 <th>Temperature in:<td> <span id="Temp_In"> </span>
766 <th>Humidity:<td> <span id="Humidity_In"> </span>
769 <th>Temperature out:<td> <span id="Temp_Out"> </span>
770 <th>Min:<td> <span id="Temp_Out_Min"> </span>
771 <th>Max:<td> <span id="Temp_Out_Max"> </span>
772 <th>Humidity:<td> <span id="Humidity_Out"> </span>
773 <th>Dew Point:<td> <span id="Dew_Point"> </span>
776 <th>Wind Direction:<td> <span id="Dir"> </span>
777 <th>Minute Avg:<td> <span id="Dir_1m"> </span>
778 <th>Speed:<td> <span id="Wind"> </span>
779 <th>Minute Avg:<td> <span id="Wind_1m"> </span>
782 <th>Rain 30mins:<td> <span id="Rain_1h"> </span>
783 <th>Day:<td> <span id="Rain_Day"> </span>
784 <th>24hrs:<td> <span id="Rain_24h"> </span>
785 <th>Month:<td> <span id="Rain_Month"> </span>
786 <th>Year:<td> <span id="Rain_Year"> </span>
789 <div id="wsconnect" align="center"> </div>
792 <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
793 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
794 <!-- Latest compiled and minified JavaScript -->
795 <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>