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 constant pi => 3.14159265358979;
19 my $devname = "/dev/davis";
20 my $datafn = ".loop_data";
23 my $poll_interval = 2.5;
24 my $rain_mult = 0.2; # 0.1 or 0.2 mm or 0.01 inches
32 my $ser; # the serial port Mojo::IOLoop::Stream
36 our $json = JSON->new->canonical(1);
37 our $WS = {}; # websocket connections
41 our $loop_count; # how many LOOPs we have done, used as start indicator
44 0x0, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
45 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
46 0x1231, 0x210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
47 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
48 0x2462, 0x3443, 0x420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
49 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
50 0x3653, 0x2672, 0x1611, 0x630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
51 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
52 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x840, 0x1861, 0x2802, 0x3823,
53 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
54 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0xa50, 0x3a33, 0x2a12,
55 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
56 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0xc60, 0x1c41,
57 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
58 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0xe70,
59 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
60 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
61 0x1080, 0xa1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
62 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
63 0x2b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
64 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
65 0x34e2, 0x24c3, 0x14a0, 0x481, 0x7466, 0x6447, 0x5424, 0x4405,
66 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
67 0x26d3, 0x36f2, 0x691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
68 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
69 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x8e1, 0x3882, 0x28a3,
70 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
71 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0xaf1, 0x1ad0, 0x2ab3, 0x3a92,
72 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
73 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0xcc1,
74 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
75 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0xed1, 0x1ef0
80 $bar_trend{-60} = "Falling Rapidly";
81 $bar_trend{196} = "Falling Rapidly";
82 $bar_trend{-20} = "Falling Slowly";
83 $bar_trend{236} = "Falling Slowly";
84 $bar_trend{0} = "Steady";
85 $bar_trend{20} = "Rising Slowly";
86 $bar_trend{60} = "Rising Rapidly";
90 $SIG{TERM} = $SIG{INT} = sub {++$ending; Mojo::IOLoop->stop;};
94 # WebSocket weather service
95 websocket '/weather' => sub {
101 app->log->debug('WebSocket opened.');
102 dbg 'WebSocket opened' if isdbg 'chan';
105 # send historical data
106 $c->send($ld->{lasthour_h}) if exists $ld->{lasthour_h};
107 $c->send($ld->{lastmin_h}) if exists $ld->{lastmin_h};
110 $c->inactivity_timeout(3615);
116 dbg "websocket: text $msg" if isdbg 'chan';
120 dbg "websocket: json $msg" if isdbg 'chan';
125 $c->on(finish => sub {
126 my ($c, $code, $reason) = @_;
127 app->log->debug("WebSocket closed with status $code.");
128 dbg 'webwocket closed with status $code' if isdbg 'chan';
133 get '/' => {template => 'index'};
143 dbg "*** starting $0";
146 our $dlog = SMGLog->new("day");
147 dbg "before next tick";
148 Mojo::IOLoop->next_tick(sub { loop() });
149 dbg "before app start";
151 dbg "after app start";
154 close $dataf if $dataf;
162 ##################################################################################
167 open $dataf, "+>>", $datafn or die "cannot open $datafn $!";
168 $dataf->autoflush(1);
172 dbg "last_min: " . scalar gmtime($ld->{last_min});
173 dbg "last_hour: " . scalar gmtime($ld->{last_hour});
175 $did = Mojo::IOLoop->recurring(1 => sub {$dlog->flushall});
186 $d =~ s/([\%\x00-\x1f\x7f-\xff])/sprintf("%%%02X", ord($1))/eg;
187 dbg "read added '$d' buf lth=" . length $buf if isdbg 'raw';
188 if ($state eq 'waitnl' && $buf =~ /[\cJ\cM]+/) {
189 dbg "Got \\n" if isdbg 'state';
190 Mojo::IOLoop->remove($tid) if $tid;
194 $ser->write("LPS 1 1\n");
195 chgstate("waitloop");
196 } elsif ($state eq "waitloop") {
197 if ($buf =~ /\x06/) {
198 dbg "Got ACK 0x06" if isdbg 'state';
199 chgstate('waitlooprec');
202 } elsif ($state eq 'waitlooprec') {
203 if (length $buf >= 99) {
204 dbg "got loop record" if isdbg 'chan';
215 dbg "start_loop writing $nlcount \\n" if isdbg 'state';
217 Mojo::IOLoop->remove($tid) if $tid;
219 $tid = Mojo::IOLoop->recurring(0.6 => sub {
220 if (++$nlcount > 10) {
221 dbg "\\n count > 10, closing connection" if isdbg 'chan';
225 dbg "writing $nlcount \\n" if isdbg 'state';
233 dbg "state '$state' -> '$_[0]'" if isdbg 'state';
240 dbg "do reopen on '$name' ending $ending";
242 $ser = do_open($name);
246 Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
257 my $ob = Serial->new($name, 19200) || die "$name $!\n";
258 dbg "streaming $name fileno(" . fileno($ob) . ")" if isdbg 'chan';
260 my $ser = Mojo::IOLoop::Stream->new($ob);
261 $ser->on(error=>sub {dbg "serial $_[1]"; do_reopen($name) unless $ending});
262 $ser->on(close=>sub {dbg "serial closing"; do_reopen($name) unless $ending});
263 $ser->on(timeout=>sub {dbg "serial timeout";});
264 $ser->on(read=>sub {on_read(@_)});
267 Mojo::IOLoop->remove($tid) if $tid;
269 Mojo::IOLoop->remove($rid) if $rid;
271 $rid = Mojo::IOLoop->recurring($poll_interval => sub {
272 start_loop() if !$state;
286 my $loo = substr $blk,0,3;
287 unless ( $loo eq 'LOO') {
288 dbg "Block invalid loo -> $loo" if isdbg 'chan'; return;
296 my $crc_calc = CRC_CCITT($blk);
301 $tmp = unpack("s", substr $blk,7,2) / 1000;
302 $h{Pressure} = nearest(1, in2mb($tmp));
304 $tmp = unpack("s", substr $blk,9,2) / 10;
305 $h{Temp_In} = nearest(0.1, f2c($tmp));
307 $temp = nearest(0.1, f2c(unpack("s", substr $blk,12,2) / 10));
308 $h{Temp_Out} = $temp;
309 if ($temp > 75 || $temp < -75) {
310 dbg "LOOP Temperature out of range ($temp), record ignored";
314 $tmp = unpack("C", substr $blk,14,1);
315 $h{Wind} = nearest(0.1, mph2mps($tmp));
316 $h{Dir} = unpack("s", substr $blk,16,2)+0;
318 my $wind = {w => $h{Wind}, d => $h{Dir}};
319 $wind = 0 if $wind == 255;
320 push @{$ld->{wind_min}}, $wind;
322 $tmp = int(unpack("C", substr $blk,33,1)+0);
324 dbg "LOOP Outside Humidity out of range ($tmp), record ignored";
327 $h{Humidity_Out} = $tmp;
328 $tmp = int(unpack("C", substr $blk,11,1)+0);
330 dbg "LOOP Inside Humidity out of range ($tmp), record ignored";
333 $h{Humidity_In} = $tmp;
336 $tmp = unpack("C", substr $blk,43,1)+0;
337 $h{UV} = $tmp unless $tmp >= 255;
338 $tmp = unpack("s", substr $blk,44,2)+0; # watt/m**2
339 $h{Solar} = $tmp unless $tmp >= 32767;
341 # $h{Rain_Rate} = nearest(0.1,unpack("s", substr $blk,41,2) * $rain_mult);
342 $rain = $h{Rain_Day} = nearest(0.1, unpack("s", substr $blk,50,2) * $rain_mult);
343 my $delta_rain = $h{Rain} = nearest(0.1, ($rain >= $ld->{last_rain} ? $rain - $ld->{last_rain} : $rain)) if $loop_count;
344 $ld->{last_rain} = $rain;
346 # what sort of packet is it?
347 my $sort = unpack("C", substr $blk,4,1);
351 $tmp = unpack("C", substr $blk,18,2);
352 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp/10));
353 $tmp = unpack("C", substr $blk,20,2);
354 # $h{Wind_Avg_2} = nearest(0.1,mph2mps($tmp/10));
355 $tmp = unpack("C", substr $blk,22,2);
356 # $h{Wind_Gust_10} = nearest(0.1,mph2mps($tmp/10));
358 # $h{Dir_Avg_10} = unpack("C", substr $blk,24,2)+0;
359 $tmp = unpack("C", substr $blk,30,2);
360 $h{Dew_Point} = nearest(0.1, f2c($tmp));
365 $tmp = unpack("C", substr $blk,15,1);
366 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp));
367 $h{Dew_Point} = nearest(0.1, dew_point($h{Temp_Out}, $h{Humidity_Out}));
368 $h{Rain_Month} = nearest(0.1, unpack("s", substr $blk,52,2) * $rain_mult);
369 $h{Rain_Year} = nearest(0.1, unpack("s", substr $blk,54,2) * $rain_mult);
374 my $dayno = int($ts/86400);
375 if ($dayno > $ld->{last_day}) {
376 $ld->{Temp_Out_Max} = $ld->{Temp_Out_Min} = $temp;
377 $ld->{last_day} = $dayno;
379 $ld->{Temp_Out_Max} = $temp if $temp > $ld->{Temp_Out_Max};
380 $ld->{Temp_Out_Min} = $temp if $temp < $ld->{Temp_Out_Min};
382 if ($ts >= $ld->{last_hour} + 1800) {
383 $h{Pressure_Trend} = unpack("C", substr $blk,3,1);
384 $h{Pressure_Trend_txt} = $bar_trend{$h{Pressure_Trend}};
385 $h{Batt_TX_OK} = (unpack("C", substr $blk,86,1)+0) ^ 1;
386 $h{Batt_Console} = nearest(0.01, unpack("s", substr $blk,87,2) * 0.005859375);
387 $h{Forecast_Icon} = unpack("C", substr $blk,89,1);
388 $h{Forecast_Rule} = unpack("C", substr $blk,90,1);
389 $h{Sunrise} = sprintf( "%04d", unpack("S", substr $blk,91,2) );
390 $h{Sunrise} =~ s/(\d{2})(\d{2})/$1:$2/;
391 $h{Sunset} = sprintf( "%04d", unpack("S", substr $blk,93,2) );
392 $h{Sunset} =~ s/(\d{2})(\d{2})/$1:$2/;
393 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
394 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
396 if ($loop_count) { # i.e not the first
397 my $a = wind_average(scalar @{$ld->{wind_hour}} ? @{$ld->{wind_hour}} : {w => $h{Wind}, d => $h{Dir}});
399 $h{Wind_1h} = nearest(0.1, $a->{w});
400 $h{Dir_1h} = nearest(0.1, $a->{d});
402 $a = wind_average(@{$ld->{wind_min}});
403 $h{Wind_1m} = nearest(0.1, $a->{w});
404 $h{Dir_1m} = nearest(1, $a->{d});
406 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
408 $ld->{last_rain_min} = $ld->{last_rain_hour} = $rain;
411 $s = genstr($ts, 'h', \%h);
412 $ld->{lasthour_h} = $s;
414 $ld->{last_hour} = int($ts/1800)*1800;
415 $ld->{last_min} = int($ts/60)*60;
416 @{$ld->{wind_hour}} = ();
417 @{$ld->{wind_min}} = ();
421 } elsif ($ts >= $ld->{last_min} + 60) {
422 my $a = wind_average(@{$ld->{wind_min}});
425 push @{$ld->{wind_hour}}, $a;
427 if ($loop_count) { # i.e not the first
430 $h{Wind_1m} = nearest(0.1, $a->{w});
431 $h{Dir_1m} = nearest(1, $a->{d});
432 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
434 $ld->{last_rain_min} = $rain;
436 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
437 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
440 $s = genstr($ts, 'm', \%h);
441 $ld->{lastmin_h} = $s;
443 $ld->{last_min} = int($ts/60)*60;
444 @{$ld->{wind_min}} = ();
449 my $o = gen_hash_diff($ld->{last_h}, \%h);
451 $s = genstr($ts, 'r', $o);
454 dbg "loop rec not changed" if isdbg 'chan';
457 output_str($s) if $s;
461 dbg "CRC check failed for LOOP data!";
472 my $j = $json->encode($h);
473 my ($sec,$min,$hr) = (gmtime $ts)[0,1,2];
474 my $tm = sprintf "%02d:%02d:%02d", $hr, $min, $sec;
476 return qq|{"tm":"$tm","t":$ts,"$let":$j}|;
485 foreach my $ws (keys $WS) {
502 while (my ($k, $v) = each %$now) {
503 if ($last->{$k} ne $now->{$k}) {
508 return $count ? \%o : undef;
516 # Using the simplified approximation for dew point
517 # Accurate to 1 degree C for humidities > 50 %
518 # http://en.wikipedia.org/wiki/Dew_point
520 my $dewpoint = $temp - ((100 - $rh) / 5);
522 # this is the more complete one (which doesn't work)
526 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
527 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
534 # Expects packed data...
535 my $data_str = shift @_;
538 my @lst = split //, $data_str;
539 foreach my $data (@lst) {
540 my $data = unpack("c",$data);
543 my $index = $crc >> 8 ^ $data;
544 my $lhs = $crc_table[$index];
545 #print "lhs=$lhs, crc=$crc\n";
546 my $rhs = ($crc << 8) & 0xFFFF;
557 return ($_[0] - 32) * 5/9;
562 return $_[0] * 0.44704;
567 return $_[0] * 33.8637526;
572 my ($sindir, $cosdir, $wind);
577 $sindir += sin(d2r($r->{d})) * $r->{w};
578 $cosdir += cos(d2r($r->{d})) * $r->{w};
582 my $avhdg = r2d(atan2($sindir, $cosdir));
583 $avhdg += 360 if $avhdg < 0;
584 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
591 return ($n / pi) * 180;
598 return ($n / 180) * pi;
605 $ld->{rain24} ||= [];
607 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
608 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
609 my $Rain_1m = nearest(0.1, $rm);
610 push @{$ld->{rain24}}, $Rain_1m;
611 $ld->{rain_24} += $rm;
612 while (@{$ld->{rain24}} > 24*60) {
613 $ld->{rain_24} -= shift @{$ld->{rain24}};
615 my $Rain_24h = nearest(0.1, $ld->{rain_24});
616 return ($Rain_1m, $Rain_1h, $Rain_24h);
621 return unless $dataf;
626 dbg "read loop data: $s" if isdbg 'json';
627 $ld = $json->decode($s) if length $s;
629 # sort out rain stats
631 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
632 my $diff = 24*60 - $c;
633 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
638 $rain += $_ for @{$ld->{rain24}};
641 $ld->{rain_24} = nearest(0.1, $rain);
648 return unless $dataf;
653 my $s = $json->encode($ld);
654 dbg "write loop data: $s" if isdbg 'json';
662 % my $url = url_for 'weather';
665 <head><title>DWeather</title></head>
672 function process(key,value) {
673 var d = document.getElementById(key);
679 function traverse(o) {
683 if (o[i] !== null && typeof(o[i])=="object") {
690 ws = new WebSocket('<%= $url->to_abs %>');
691 document.body.innerHTML += 'ws connecting to: <%= $url->to_abs %> type_of: ' + typeof(ws) + '<br>';
692 if (typeof(ws) === 'object') {
693 ws.onmessage = function (event) {
694 var js = JSON.parse(event.data);
695 if (js !== null && typeof(js) === 'object') {
699 ws.onopen = function (event) {
700 ws.send('WebSocket support works! ♥');
703 document.body.innerHTML += 'Webserver only works with Websocket aware browsers';
709 <table border=1 width=80%>
710 <th>Time:<td><span id="tm"> </span>
712 <th>Pressure:<td><span id="Pressure"> </span>
714 <th>Temperature in:<td> <span id="Temp_In"> </span>
715 <th>Humidity:<td> <span id="Humidity_In"> </span>
717 <th>Temperature out:<td> <span id="Temp_Out"> </span>
718 <th>Min:<td> <span id="Temp_Out_Min"> </span>
719 <th>Max:<td> <span id="Temp_Out_Max"> </span>
720 <th>Humidity:<td> <span id="Humidity_Out"> </span>
721 <th>Dew Point:<td> <span id="Dew_Point"> </span>
723 <th>Wind Direction:<td> <span id="Dir"> </span>
724 <th>Minute:<td> <span id="Dir_1m"> </span>
725 <th>Speed:<td> <span id="Wind"> </span>
726 <th>Minute:<td> <span id="Wind_1m"> </span>
728 <th>Rain Hour:<td> <span id="Rain_1h"> </span>
729 <th>Day:<td> <span id="Rain_Day"> </span>
730 <th>24hrs:<td> <span id="Rain_24h"> </span>
731 <th>Month:<td> <span id="Rain_Month"> </span>
732 <th>Year:<td> <span id="Rain_Year"> </span>