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);
20 use constant pi => 3.14159265358979;
22 my $devname = "/dev/davis";
23 my $datafn = ".loop_data";
26 my $poll_interval = 2.5;
27 my $rain_mult = 0.2; # 0.1 or 0.2 mm or 0.01 inches
35 my $ser; # the serial port Mojo::IOLoop::Stream
39 our $json = JSON->new->canonical(1);
40 our $WS = {}; # websocket connections
44 our $loop_count; # how many LOOPs we have done, used as start indicator
47 0x0, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
48 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
49 0x1231, 0x210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
50 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
51 0x2462, 0x3443, 0x420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
52 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
53 0x3653, 0x2672, 0x1611, 0x630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
54 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
55 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x840, 0x1861, 0x2802, 0x3823,
56 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
57 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0xa50, 0x3a33, 0x2a12,
58 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
59 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0xc60, 0x1c41,
60 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
61 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0xe70,
62 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
63 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
64 0x1080, 0xa1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
65 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
66 0x2b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
67 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
68 0x34e2, 0x24c3, 0x14a0, 0x481, 0x7466, 0x6447, 0x5424, 0x4405,
69 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
70 0x26d3, 0x36f2, 0x691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
71 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
72 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x8e1, 0x3882, 0x28a3,
73 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
74 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0xaf1, 0x1ad0, 0x2ab3, 0x3a92,
75 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
76 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0xcc1,
77 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
78 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0xed1, 0x1ef0
83 $bar_trend{-60} = "Falling Rapidly";
84 $bar_trend{196} = "Falling Rapidly";
85 $bar_trend{-20} = "Falling Slowly";
86 $bar_trend{236} = "Falling Slowly";
87 $bar_trend{0} = "Steady";
88 $bar_trend{20} = "Rising Slowly";
89 $bar_trend{60} = "Rising Rapidly";
93 $SIG{TERM} = $SIG{INT} = sub {++$ending; Mojo::IOLoop->stop;};
97 # WebSocket weather service
98 websocket '/weather' => sub {
104 app->log->debug('WebSocket opened.');
105 dbg 'WebSocket opened' if isdbg 'chan';
108 # send historical data
109 $c->send($ld->{lasthour_h}) if exists $ld->{lasthour_h};
110 $c->send($ld->{lastmin_h}) if exists $ld->{lastmin_h};
113 $c->inactivity_timeout(3615);
119 dbg "websocket: text $msg" if isdbg 'chan';
123 dbg "websocket: json $msg" if isdbg 'chan';
128 $c->on(finish => sub {
129 my ($c, $code, $reason) = @_;
130 app->log->debug("WebSocket closed with status $code.");
131 dbg 'webwocket closed with status $code' if isdbg 'chan';
136 get '/' => {template => 'index'};
146 dbg "*** starting $0";
151 our $dlog = SMGLog->new("day");
152 dbg "before next tick";
153 Mojo::IOLoop->next_tick(sub { loop() });
154 dbg "before app start";
156 dbg "after app start";
159 $dataf->close if $dataf;
163 # move all the files along one
164 cycle_loop_data_files();
172 ##################################################################################
176 dbg "last_min: " . scalar gmtime($ld->{last_min});
177 dbg "last_hour: " . scalar gmtime($ld->{last_hour});
179 $did = Mojo::IOLoop->recurring(1 => sub {$dlog->flushall});
190 $d =~ s/([\%\x00-\x1f\x7f-\xff])/sprintf("%%%02X", ord($1))/eg;
191 dbg "read added '$d' buf lth=" . length $buf if isdbg 'raw';
192 if ($state eq 'waitnl' && $buf =~ /[\cJ\cM]+/) {
193 dbg "Got \\n" if isdbg 'state';
194 Mojo::IOLoop->remove($tid) if $tid;
198 $ser->write("LPS 1 1\n");
199 chgstate("waitloop");
200 } elsif ($state eq "waitloop") {
201 if ($buf =~ /\x06/) {
202 dbg "Got ACK 0x06" if isdbg 'state';
203 chgstate('waitlooprec');
206 } elsif ($state eq 'waitlooprec') {
207 if (length $buf >= 99) {
208 dbg "got loop record" if isdbg 'chan';
219 dbg "start_loop writing $nlcount \\n" if isdbg 'state';
221 Mojo::IOLoop->remove($tid) if $tid;
223 $tid = Mojo::IOLoop->recurring(0.6 => sub {
224 if (++$nlcount > 10) {
225 dbg "\\n count > 10, closing connection" if isdbg 'chan';
229 dbg "writing $nlcount \\n" if isdbg 'state';
237 dbg "state '$state' -> '$_[0]'" if isdbg 'state';
244 dbg "do reopen on '$name' ending $ending";
246 $ser = do_open($name);
250 Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
261 my $ob = Serial->new($name, 19200) || die "$name $!\n";
262 dbg "streaming $name fileno(" . fileno($ob) . ")" if isdbg 'chan';
264 my $ser = Mojo::IOLoop::Stream->new($ob);
265 $ser->on(error=>sub {dbg "serial $_[1]"; do_reopen($name) unless $ending});
266 $ser->on(close=>sub {dbg "serial closing"; do_reopen($name) unless $ending});
267 $ser->on(timeout=>sub {dbg "serial timeout";});
268 $ser->on(read=>sub {on_read(@_)});
271 Mojo::IOLoop->remove($tid) if $tid;
273 Mojo::IOLoop->remove($rid) if $rid;
275 $rid = Mojo::IOLoop->recurring($poll_interval => sub {
276 start_loop() if !$state;
290 my $loo = substr $blk,0,3;
291 unless ( $loo eq 'LOO') {
292 dbg "Block invalid loo -> $loo" if isdbg 'chan'; return;
300 my $crc_calc = CRC_CCITT($blk);
305 $tmp = unpack("s", substr $blk,7,2) / 1000;
306 $h{Pressure} = nearest(1, in2mb($tmp));
308 $tmp = unpack("s", substr $blk,9,2) / 10;
309 $h{Temp_In} = nearest(0.1, f2c($tmp));
311 $temp = nearest(0.1, f2c(unpack("s", substr $blk,12,2) / 10));
312 $h{Temp_Out} = $temp;
313 if ($temp > 75 || $temp < -75) {
314 dbg "LOOP Temperature out of range ($temp), record ignored";
318 $tmp = unpack("C", substr $blk,14,1);
319 $h{Wind} = nearest(0.1, mph2mps($tmp));
320 $h{Dir} = unpack("s", substr $blk,16,2)+0;
322 my $wind = {w => $h{Wind}, d => $h{Dir}};
323 $wind = 0 if $wind == 255;
324 push @{$ld->{wind_min}}, $wind;
326 $tmp = int(unpack("C", substr $blk,33,1)+0);
328 dbg "LOOP Outside Humidity out of range ($tmp), record ignored";
331 $h{Humidity_Out} = $tmp;
332 $tmp = int(unpack("C", substr $blk,11,1)+0);
334 dbg "LOOP Inside Humidity out of range ($tmp), record ignored";
337 $h{Humidity_In} = $tmp;
340 $tmp = unpack("C", substr $blk,43,1)+0;
341 $h{UV} = $tmp unless $tmp >= 255;
342 $tmp = unpack("s", substr $blk,44,2)+0; # watt/m**2
343 $h{Solar} = $tmp unless $tmp >= 32767;
345 # $h{Rain_Rate} = nearest(0.1,unpack("s", substr $blk,41,2) * $rain_mult);
346 $rain = $h{Rain_Day} = nearest(0.1, unpack("s", substr $blk,50,2) * $rain_mult);
347 my $delta_rain = $h{Rain} = nearest(0.1, ($rain >= $ld->{last_rain} ? $rain - $ld->{last_rain} : $rain)) if $loop_count;
348 $ld->{last_rain} = $rain;
350 # what sort of packet is it?
351 my $sort = unpack("C", substr $blk,4,1);
355 $tmp = unpack("C", substr $blk,18,2);
356 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp/10));
357 $tmp = unpack("C", substr $blk,20,2);
358 # $h{Wind_Avg_2} = nearest(0.1,mph2mps($tmp/10));
359 $tmp = unpack("C", substr $blk,22,2);
360 # $h{Wind_Gust_10} = nearest(0.1,mph2mps($tmp/10));
362 # $h{Dir_Avg_10} = unpack("C", substr $blk,24,2)+0;
363 $tmp = unpack("C", substr $blk,30,2);
364 $h{Dew_Point} = nearest(0.1, f2c($tmp));
369 $tmp = unpack("C", substr $blk,15,1);
370 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp));
371 $h{Dew_Point} = nearest(0.1, dew_point($h{Temp_Out}, $h{Humidity_Out}));
372 $h{Rain_Month} = nearest(0.1, unpack("s", substr $blk,52,2) * $rain_mult);
373 $h{Rain_Year} = nearest(0.1, unpack("s", substr $blk,54,2) * $rain_mult);
378 my $dayno = int($ts/86400);
379 if ($dayno > $ld->{last_day}) {
380 $ld->{Temp_Out_Max} = $ld->{Temp_Out_Min} = $temp;
381 $ld->{Temp_Out_Max_T} = $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
382 $ld->{last_day} = $dayno;
384 cycle_loop_data_files();
386 if ($temp > $ld->{Temp_Out_Max}) {
387 $ld->{Temp_Out_Max} = $temp;
388 $ld->{Temp_Out_Max_T} = clocktime($ts, 0);
391 if ($temp < $ld->{Temp_Out_Min}) {
392 $ld->{Temp_Out_Min} = $temp;
393 $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
397 if ($ts >= $ld->{last_hour} + 1800) {
398 $h{Pressure_Trend} = unpack("C", substr $blk,3,1);
399 $h{Pressure_Trend_txt} = $bar_trend{$h{Pressure_Trend}};
400 $h{Batt_TX_OK} = (unpack("C", substr $blk,86,1)+0) ^ 1;
401 $h{Batt_Console} = nearest(0.01, unpack("s", substr $blk,87,2) * 0.005859375);
402 $h{Forecast_Icon} = unpack("C", substr $blk,89,1);
403 $h{Forecast_Rule} = unpack("C", substr $blk,90,1);
404 $h{Sunrise} = sprintf( "%04d", unpack("S", substr $blk,91,2) );
405 $h{Sunrise} =~ s/(\d{2})(\d{2})/$1:$2/;
406 $h{Sunset} = sprintf( "%04d", unpack("S", substr $blk,93,2) );
407 $h{Sunset} =~ s/(\d{2})(\d{2})/$1:$2/;
408 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
409 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
410 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
411 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
414 if ($loop_count) { # i.e not the first
415 my $a = wind_average(scalar @{$ld->{wind_hour}} ? @{$ld->{wind_hour}} : {w => $h{Wind}, d => $h{Dir}});
417 $h{Wind_1h} = nearest(0.1, $a->{w});
418 $h{Dir_1h} = nearest(0.1, $a->{d});
420 $a = wind_average(@{$ld->{wind_min}});
421 $h{Wind_1m} = nearest(0.1, $a->{w});
422 $h{Dir_1m} = nearest(1, $a->{d});
424 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
426 $ld->{last_rain_min} = $ld->{last_rain_hour} = $rain;
429 $s = genstr($ts, 'h', \%h);
430 $ld->{lasthour_h} = $s;
432 $ld->{last_hour} = int($ts/1800)*1800;
433 $ld->{last_min} = int($ts/60)*60;
434 @{$ld->{wind_hour}} = ();
435 @{$ld->{wind_min}} = ();
437 output_str($s, 1) if $s;
440 } elsif ($ts >= $ld->{last_min} + 60) {
441 my $a = wind_average(@{$ld->{wind_min}});
444 push @{$ld->{wind_hour}}, $a;
446 if ($loop_count) { # i.e not the first
449 $h{Wind_1m} = nearest(0.1, $a->{w});
450 $h{Dir_1m} = nearest(1, $a->{d});
451 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
453 $ld->{last_rain_min} = $rain;
455 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
456 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
457 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
458 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
461 $s = genstr($ts, 'm', \%h);
462 $ld->{lastmin_h} = $s;
464 $ld->{last_min} = int($ts/60)*60;
465 @{$ld->{wind_min}} = ();
467 output_str($s, 1) if $s;
471 my $o = gen_hash_diff($ld->{last_h}, \%h);
473 $s = genstr($ts, 'r', $o);
476 dbg "loop rec not changed" if isdbg 'chan';
478 output_str($s, 0) if $s;
483 dbg "CRC check failed for LOOP data!";
494 my $j = $json->encode($h);
495 my $tm = clocktime($ts, 1);
496 return qq|{"tm":"$tm","t":$ts,"$let":$j}|;
503 my ($sec,$min,$hr) = (gmtime $ts)[0,1,2];
506 $s = sprintf "%02d:%02d:%02d", $hr, $min, $sec;
508 $s = sprintf "%02d:%02d", $hr, $min;
520 $dlog->writenow($s) if $logit;
521 foreach my $ws (keys $WS) {
538 while (my ($k, $v) = each %$now) {
539 if (!exists $last->{$k} || $last->{$k} ne $now->{$k}) {
544 return $count ? \%o : undef;
552 # Using the simplified approximation for dew point
553 # Accurate to 1 degree C for humidities > 50 %
554 # http://en.wikipedia.org/wiki/Dew_point
556 my $dewpoint = $temp - ((100 - $rh) / 5);
558 # this is the more complete one (which doesn't work)
562 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
563 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
570 # Expects packed data...
571 my $data_str = shift @_;
574 my @lst = split //, $data_str;
575 foreach my $data (@lst) {
576 my $data = unpack("c",$data);
579 my $index = $crc >> 8 ^ $data;
580 my $lhs = $crc_table[$index];
581 #print "lhs=$lhs, crc=$crc\n";
582 my $rhs = ($crc << 8) & 0xFFFF;
593 return ($_[0] - 32) * 5/9;
598 return $_[0] * 0.44704;
603 return $_[0] * 33.8637526;
608 my ($sindir, $cosdir, $wind);
613 $sindir += sin(d2r($r->{d})) * $r->{w};
614 $cosdir += cos(d2r($r->{d})) * $r->{w};
618 my $avhdg = r2d(atan2($sindir, $cosdir));
619 $avhdg += 360 if $avhdg < 0;
620 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
627 return ($n / pi) * 180;
634 return ($n / 180) * pi;
641 $ld->{rain24} ||= [];
643 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
644 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
645 my $Rain_1m = nearest(0.1, $rm);
646 push @{$ld->{rain24}}, $Rain_1m;
647 $ld->{rain_24} += $rm;
648 while (@{$ld->{rain24}} > 24*60) {
649 $ld->{rain_24} -= shift @{$ld->{rain24}};
651 my $Rain_24h = nearest(0.1, $ld->{rain_24});
652 return ($Rain_1m, $Rain_1h, $Rain_24h);
658 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
659 $dataf->autoflush(1);
665 dbg "read loop data: $s" if isdbg 'json';
666 $ld = $json->decode($s) if length $s;
668 # sort out rain stats
670 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
671 my $diff = 24*60 - $c;
672 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
677 $rain += $_ for @{$ld->{rain24}};
680 $ld->{rain_24} = nearest(0.1, $rain);
688 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
689 $dataf->autoflush(1);
695 my $s = $json->encode($ld);
696 dbg "write loop data: $s" if isdbg 'json';
700 sub cycle_loop_data_files
702 $dataf->close if $dataf;
705 rename "$datafn.oooo", "$datafn.ooooo";
706 rename "$datafn.ooo", "$datafn.oooo";
707 rename "$datafn.oo", "$datafn.ooo";
708 rename "$datafn.o", "$datafn.oo";
709 copy $datafn, "$datafn.o";
715 % my $url = url_for 'weather';
719 <title>DWeather</title>
720 <meta charset="utf-8">
721 <meta http-equiv="X-UA-Compatible" content="IE=edge">
722 <meta name="viewport" content="width=device-width, initial-scale=1">
724 <!-- Latest compiled and minified CSS -->
725 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
727 <!-- Optional theme -->
728 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
732 <center><h1>High View Weather</h1></center>
736 var h = new Object();
738 function update_h(key, value) {
742 function fill_html(key,value) {
743 var d = document.getElementById(key);
749 function traverse(o, func) {
752 if (o[i] !== null && typeof(o[i])=="object") {
753 traverse(o[i], func);
761 ws = new WebSocket('<%= $url->to_abs %>');
763 if (typeof(ws) !== null) {
764 ws.onmessage = function (event) {
765 var js = JSON.parse(event.data);
766 if (js !== null && typeof(js) === 'object') {
767 traverse(js, fill_html);
768 traverse(js, update_h);
769 document.getElementById("hh").innerHTML = JSON.stringify(h);
772 ws.onopen = function (event) {
773 document.getElementById("wsconnect").innerHTML = 'ws connected to: <%= $url->to_abs %>';
774 ws.send('WebSocket support works!');
776 ws.onclose = function(event) {
777 document.getElementById("wsconnect").innerHTML = 'ws disconnected, refresh to restart';
781 document.body.innerHTML += 'Webserver only works with Websocket aware browsers';
785 window.onload = function() {
787 window.setInterval(function() {
795 <div id="start-template">
797 <table border=1 width=80% align="center">
799 <th>Time:<td><span id="tm"> </span>
800 <th>Sunrise:<td><span id="Sunrise"> </span>
801 <th>Sunset:<td><span id="Sunset"> </span>
802 <th>Console Volts:<td><span id="Batt_Console"> </span>
803 <th>TX Battery OK:<td><span id="Batt_TX_OK"> </span>
806 <th>Pressure:<td><span id="Pressure"> </span>
807 <th>Trend:<td><span id="Pressure_Trend_txt"> </span>
810 <th>Temperature in:<td> <span id="Temp_In"> </span>
811 <th>Humidity:<td> <span id="Humidity_In"> </span>
814 <th>Temperature out:<td> <span id="Temp_Out"> </span>
815 <th>Min:<td> <span id="Temp_Out_Min"> </span> @ <span id="Temp_Out_Min_T"> </span>
816 <th>Max:<td> <span id="Temp_Out_Max"> </span> @ <span id="Temp_Out_Max_T"> </span>
817 <th>Humidity:<td> <span id="Humidity_Out"> </span>
818 <th>Dew Point:<td> <span id="Dew_Point"> </span>
821 <th>Wind Direction:<td> <span id="Dir"> </span>
822 <th>Minute Avg:<td> <span id="Dir_1m"> </span>
823 <th>Speed:<td> <span id="Wind"> </span>
824 <th>Minute Avg:<td> <span id="Wind_1m"> </span>
827 <th>Rain 30mins:<td> <span id="Rain_1h"> </span>
828 <th>Day:<td> <span id="Rain_Day"> </span>
829 <th>24hrs:<td> <span id="Rain_24h"> </span>
830 <th>Month:<td> <span id="Rain_Month"> </span>
831 <th>Year:<td> <span id="Rain_Year"> </span>
834 <div id="wsconnect" align="center"> </div>
836 <div id="hh" align="center"> </div>
839 <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
840 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
841 <!-- Latest compiled and minified JavaScript -->
842 <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>