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);
18 use constant pi => 3.14159265358979;
20 my $devname = "/dev/davis";
21 my $datafn = ".loop_data";
24 my $poll_interval = 2.5;
25 my $rain_mult = 0.2; # 0.1 or 0.2 mm or 0.01 inches
33 my $ser; # the serial port Mojo::IOLoop::Stream
37 our $json = JSON->new->canonical(1);
38 our $WS = {}; # websocket connections
42 our $loop_count; # how many LOOPs we have done, used as start indicator
45 0x0, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
46 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
47 0x1231, 0x210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
48 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
49 0x2462, 0x3443, 0x420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
50 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
51 0x3653, 0x2672, 0x1611, 0x630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
52 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
53 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x840, 0x1861, 0x2802, 0x3823,
54 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
55 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0xa50, 0x3a33, 0x2a12,
56 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
57 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0xc60, 0x1c41,
58 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
59 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0xe70,
60 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
61 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
62 0x1080, 0xa1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
63 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
64 0x2b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
65 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
66 0x34e2, 0x24c3, 0x14a0, 0x481, 0x7466, 0x6447, 0x5424, 0x4405,
67 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
68 0x26d3, 0x36f2, 0x691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
69 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
70 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x8e1, 0x3882, 0x28a3,
71 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
72 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0xaf1, 0x1ad0, 0x2ab3, 0x3a92,
73 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
74 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0xcc1,
75 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
76 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0xed1, 0x1ef0
81 $bar_trend{-60} = "Falling Rapidly";
82 $bar_trend{196} = "Falling Rapidly";
83 $bar_trend{-20} = "Falling Slowly";
84 $bar_trend{236} = "Falling Slowly";
85 $bar_trend{0} = "Steady";
86 $bar_trend{20} = "Rising Slowly";
87 $bar_trend{60} = "Rising Rapidly";
91 $SIG{TERM} = $SIG{INT} = sub {++$ending; Mojo::IOLoop->stop;};
95 # WebSocket weather service
96 websocket '/weather' => sub {
102 app->log->debug('WebSocket opened.');
103 dbg 'WebSocket opened' if isdbg 'chan';
106 # send historical data
107 $c->send($ld->{lasthour_h}) if exists $ld->{lasthour_h};
108 $c->send($ld->{lastmin_h}) if exists $ld->{lastmin_h};
111 $c->inactivity_timeout(3615);
117 dbg "websocket: text $msg" if isdbg 'chan';
121 dbg "websocket: json $msg" if isdbg 'chan';
126 $c->on(finish => sub {
127 my ($c, $code, $reason) = @_;
128 app->log->debug("WebSocket closed with status $code.");
129 dbg 'webwocket closed with status $code' if isdbg 'chan';
134 get '/' => {template => 'index'};
144 dbg "*** starting $0";
147 our $dlog = SMGLog->new("day");
148 dbg "before next tick";
149 Mojo::IOLoop->next_tick(sub { loop() });
150 dbg "before app start";
152 dbg "after app start";
155 close $dataf if $dataf;
157 # move all the files along one
158 copy "$datafn.oooo", "$datafn.ooooo";
159 copy "$datafn.ooo", "$datafn.oooo";
160 copy "$datafn.oo", "$datafn.ooo";
161 copy "$datafn.o", "$datafn.oo";
162 copy $datafn, "$datafn.o";
171 ##################################################################################
176 open $dataf, "+>>", $datafn or die "cannot open $datafn $!";
177 $dataf->autoflush(1);
181 dbg "last_min: " . scalar gmtime($ld->{last_min});
182 dbg "last_hour: " . scalar gmtime($ld->{last_hour});
184 $did = Mojo::IOLoop->recurring(1 => sub {$dlog->flushall});
195 $d =~ s/([\%\x00-\x1f\x7f-\xff])/sprintf("%%%02X", ord($1))/eg;
196 dbg "read added '$d' buf lth=" . length $buf if isdbg 'raw';
197 if ($state eq 'waitnl' && $buf =~ /[\cJ\cM]+/) {
198 dbg "Got \\n" if isdbg 'state';
199 Mojo::IOLoop->remove($tid) if $tid;
203 $ser->write("LPS 1 1\n");
204 chgstate("waitloop");
205 } elsif ($state eq "waitloop") {
206 if ($buf =~ /\x06/) {
207 dbg "Got ACK 0x06" if isdbg 'state';
208 chgstate('waitlooprec');
211 } elsif ($state eq 'waitlooprec') {
212 if (length $buf >= 99) {
213 dbg "got loop record" if isdbg 'chan';
224 dbg "start_loop writing $nlcount \\n" if isdbg 'state';
226 Mojo::IOLoop->remove($tid) if $tid;
228 $tid = Mojo::IOLoop->recurring(0.6 => sub {
229 if (++$nlcount > 10) {
230 dbg "\\n count > 10, closing connection" if isdbg 'chan';
234 dbg "writing $nlcount \\n" if isdbg 'state';
242 dbg "state '$state' -> '$_[0]'" if isdbg 'state';
249 dbg "do reopen on '$name' ending $ending";
251 $ser = do_open($name);
255 Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
266 my $ob = Serial->new($name, 19200) || die "$name $!\n";
267 dbg "streaming $name fileno(" . fileno($ob) . ")" if isdbg 'chan';
269 my $ser = Mojo::IOLoop::Stream->new($ob);
270 $ser->on(error=>sub {dbg "serial $_[1]"; do_reopen($name) unless $ending});
271 $ser->on(close=>sub {dbg "serial closing"; do_reopen($name) unless $ending});
272 $ser->on(timeout=>sub {dbg "serial timeout";});
273 $ser->on(read=>sub {on_read(@_)});
276 Mojo::IOLoop->remove($tid) if $tid;
278 Mojo::IOLoop->remove($rid) if $rid;
280 $rid = Mojo::IOLoop->recurring($poll_interval => sub {
281 start_loop() if !$state;
295 my $loo = substr $blk,0,3;
296 unless ( $loo eq 'LOO') {
297 dbg "Block invalid loo -> $loo" if isdbg 'chan'; return;
305 my $crc_calc = CRC_CCITT($blk);
310 $tmp = unpack("s", substr $blk,7,2) / 1000;
311 $h{Pressure} = nearest(1, in2mb($tmp));
313 $tmp = unpack("s", substr $blk,9,2) / 10;
314 $h{Temp_In} = nearest(0.1, f2c($tmp));
316 $temp = nearest(0.1, f2c(unpack("s", substr $blk,12,2) / 10));
317 $h{Temp_Out} = $temp;
318 if ($temp > 75 || $temp < -75) {
319 dbg "LOOP Temperature out of range ($temp), record ignored";
323 $tmp = unpack("C", substr $blk,14,1);
324 $h{Wind} = nearest(0.1, mph2mps($tmp));
325 $h{Dir} = unpack("s", substr $blk,16,2)+0;
327 my $wind = {w => $h{Wind}, d => $h{Dir}};
328 $wind = 0 if $wind == 255;
329 push @{$ld->{wind_min}}, $wind;
331 $tmp = int(unpack("C", substr $blk,33,1)+0);
333 dbg "LOOP Outside Humidity out of range ($tmp), record ignored";
336 $h{Humidity_Out} = $tmp;
337 $tmp = int(unpack("C", substr $blk,11,1)+0);
339 dbg "LOOP Inside Humidity out of range ($tmp), record ignored";
342 $h{Humidity_In} = $tmp;
345 $tmp = unpack("C", substr $blk,43,1)+0;
346 $h{UV} = $tmp unless $tmp >= 255;
347 $tmp = unpack("s", substr $blk,44,2)+0; # watt/m**2
348 $h{Solar} = $tmp unless $tmp >= 32767;
350 # $h{Rain_Rate} = nearest(0.1,unpack("s", substr $blk,41,2) * $rain_mult);
351 $rain = $h{Rain_Day} = nearest(0.1, unpack("s", substr $blk,50,2) * $rain_mult);
352 my $delta_rain = $h{Rain} = nearest(0.1, ($rain >= $ld->{last_rain} ? $rain - $ld->{last_rain} : $rain)) if $loop_count;
353 $ld->{last_rain} = $rain;
355 # what sort of packet is it?
356 my $sort = unpack("C", substr $blk,4,1);
360 $tmp = unpack("C", substr $blk,18,2);
361 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp/10));
362 $tmp = unpack("C", substr $blk,20,2);
363 # $h{Wind_Avg_2} = nearest(0.1,mph2mps($tmp/10));
364 $tmp = unpack("C", substr $blk,22,2);
365 # $h{Wind_Gust_10} = nearest(0.1,mph2mps($tmp/10));
367 # $h{Dir_Avg_10} = unpack("C", substr $blk,24,2)+0;
368 $tmp = unpack("C", substr $blk,30,2);
369 $h{Dew_Point} = nearest(0.1, f2c($tmp));
374 $tmp = unpack("C", substr $blk,15,1);
375 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp));
376 $h{Dew_Point} = nearest(0.1, dew_point($h{Temp_Out}, $h{Humidity_Out}));
377 $h{Rain_Month} = nearest(0.1, unpack("s", substr $blk,52,2) * $rain_mult);
378 $h{Rain_Year} = nearest(0.1, unpack("s", substr $blk,54,2) * $rain_mult);
383 my $dayno = int($ts/86400);
384 if ($dayno > $ld->{last_day}) {
385 $ld->{Temp_Out_Max} = $ld->{Temp_Out_Min} = $temp;
386 $ld->{last_day} = $dayno;
388 $ld->{Temp_Out_Max} = $temp if $temp > $ld->{Temp_Out_Max};
389 $ld->{Temp_Out_Min} = $temp if $temp < $ld->{Temp_Out_Min};
391 if ($ts >= $ld->{last_hour} + 1800) {
392 $h{Pressure_Trend} = unpack("C", substr $blk,3,1);
393 $h{Pressure_Trend_txt} = $bar_trend{$h{Pressure_Trend}};
394 $h{Batt_TX_OK} = (unpack("C", substr $blk,86,1)+0) ^ 1;
395 $h{Batt_Console} = nearest(0.01, unpack("s", substr $blk,87,2) * 0.005859375);
396 $h{Forecast_Icon} = unpack("C", substr $blk,89,1);
397 $h{Forecast_Rule} = unpack("C", substr $blk,90,1);
398 $h{Sunrise} = sprintf( "%04d", unpack("S", substr $blk,91,2) );
399 $h{Sunrise} =~ s/(\d{2})(\d{2})/$1:$2/;
400 $h{Sunset} = sprintf( "%04d", unpack("S", substr $blk,93,2) );
401 $h{Sunset} =~ s/(\d{2})(\d{2})/$1:$2/;
402 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
403 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
405 if ($loop_count) { # i.e not the first
406 my $a = wind_average(scalar @{$ld->{wind_hour}} ? @{$ld->{wind_hour}} : {w => $h{Wind}, d => $h{Dir}});
408 $h{Wind_1h} = nearest(0.1, $a->{w});
409 $h{Dir_1h} = nearest(0.1, $a->{d});
411 $a = wind_average(@{$ld->{wind_min}});
412 $h{Wind_1m} = nearest(0.1, $a->{w});
413 $h{Dir_1m} = nearest(1, $a->{d});
415 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
417 $ld->{last_rain_min} = $ld->{last_rain_hour} = $rain;
420 $s = genstr($ts, 'h', \%h);
421 $ld->{lasthour_h} = $s;
423 $ld->{last_hour} = int($ts/1800)*1800;
424 $ld->{last_min} = int($ts/60)*60;
425 @{$ld->{wind_hour}} = ();
426 @{$ld->{wind_min}} = ();
430 } elsif ($ts >= $ld->{last_min} + 60) {
431 my $a = wind_average(@{$ld->{wind_min}});
434 push @{$ld->{wind_hour}}, $a;
436 if ($loop_count) { # i.e not the first
439 $h{Wind_1m} = nearest(0.1, $a->{w});
440 $h{Dir_1m} = nearest(1, $a->{d});
441 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
443 $ld->{last_rain_min} = $rain;
445 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
446 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
449 $s = genstr($ts, 'm', \%h);
450 $ld->{lastmin_h} = $s;
452 $ld->{last_min} = int($ts/60)*60;
453 @{$ld->{wind_min}} = ();
458 my $o = gen_hash_diff($ld->{last_h}, \%h);
460 $s = genstr($ts, 'r', $o);
463 dbg "loop rec not changed" if isdbg 'chan';
466 output_str($s) if $s;
470 dbg "CRC check failed for LOOP data!";
481 my $j = $json->encode($h);
482 my ($sec,$min,$hr) = (gmtime $ts)[0,1,2];
483 my $tm = sprintf "%02d:%02d:%02d", $hr, $min, $sec;
485 return qq|{"tm":"$tm","t":$ts,"$let":$j}|;
494 foreach my $ws (keys $WS) {
511 while (my ($k, $v) = each %$now) {
512 if ($last->{$k} ne $now->{$k}) {
517 return $count ? \%o : undef;
525 # Using the simplified approximation for dew point
526 # Accurate to 1 degree C for humidities > 50 %
527 # http://en.wikipedia.org/wiki/Dew_point
529 my $dewpoint = $temp - ((100 - $rh) / 5);
531 # this is the more complete one (which doesn't work)
535 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
536 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
543 # Expects packed data...
544 my $data_str = shift @_;
547 my @lst = split //, $data_str;
548 foreach my $data (@lst) {
549 my $data = unpack("c",$data);
552 my $index = $crc >> 8 ^ $data;
553 my $lhs = $crc_table[$index];
554 #print "lhs=$lhs, crc=$crc\n";
555 my $rhs = ($crc << 8) & 0xFFFF;
566 return ($_[0] - 32) * 5/9;
571 return $_[0] * 0.44704;
576 return $_[0] * 33.8637526;
581 my ($sindir, $cosdir, $wind);
586 $sindir += sin(d2r($r->{d})) * $r->{w};
587 $cosdir += cos(d2r($r->{d})) * $r->{w};
591 my $avhdg = r2d(atan2($sindir, $cosdir));
592 $avhdg += 360 if $avhdg < 0;
593 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
600 return ($n / pi) * 180;
607 return ($n / 180) * pi;
614 $ld->{rain24} ||= [];
616 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
617 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
618 my $Rain_1m = nearest(0.1, $rm);
619 push @{$ld->{rain24}}, $Rain_1m;
620 $ld->{rain_24} += $rm;
621 while (@{$ld->{rain24}} > 24*60) {
622 $ld->{rain_24} -= shift @{$ld->{rain24}};
624 my $Rain_24h = nearest(0.1, $ld->{rain_24});
625 return ($Rain_1m, $Rain_1h, $Rain_24h);
630 return unless $dataf;
635 dbg "read loop data: $s" if isdbg 'json';
636 $ld = $json->decode($s) if length $s;
638 # sort out rain stats
640 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
641 my $diff = 24*60 - $c;
642 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
647 $rain += $_ for @{$ld->{rain24}};
650 $ld->{rain_24} = nearest(0.1, $rain);
657 return unless $dataf;
662 my $s = $json->encode($ld);
663 dbg "write loop data: $s" if isdbg 'json';
671 % my $url = url_for 'weather';
674 <head><title>DWeather</title></head>
681 function process(key,value) {
682 var d = document.getElementById(key);
688 function traverse(o) {
692 if (o[i] !== null && typeof(o[i])=="object") {
699 ws = new WebSocket('<%= $url->to_abs %>');
700 document.body.innerHTML += 'ws connecting to: <%= $url->to_abs %> type_of: ' + typeof(ws) + '<br>';
701 if (typeof(ws) === 'object') {
702 ws.onmessage = function (event) {
703 var js = JSON.parse(event.data);
704 if (js !== null && typeof(js) === 'object') {
708 ws.onopen = function (event) {
709 ws.send('WebSocket support works! ♥');
712 document.body.innerHTML += 'Webserver only works with Websocket aware browsers';
718 <table border=1 width=80%>
720 <th>Time:<td><span id="tm"> </span>
721 <th>Sunrise:<td><span id="Sunrise"> </span>
722 <th>Sunset:<td><span id="Sunset"> </span>
723 <th>Console Volts:<td><span id="Batt_Console"> </span>
724 <th>TX Battery OK:<td><span id="Batt_TX_OK"> </span>
727 <th>Pressure:<td><span id="Pressure"> </span>
728 <th>Trend:<td><span id="Pressure_Trend_txt"> </span>
731 <th>Temperature in:<td> <span id="Temp_In"> </span>
732 <th>Humidity:<td> <span id="Humidity_In"> </span>
735 <th>Temperature out:<td> <span id="Temp_Out"> </span>
736 <th>Min:<td> <span id="Temp_Out_Min"> </span>
737 <th>Max:<td> <span id="Temp_Out_Max"> </span>
738 <th>Humidity:<td> <span id="Humidity_Out"> </span>
739 <th>Dew Point:<td> <span id="Dew_Point"> </span>
742 <th>Wind Direction:<td> <span id="Dir"> </span>
743 <th>Minute Avg:<td> <span id="Dir_1m"> </span>
744 <th>Speed:<td> <span id="Wind"> </span>
745 <th>Minute Avg:<td> <span id="Wind_1m"> </span>
748 <th>Rain Hour:<td> <span id="Rain_1h"> </span>
749 <th>Day:<td> <span id="Rain_Day"> </span>
750 <th>24hrs:<td> <span id="Rain_24h"> </span>
751 <th>Month:<td> <span id="Rain_Month"> </span>
752 <th>Year:<td> <span id="Rain_Year"> </span>