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 ##################################################################################
172 open $dataf, "+>>", $datafn or die "cannot open $datafn $!";
173 $dataf->autoflush(1);
177 dbg "last_min: " . scalar gmtime($ld->{last_min});
178 dbg "last_hour: " . scalar gmtime($ld->{last_hour});
180 $did = Mojo::IOLoop->recurring(1 => sub {$dlog->flushall});
191 $d =~ s/([\%\x00-\x1f\x7f-\xff])/sprintf("%%%02X", ord($1))/eg;
192 dbg "read added '$d' buf lth=" . length $buf if isdbg 'raw';
193 if ($state eq 'waitnl' && $buf =~ /[\cJ\cM]+/) {
194 dbg "Got \\n" if isdbg 'state';
195 Mojo::IOLoop->remove($tid) if $tid;
199 $ser->write("LPS 1 1\n");
200 chgstate("waitloop");
201 } elsif ($state eq "waitloop") {
202 if ($buf =~ /\x06/) {
203 dbg "Got ACK 0x06" if isdbg 'state';
204 chgstate('waitlooprec');
207 } elsif ($state eq 'waitlooprec') {
208 if (length $buf >= 99) {
209 dbg "got loop record" if isdbg 'chan';
220 dbg "start_loop writing $nlcount \\n" if isdbg 'state';
222 Mojo::IOLoop->remove($tid) if $tid;
224 $tid = Mojo::IOLoop->recurring(0.6 => sub {
225 if (++$nlcount > 10) {
226 dbg "\\n count > 10, closing connection" if isdbg 'chan';
230 dbg "writing $nlcount \\n" if isdbg 'state';
238 dbg "state '$state' -> '$_[0]'" if isdbg 'state';
245 dbg "do reopen on '$name' ending $ending";
247 $ser = do_open($name);
251 Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
262 my $ob = Serial->new($name, 19200) || die "$name $!\n";
263 dbg "streaming $name fileno(" . fileno($ob) . ")" if isdbg 'chan';
265 my $ser = Mojo::IOLoop::Stream->new($ob);
266 $ser->on(error=>sub {dbg "serial $_[1]"; do_reopen($name) unless $ending});
267 $ser->on(close=>sub {dbg "serial closing"; do_reopen($name) unless $ending});
268 $ser->on(timeout=>sub {dbg "serial timeout";});
269 $ser->on(read=>sub {on_read(@_)});
272 Mojo::IOLoop->remove($tid) if $tid;
274 Mojo::IOLoop->remove($rid) if $rid;
276 $rid = Mojo::IOLoop->recurring($poll_interval => sub {
277 start_loop() if !$state;
291 my $loo = substr $blk,0,3;
292 unless ( $loo eq 'LOO') {
293 dbg "Block invalid loo -> $loo" if isdbg 'chan'; return;
301 my $crc_calc = CRC_CCITT($blk);
306 $tmp = unpack("s", substr $blk,7,2) / 1000;
307 $h{Pressure} = nearest(1, in2mb($tmp));
309 $tmp = unpack("s", substr $blk,9,2) / 10;
310 $h{Temp_In} = nearest(0.1, f2c($tmp));
312 $temp = nearest(0.1, f2c(unpack("s", substr $blk,12,2) / 10));
313 $h{Temp_Out} = $temp;
314 if ($temp > 75 || $temp < -75) {
315 dbg "LOOP Temperature out of range ($temp), record ignored";
319 $tmp = unpack("C", substr $blk,14,1);
320 $h{Wind} = nearest(0.1, mph2mps($tmp));
321 $h{Dir} = unpack("s", substr $blk,16,2)+0;
323 my $wind = {w => $h{Wind}, d => $h{Dir}};
324 $wind = 0 if $wind == 255;
325 push @{$ld->{wind_min}}, $wind;
327 $tmp = int(unpack("C", substr $blk,33,1)+0);
329 dbg "LOOP Outside Humidity out of range ($tmp), record ignored";
332 $h{Humidity_Out} = $tmp;
333 $tmp = int(unpack("C", substr $blk,11,1)+0);
335 dbg "LOOP Inside Humidity out of range ($tmp), record ignored";
338 $h{Humidity_In} = $tmp;
341 $tmp = unpack("C", substr $blk,43,1)+0;
342 $h{UV} = $tmp unless $tmp >= 255;
343 $tmp = unpack("s", substr $blk,44,2)+0; # watt/m**2
344 $h{Solar} = $tmp unless $tmp >= 32767;
346 # $h{Rain_Rate} = nearest(0.1,unpack("s", substr $blk,41,2) * $rain_mult);
347 $rain = $h{Rain_Day} = nearest(0.1, unpack("s", substr $blk,50,2) * $rain_mult);
348 my $delta_rain = $h{Rain} = nearest(0.1, ($rain >= $ld->{last_rain} ? $rain - $ld->{last_rain} : $rain)) if $loop_count;
349 $ld->{last_rain} = $rain;
351 # what sort of packet is it?
352 my $sort = unpack("C", substr $blk,4,1);
356 $tmp = unpack("C", substr $blk,18,2);
357 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp/10));
358 $tmp = unpack("C", substr $blk,20,2);
359 # $h{Wind_Avg_2} = nearest(0.1,mph2mps($tmp/10));
360 $tmp = unpack("C", substr $blk,22,2);
361 # $h{Wind_Gust_10} = nearest(0.1,mph2mps($tmp/10));
363 # $h{Dir_Avg_10} = unpack("C", substr $blk,24,2)+0;
364 $tmp = unpack("C", substr $blk,30,2);
365 $h{Dew_Point} = nearest(0.1, f2c($tmp));
370 $tmp = unpack("C", substr $blk,15,1);
371 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp));
372 $h{Dew_Point} = nearest(0.1, dew_point($h{Temp_Out}, $h{Humidity_Out}));
373 $h{Rain_Month} = nearest(0.1, unpack("s", substr $blk,52,2) * $rain_mult);
374 $h{Rain_Year} = nearest(0.1, unpack("s", substr $blk,54,2) * $rain_mult);
379 my $dayno = int($ts/86400);
380 if ($dayno > $ld->{last_day}) {
381 $ld->{Temp_Out_Max} = $ld->{Temp_Out_Min} = $temp;
382 $ld->{last_day} = $dayno;
384 $ld->{Temp_Out_Max} = $temp if $temp > $ld->{Temp_Out_Max};
385 $ld->{Temp_Out_Min} = $temp if $temp < $ld->{Temp_Out_Min};
387 if ($ts >= $ld->{last_hour} + 1800) {
388 $h{Pressure_Trend} = unpack("C", substr $blk,3,1);
389 $h{Pressure_Trend_txt} = $bar_trend{$h{Pressure_Trend}};
390 $h{Batt_TX_OK} = (unpack("C", substr $blk,86,1)+0) ^ 1;
391 $h{Batt_Console} = nearest(0.01, unpack("s", substr $blk,87,2) * 0.005859375);
392 $h{Forecast_Icon} = unpack("C", substr $blk,89,1);
393 $h{Forecast_Rule} = unpack("C", substr $blk,90,1);
394 $h{Sunrise} = sprintf( "%04d", unpack("S", substr $blk,91,2) );
395 $h{Sunrise} =~ s/(\d{2})(\d{2})/$1:$2/;
396 $h{Sunset} = sprintf( "%04d", unpack("S", substr $blk,93,2) );
397 $h{Sunset} =~ s/(\d{2})(\d{2})/$1:$2/;
398 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
399 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
401 if ($loop_count) { # i.e not the first
402 my $a = wind_average(scalar @{$ld->{wind_hour}} ? @{$ld->{wind_hour}} : {w => $h{Wind}, d => $h{Dir}});
404 $h{Wind_1h} = nearest(0.1, $a->{w});
405 $h{Dir_1h} = nearest(0.1, $a->{d});
407 $a = wind_average(@{$ld->{wind_min}});
408 $h{Wind_1m} = nearest(0.1, $a->{w});
409 $h{Dir_1m} = nearest(1, $a->{d});
411 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
413 $ld->{last_rain_min} = $ld->{last_rain_hour} = $rain;
416 $s = genstr($ts, 'h', \%h);
417 $ld->{lasthour_h} = $s;
419 $ld->{last_hour} = int($ts/1800)*1800;
420 $ld->{last_min} = int($ts/60)*60;
421 @{$ld->{wind_hour}} = ();
422 @{$ld->{wind_min}} = ();
426 } elsif ($ts >= $ld->{last_min} + 60) {
427 my $a = wind_average(@{$ld->{wind_min}});
430 push @{$ld->{wind_hour}}, $a;
432 if ($loop_count) { # i.e not the first
435 $h{Wind_1m} = nearest(0.1, $a->{w});
436 $h{Dir_1m} = nearest(1, $a->{d});
437 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
439 $ld->{last_rain_min} = $rain;
441 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
442 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
445 $s = genstr($ts, 'm', \%h);
446 $ld->{lastmin_h} = $s;
448 $ld->{last_min} = int($ts/60)*60;
449 @{$ld->{wind_min}} = ();
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';
462 output_str($s) 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}|;
490 foreach my $ws (keys $WS) {
507 while (my ($k, $v) = each %$now) {
508 if ($last->{$k} ne $now->{$k}) {
513 return $count ? \%o : undef;
521 # Using the simplified approximation for dew point
522 # Accurate to 1 degree C for humidities > 50 %
523 # http://en.wikipedia.org/wiki/Dew_point
525 my $dewpoint = $temp - ((100 - $rh) / 5);
527 # this is the more complete one (which doesn't work)
531 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
532 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
539 # Expects packed data...
540 my $data_str = shift @_;
543 my @lst = split //, $data_str;
544 foreach my $data (@lst) {
545 my $data = unpack("c",$data);
548 my $index = $crc >> 8 ^ $data;
549 my $lhs = $crc_table[$index];
550 #print "lhs=$lhs, crc=$crc\n";
551 my $rhs = ($crc << 8) & 0xFFFF;
562 return ($_[0] - 32) * 5/9;
567 return $_[0] * 0.44704;
572 return $_[0] * 33.8637526;
577 my ($sindir, $cosdir, $wind);
582 $sindir += sin(d2r($r->{d})) * $r->{w};
583 $cosdir += cos(d2r($r->{d})) * $r->{w};
587 my $avhdg = r2d(atan2($sindir, $cosdir));
588 $avhdg += 360 if $avhdg < 0;
589 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
596 return ($n / pi) * 180;
603 return ($n / 180) * pi;
610 $ld->{rain24} ||= [];
612 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
613 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
614 my $Rain_1m = nearest(0.1, $rm);
615 push @{$ld->{rain24}}, $Rain_1m;
616 $ld->{rain_24} += $rm;
617 while (@{$ld->{rain24}} > 24*60) {
618 $ld->{rain_24} -= shift @{$ld->{rain24}};
620 my $Rain_24h = nearest(0.1, $ld->{rain_24});
621 return ($Rain_1m, $Rain_1h, $Rain_24h);
626 return unless $dataf;
631 dbg "read loop data: $s" if isdbg 'json';
632 $ld = $json->decode($s) if length $s;
634 # sort out rain stats
636 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
637 my $diff = 24*60 - $c;
638 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
643 $rain += $_ for @{$ld->{rain24}};
646 $ld->{rain_24} = nearest(0.1, $rain);
653 return unless $dataf;
658 my $s = $json->encode($ld);
659 dbg "write loop data: $s" if isdbg 'json';
663 sub cycle_loop_data_files
665 rename "$datafn.oooo", "$datafn.ooooo";
666 rename "$datafn.ooo", "$datafn.oooo";
667 rename "$datafn.oo", "$datafn.ooo";
668 rename "$datafn.o", "$datafn.oo";
669 copy $datafn, "$datafn.o";
675 % my $url = url_for 'weather';
678 <head><title>DWeather</title></head>
685 function process(key,value) {
686 var d = document.getElementById(key);
692 function traverse(o) {
696 if (o[i] !== null && typeof(o[i])=="object") {
703 ws = new WebSocket('<%= $url->to_abs %>');
704 document.body.innerHTML += 'ws connecting to: <%= $url->to_abs %> type_of: ' + typeof(ws) + '<br>';
705 if (typeof(ws) === 'object') {
706 ws.onmessage = function (event) {
707 var js = JSON.parse(event.data);
708 if (js !== null && typeof(js) === 'object') {
712 ws.onopen = function (event) {
713 ws.send('WebSocket support works! ♥');
716 document.body.innerHTML += 'Webserver only works with Websocket aware browsers';
722 <table border=1 width=80%>
724 <th>Time:<td><span id="tm"> </span>
725 <th>Sunrise:<td><span id="Sunrise"> </span>
726 <th>Sunset:<td><span id="Sunset"> </span>
727 <th>Console Volts:<td><span id="Batt_Console"> </span>
728 <th>TX Battery OK:<td><span id="Batt_TX_OK"> </span>
731 <th>Pressure:<td><span id="Pressure"> </span>
732 <th>Trend:<td><span id="Pressure_Trend_txt"> </span>
735 <th>Temperature in:<td> <span id="Temp_In"> </span>
736 <th>Humidity:<td> <span id="Humidity_In"> </span>
739 <th>Temperature out:<td> <span id="Temp_Out"> </span>
740 <th>Min:<td> <span id="Temp_Out_Min"> </span>
741 <th>Max:<td> <span id="Temp_Out_Max"> </span>
742 <th>Humidity:<td> <span id="Humidity_Out"> </span>
743 <th>Dew Point:<td> <span id="Dew_Point"> </span>
746 <th>Wind Direction:<td> <span id="Dir"> </span>
747 <th>Minute Avg:<td> <span id="Dir_1m"> </span>
748 <th>Speed:<td> <span id="Wind"> </span>
749 <th>Minute Avg:<td> <span id="Wind_1m"> </span>
752 <th>Rain 30mins:<td> <span id="Rain_1h"> </span>
753 <th>Day:<td> <span id="Rain_Day"> </span>
754 <th>24hrs:<td> <span id="Rain_24h"> </span>
755 <th>Month:<td> <span id="Rain_Month"> </span>
756 <th>Year:<td> <span id="Rain_Year"> </span>