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";
148 our $dlog = SMGLog->new("day");
149 dbg "before next tick";
150 Mojo::IOLoop->next_tick(sub { loop() });
151 dbg "before app start";
153 dbg "after app start";
156 close $dataf if $dataf;
158 # move all the files along one
159 cycle_loop_data_files();
167 ##################################################################################
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}} = ();
424 } elsif ($ts >= $ld->{last_min} + 60) {
425 my $a = wind_average(@{$ld->{wind_min}});
428 push @{$ld->{wind_hour}}, $a;
430 if ($loop_count) { # i.e not the first
433 $h{Wind_1m} = nearest(0.1, $a->{w});
434 $h{Dir_1m} = nearest(1, $a->{d});
435 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
437 $ld->{last_rain_min} = $rain;
439 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
440 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
443 $s = genstr($ts, 'm', \%h);
444 $ld->{lastmin_h} = $s;
446 $ld->{last_min} = int($ts/60)*60;
447 @{$ld->{wind_min}} = ();
452 my $o = gen_hash_diff($ld->{last_h}, \%h);
454 $s = genstr($ts, 'r', $o);
457 dbg "loop rec not changed" if isdbg 'chan';
460 output_str($s) if $s;
464 dbg "CRC check failed for LOOP data!";
475 my $j = $json->encode($h);
476 my ($sec,$min,$hr) = (gmtime $ts)[0,1,2];
477 my $tm = sprintf "%02d:%02d:%02d", $hr, $min, $sec;
479 return qq|{"tm":"$tm","t":$ts,"$let":$j}|;
488 foreach my $ws (keys $WS) {
505 while (my ($k, $v) = each %$now) {
506 if ($last->{$k} ne $now->{$k}) {
511 return $count ? \%o : undef;
519 # Using the simplified approximation for dew point
520 # Accurate to 1 degree C for humidities > 50 %
521 # http://en.wikipedia.org/wiki/Dew_point
523 my $dewpoint = $temp - ((100 - $rh) / 5);
525 # this is the more complete one (which doesn't work)
529 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
530 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
537 # Expects packed data...
538 my $data_str = shift @_;
541 my @lst = split //, $data_str;
542 foreach my $data (@lst) {
543 my $data = unpack("c",$data);
546 my $index = $crc >> 8 ^ $data;
547 my $lhs = $crc_table[$index];
548 #print "lhs=$lhs, crc=$crc\n";
549 my $rhs = ($crc << 8) & 0xFFFF;
560 return ($_[0] - 32) * 5/9;
565 return $_[0] * 0.44704;
570 return $_[0] * 33.8637526;
575 my ($sindir, $cosdir, $wind);
580 $sindir += sin(d2r($r->{d})) * $r->{w};
581 $cosdir += cos(d2r($r->{d})) * $r->{w};
585 my $avhdg = r2d(atan2($sindir, $cosdir));
586 $avhdg += 360 if $avhdg < 0;
587 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
594 return ($n / pi) * 180;
601 return ($n / 180) * pi;
608 $ld->{rain24} ||= [];
610 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
611 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
612 my $Rain_1m = nearest(0.1, $rm);
613 push @{$ld->{rain24}}, $Rain_1m;
614 $ld->{rain_24} += $rm;
615 while (@{$ld->{rain24}} > 24*60) {
616 $ld->{rain_24} -= shift @{$ld->{rain24}};
618 my $Rain_24h = nearest(0.1, $ld->{rain_24});
619 return ($Rain_1m, $Rain_1h, $Rain_24h);
625 open $dataf, "+>>", $datafn or die "cannot open $datafn $!";
626 $dataf->autoflush(1);
632 dbg "read loop data: $s" if isdbg 'json';
633 $ld = $json->decode($s) if length $s;
635 # sort out rain stats
637 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
638 my $diff = 24*60 - $c;
639 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
644 $rain += $_ for @{$ld->{rain24}};
647 $ld->{rain_24} = nearest(0.1, $rain);
655 open $dataf, "+>>", $datafn or die "cannot open $datafn $!";
656 $dataf->autoflush(1);
662 my $s = $json->encode($ld);
663 dbg "write loop data: $s" if isdbg 'json';
667 sub cycle_loop_data_files
669 close $dataf if $dataf;
671 rename "$datafn.oooo", "$datafn.ooooo";
672 rename "$datafn.ooo", "$datafn.oooo";
673 rename "$datafn.oo", "$datafn.ooo";
674 rename "$datafn.o", "$datafn.oo";
675 copy $datafn, "$datafn.o";
681 % my $url = url_for 'weather';
684 <head><title>DWeather</title></head>
691 function process(key,value) {
692 var d = document.getElementById(key);
698 function traverse(o) {
702 if (o[i] !== null && typeof(o[i])=="object") {
709 ws = new WebSocket('<%= $url->to_abs %>');
710 document.body.innerHTML += 'ws connecting to: <%= $url->to_abs %> type_of: ' + typeof(ws) + '<br>';
711 if (typeof(ws) === 'object') {
712 ws.onmessage = function (event) {
713 var js = JSON.parse(event.data);
714 if (js !== null && typeof(js) === 'object') {
718 ws.onopen = function (event) {
719 ws.send('WebSocket support works! ♥');
722 document.body.innerHTML += 'Webserver only works with Websocket aware browsers';
728 <table border=1 width=80%>
730 <th>Time:<td><span id="tm"> </span>
731 <th>Sunrise:<td><span id="Sunrise"> </span>
732 <th>Sunset:<td><span id="Sunset"> </span>
733 <th>Console Volts:<td><span id="Batt_Console"> </span>
734 <th>TX Battery OK:<td><span id="Batt_TX_OK"> </span>
737 <th>Pressure:<td><span id="Pressure"> </span>
738 <th>Trend:<td><span id="Pressure_Trend_txt"> </span>
741 <th>Temperature in:<td> <span id="Temp_In"> </span>
742 <th>Humidity:<td> <span id="Humidity_In"> </span>
745 <th>Temperature out:<td> <span id="Temp_Out"> </span>
746 <th>Min:<td> <span id="Temp_Out_Min"> </span>
747 <th>Max:<td> <span id="Temp_Out_Max"> </span>
748 <th>Humidity:<td> <span id="Humidity_Out"> </span>
749 <th>Dew Point:<td> <span id="Dew_Point"> </span>
752 <th>Wind Direction:<td> <span id="Dir"> </span>
753 <th>Minute Avg:<td> <span id="Dir_1m"> </span>
754 <th>Speed:<td> <span id="Wind"> </span>
755 <th>Minute Avg:<td> <span id="Wind_1m"> </span>
758 <th>Rain 30mins:<td> <span id="Rain_1h"> </span>
759 <th>Day:<td> <span id="Rain_Day"> </span>
760 <th>24hrs:<td> <span id="Rain_24h"> </span>
761 <th>Month:<td> <span id="Rain_Month"> </span>
762 <th>Year:<td> <span id="Rain_Year"> </span>