Home » Linux Magazine » Weather information to the Web using Linux
webwethrimg

Weather information to the Web using Linux

Chris Howard

Issue #56, December 1998

Mr. Howard tells us how he gathers and outputs weather information to the Web using Linux, Perl and automated FTP.

Last fall, my family and I moved from central Iowa to the little mountain resort town of Estes Park, Colorado. Estes Park is a beautiful town at the east entrance of Rocky Mountain National Park. More than three million tourists visit the park each summer.

When we moved here, I brought along my fledgling consulting business, Daylight Software, and set up web pages to drum up a little work. In a flash of inspiration, I decided I would either buy or build weather station equipment and offer weather data on the Web. Visitors from around the nation—and the globe—would see my web pages, and Daylight Software would be established as a Linux consulting powerhouse. Well, maybe it wouldn’t lead to global domination, but it would surely be a good thing. I saw it as a community service, since no public weather reporting service was available, other than the time and temperature sign on one of the local banks.

After some investigation, I decided that my hardware development skills were not sufficient to design and build weather sensors. I shopped around and eventually purchased a Texas Weather Instruments “Weather Report” WRL-25 system from American Weather Enterprises of Media, PA (http://www.americanweather.com/).

The WRL-25 is like many weather stations in that it includes an RS-232 connection and comes with DOS/Windows software for downloading and viewing the gathered data. It has sensors for wind speed, wind direction, temperature, humidity, atmospheric pressure and rainfall. Using a regular television antenna mast and fittings, I mounted the weather station sensors on the roof of my house, snaking the sensor cables through the attic space and down to my office in the basement. I mounted the handsome display unit on a shelf in the office.

Across the room from the weather display is my array of computers, including my Linux workstation. I am running the Red Hat 4.0 Colgate release, 2.0.18 kernel, on my AMD 486DX-4 120MHz ISA server. I built a cable and attached the weather display to the Boca AT-66 serial card in the server. I then wrote some Perl scripts to build HTML and GIF files and upload them to my ISP, and to manage the data readings put out by the weather station.

Equipment Installation

The sensors are mounted on two ten-foot sections of steel TV antenna mast, available in the U.S. from stores such as Radio Shack. Approximately four feet are buried in the ground, and the rest of the mast is vertical at the gable end of our single-story house. The mast is attached with a TV mast bracket at the roof line of the house. About six feet of mast projects above the roof line.

Figure 1. Weather Sensors on TV Antenna Mast

 

 

 

 

 

 

 

 

 

 

The sensors came supplied with clamps and hardware to attach them to the mast. (See Figure 1.) I followed the installation instructions and mounted the wind direction/speed sensor module pointed north at the top of the mast. The temperature and humidity sensors are in a “pagoda” enclosure to protect them from direct sunlight, and the pagoda is mounted about three feet above the roof line. The rain collector is mounted at the roof line; I used a carpenter’s level to mount it properly.

Figure 2. Wires from Junction Box into House

 

 

 

 

 

 

 

 

 

Multi-wire cables from the sensor modules go into the bottom of the junction box, where they are plugged into matching connectors from 100-foot cables which run to the weather display unit. (See Figure 2.) All excess cable is coiled up and attached securely under the eave. Cables going into the junction box were left drooping slightly, to encourage rain to drip off instead of flowing into the junction box.

The 100-foot cables run through the attic and out through a hole at the peak of the eave. The hole was later filled with caulking to discourage squirrels and other pests from getting in. From the attic, I drilled a hole in the top plate of an interior wall and snaked the cables down to the basement. A fancy wooden switchplate made for cable TV installations serves to mask the hole where the cables come through the wall into my office/computer area. The cables connect to the back of the WRL-25 display unit.

I ended up crawling through the blown-in fiberglass insulation in the attic more times than I care to remember. If you do this, be careful—wear a dust mask or respirator and long sleeves and trousers. The attic was hot. I considered this to be the most difficult part of the installation.

RS-232 Connection and WRL-25 Configuration

I had to set the current time on the display unit and change some of the option settings. Other than that, the sensors and other features of the WRL-25 were all calibrated and ready to run.

On the back of the display unit is a 9-pin serial pigtail. The WRL-25 came with a mix-and-match 4-ended 25 and 9 pin serial cable for adapting to whatever connectors are available. I did not use that particular cable. My Boca Io-AT-66 six-port serial card uses RJ-45 sockets for its connections. So, I made a cable from a ten-foot scrap of eight-wire twisted pair that already had an RJ-45 connector crimped on one end. Pin assignments and wiring information were included in the Io-AT-66 documentation.

The WRL-25 can be programmed to periodically send a status report over the serial line. This can be used to print directly on a serial printer. I programmed my unit to send a reading every 5 minutes. I also changed an optional setting to allow the rainfall rate report and to print a daily Max/Min report at the end of each day. I set the serial line data rate to 9600bps.

The unit can also be directed using single-character commands through the serial connection. I rarely use this feature. The PC software that came with the unit can be used to set various options and settings, as can the buttons on the display panel. So, more sophisticated programming could be used to query the unit on demand or change various features. I elected to use the simple logging feature to gather my data.

I was able to run a quick test on my setup by using the following command:

cat < /dev/ttyS19

This command takes any input appearing on the /dev/ttyS19 serial line and echoes it to the screen. Pushing the manual report button on the WRL-25 produced a one-line report on the screen. I was a happy camper!

WRL-25 Data Report Format

Reports that come down the serial line look like this:

17:05 08/09/97 WSW 00MPH 460F 069F 057F 085% 23.42F 00.00"D
01.39"M 00.00"R

The first two fields are time and date. The current time and date are maintained on the WRL-25 display unit. I have not noticed any great drift in the time setting.

The second two fields are wind direction (WSW—West by South West) and speed (00 MPH—miles per hour).

Fields 5 through 7 are temperature readings in degrees Fahrenheit. Field 5 is not connected to any sensor, so it should be ignored. Field 6 is indoor temperature and field 7 is outdoor temperature.

Field 8 is relative humidity, which in this example is 85%. Field 9 is atmospheric pressure in inches of mercury accompanied by a single character for falling (F), rising (R) or steady (S).

The last three fields are daily rainfall and monthly rainfall, both in inches, and rainfall rate in inches per hour.

At the end of each day, two lines of daily minimum and maximum readings are reported:

Max 08/08/97 WSW 24MPH 460F 074F 081F 100% 23.42" 00.00"D
01.39"M 00.00"R
Min 08/08/97 SW 00MPH 460F 068F 044F 021% 23.28" 00.00"D
01.39"M 00.00"R

These lines are in the same format as the other reports, except for the first field which marks these records as Max/Min reports. The readings for daily min/max are independent. In the above example, the high temperature for the day was 81 degrees F and the highest humidity reading was 100%. These readings did not occur at the same time and are unrelated, except that both are the maximum for that particular statistic.

Programming for Data Collection

My first pass at a data collection script was a simple cat command:

cat /dev/ttyS19 >> data1

It worked—mainly by accident. The next time my machine was rebooted, it didn’t work at all. When I fired it up, the weather station console started spitting out all sorts of long reports. After a little head scratching, it became obvious that regular character echoing was feeding back command characters—not at all what I had in mind.

Listing 1. Data Collection Script

# lines associated with same number are treated
# as one line. Blank lines have been deleted.
1 #!/usr/bin/perl
3 use FileHandle;
5 $DATADIR="/home/weather/data";
7 if ($#ARGV < 0) { die "Usage: weatherd tty\n";
   }
8 $TTY = $ARGV[0];
10 system(
   "stty `cat /home/weather/bin/tty` < $TTY");
11 print "stty returned $?\n";
13 # Filename for today's data
14 ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,
   $isdst) =
15    localtime(time);
16 $TODAY= sprintf "%02d%02d%02d", $mon+1,
   $mday, $year;
20 $OUTFILE = "$DATADIR/$TODAY";
21 open(LINE, "< $TTY") or die
   "Unable to open tty: $TTY";
22 open(OUT, ">> $OUTFILE") or die
   "Unable to open output file: $OUTFILE";
24 LINE->autoflush(1);
25 OUT->autoflush(1);
27 while(<LINE>)
28 {
30    # parse out date, make sure it matches $TODAY
32    ($time, $date, $wdir, $wspeed, $aux, $intemp,
33       $outtemp, $hum, $bp, $raind,<\n>
   $rainm, $rain_rate) = split;
35    $date =~ s/\///g;
37    if ( $time =~ /Min/ ) {
38       #system(" echo \'$_\' ><\n>
   $DATADIR/$date.min ");
40       open(MIN, ">> $DATADIR/$date.min")
41        or die "Unable to open output<\n>
   file: $DATADIR/$date.min";
42       printf MIN "%s", $_;
43       close MIN;
44    }
45    elsif ( $time =~ /Max/ ) {
46       #system(" echo \'$_\' > $DATADIR/$date.max ");
48       open(MAX, ">> $DATADIR/$date.max")
49        or die "Unable to open output<\n>
   file: $DATADIR/$date.max";
50       printf MAX "%s", $_;
51       close MAX;
52    }
53    else
54    {
55       $date =~ s/\///g;
57       if( $TODAY eq $date )
58       {
59          printf OUT "%s",$_;
60       }
61       else
62       {
63          $TODAY = $date;
65          # if not, close this file<\n>
            and open the next one
66          close OUT;
67          $OUTFILE =<\n>
   "$DATADIR/$TODAY";
68          open(OUT, ">> $OUTFILE")
69             or die "Unable to<\n>
   open output file: $OUTFILE";
71          OUT->autoflush(1);
72          printf OUT "%s",$_;
73       }
74    }

My current script is called weatherd.pl and is shown in Listing 1 with blank lines removed. Line 8 sets the variable $TTY to be the first command-line argument. Line 10 resets the terminal to the appropriate speed, parity and non-echoing. I used the stty command to get the terminal settings the way I wanted them, then saved the setup to a file. This stty command reads from that file and sets the terminal to the saved configuration.

Data is kept in a file, with the name of the file being the current date. Each day’s data goes into one file (lines 14-16). The print statement on line 16 helps me feel confident things are working right. Beginning with line 27, for each data line that comes in from the tty, we check to see if it is a minimum or maximum and that it is still today’s data. Minimum and maximum data go into separate files.

To start weatherd.pl, I added the following single line to my /etc/inittab file:

ws:2345:respawn:/home/weather/bin/weatherd.pl /dev/ttyS19

This line starts up weatherd.pl and respawns it if it should die for any reason.

Programming for Data Display on WWW Page

Now I had the data coming in from the weather station, getting picked up by weatherd.pl and thrown into a file using the date as its name.

Listing 2. Perl Script

#
# ws.pl -- Take data and HTML template and produce
#          an output HTML file.
#

$TEMPLATE='/home/weather/WWW/wsTemplate.html';

$twentyfourhours = 86400;  #seconds in a day

# Filename for today's data
$DATADIR="/home/weather/data";
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
   localtime(time);
$TODAY= sprintf "%02d%02d%02d", $mon+1, $mday, $year;
$DATAFILE="$DATADIR/$TODAY";

# Calculate yesterday's filename
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
   localtime(time-$twentyfourhours);
$YESTERDAY= sprintf "%02d%02d%02d", $mon+1, $mday, $year;
$MYESTERDAYFILE="$DATADIR/$YESTERDAY.max";
$mYESTERDAYFILE="$DATADIR/$YESTERDAY.min";

$CBP_CORRECTION = 6.68;  # Correction for atmospheric pressure
          # derived with help from local weather expert

# Match these patterns in the template file.
# Fill in the appropriate information
#  from the latest data record.
# %TIME% %DATE%
# %FTEMP%  (Farenheit) %CTEMP% (Celcius) %HUMIDITY%
# %BP% %CBP% (pressure and corrected pressure)
# %WSPEED% %KWSPEED% %WDIRECTION%  (speed in MPH and KPH)
# %RAIN%
# YM - yesterday Max, Ym - yesterday Min
# %YMTEMP% %YmTEMP% %YMHUMIDITY% %YmHUMIDITY%
# %YRAIN%

if( -r $DATAFILE)
{
   $lastline = `tail -1 $DATAFILE`;
}
else
{
   die "No current datafile $DATAFILE";
}

($time, $date, $wdir, $wspeed, $aux, $intemp, $outtemp, $hum, $bp,
 $raind, $rainm, $rain_rate) = split(' ',$lastline);

# Get stuff from yesterday's Max file
if( -r $MYESTERDAYFILE)
{
   $maxline = `tail -1 $MYESTERDAYFILE`;
}
($Max, $Mdate, $Mwdir, $Mwspeed, $Maux, $Mintemp, $Mouttemp, $Mhum, $Mbp,
 $Mraind, $Mrainm, $Mrain_rate) = split(' ',$maxline);

# Get stuff from yesterday's Min file
if( -r $mYESTERDAYFILE)
{
   $minline = `tail -1 $mYESTERDAYFILE`;
}
($max, $mdate, $mwdir, $mwspeed, $maux, $mintemp, $mouttemp, $mhum, $mbp,
 $mraind, $mrainm, $mrain_rate) = split(' ',$minline);


# I do some messaging of the data to remove some indicators
#  and expand others.
$wspeed =~ s/MPH//;
$wdir =~ s/E/east /g;
$wdir =~ s/N/north /g;
$wdir =~ s/W/west /g;
$wdir =~ s/S/south /g;
$outtemp =~ s/F//;
$outtemp =~ s/^0//;
$Mouttemp =~ s/F//;
$Mouttemp =~ s/^0//;
$mouttemp =~ s/F//;
$mouttemp =~ s/^0//;
$intemp =~ s/F//;
$intemp =~ s/^0//;
$hum =~ s/%//;
$hum =~ s/^0//;
$mhum =~ s/%//;
$mhum =~ s/^0//;
$Mhum =~ s/%//;
$Mhum =~ s/^0//;
$bpdir = "rising" if ($bp =~ /R/);
$bpdir = "steady" if ($bp =~ /S/);
$bpdir = "falling" if ($bp =~ /F/);
$bp =~ s/[A-Z]//;
$raind =~ s/\"D//;
$Mraind =~ s/\"D//;
$rainm =~ s/\"M//;
$rain_rate =~ s/\"R//;

$outctemp = sprintf("%3.1f",ftoc($outtemp));
$dewpoint = sprintf("%3.0f",dewpoint($outtemp,$hum));

$cbp = &cbp($bp);

$kwspeed = sprintf("%2.0f", $wspeed * 1.609);

open(T,"$TEMPLATE") or die "no such file $TEMPLATE\n";

while (<T>)
{
   # For each line in the template file...
   # Do the actual template replacements
   s/%TIME%/$time/;
   s/%DATE%/$date/;
   s/%FTEMP%/$outtemp/;
   s/%CTEMP%/$outctemp/;
   s/%HUMIDITY%/$hum/;
   s/%DEWPOINT%/$dewpoint/;
   s/%CBP%/$cbp $bpdir /;
   s/%BP%/$bp/;
   s/%WSPEED%/$wspeed/;
   s/%KWSPEED%/$kwspeed/;
   s/%WDIRECTION%/$wdir/;
   s/%RAIN%/$raind/;
   s/%YMHUMIDITY%/$Mhum/;
   s/%YmHUMIDITY%/$mhum/;
   s/%YMTEMP%/$Mouttemp/;
   s/%YmTEMP%/$mouttemp/;
   s/%YRAIN%/$Mraind/;

   # Print this doctored-up line.
   print $_;
}

# Subroutine for correction of atmospheric pressure
sub cbp
{
   local($in) = @_;
   return(sprintf("%2.2f", $in + $CBP_CORRECTION));
}

# Subroutines for F-->C  and C-->F temp conversions
sub ctof
{
   local ($C) = @_;
   $F = (9/5 * $C) + 32;
   return $F;
}

sub ftoc
{
   local ($F) = @_;
   $C = 5/9 * ($F - 32);
   return $C;
}

# Subroutine to calculate dewpoint
sub dewpoint
{
   local ($F, $H) = @_;

   $rh = $H/100; # RH. Fraction, [0..1] not %.
   $t = ftoc($F); #Celsius Temperature

   $es = 6.112 * exp(17.67 * $t / ($t + 243.5));
   $e = $rh * $es;
   $loge = log($e/6.112);
   $dp = 243.5 * $loge/(17.67 - $loge);
        return ctof($dp);
}

The next step was to format the data into an HTML file for display over the Web. I wrote a Perl script (Listing 2) that takes the last line from the current data file, in combination with a template HTML file, and fills in the weather information. It also calculates the corrected atmospheric pressure and the dew point. (The dew point calculation was given to me by John Kleist, Colorado Climate Center, johnk@loki.atmos.colostate.edu.) Last of all, it looks in yesterday’s Max/Min files and puts those values in the output HTML.

Listing 3. plotdays.pl

#!/usr/bin/perl
#
#
# plotdays.pl
#
# This guy uses the PGPLOT module, which in turn is based
# on the pgplot FORTRAN libraries...  It's a long story, but
# it works.  The goal is to produce some GIF format files
# of interesting statistics for the last X days.
#
use PGPLOT;  # Load PGPLOT module

require "timelocal.pl";

$DATADIR = "/home/weather/data";

$twentyfourhours = 86400;  #seconds in a day

@daysofweek = ('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday');

# Get the number of days of interest
if( $#ARGV >= 0 ) { $days=$ARGV[0]; }
else { $days = 7; }

$maxouttemp = 0;  $minouttemp = 100;
$maxbp = 0;  $minbp = 100;
$maxraind = 0;  $minraind = 100;
$maxrainrate = 0;  $minrainrate = 100;
$maxwinds = 0;  $minwinds = 100;

# Loop through the number of days we are interested in.
# My loop index _decreases_, day X is further back
#  in history than X-1.
#
# In true FORTRAN style, we are keeping parallel arrays for
#  each statistic and label.  And $j holds the total number
#  of data points for each statistic's array.
$j = 0;
for($i=$days; $i >= 0; $i-- )
{
   # Caculate file name for that day in the past.
   $dtime = time();
   $ttime = $dtime-($twentyfourhours * $i);
   @time = localtime($ttime);

   $DAYFILE = sprintf "%s/%02d%02d%02d",
      $DATADIR, $time[4]+1, $time[3], $time[5];

   $daylabel[$i] = sprintf "%s", $daysofweek[$time[6]];
   $daydate[$i] = sprintf "%2d/%02d/%02d",$time[4]+1,$time[3],$time[5];

   if( -r "$DAYFILE" )
   {
      # Open that day's file and read the data into local arrays
      # We are interested in outtemp, humidity,
      # pressure, rainfall, and windspeed.
      open(IN, "$DAYFILE") or die "Cannot open file $DAYFILE";
      while (<IN>)
      {
         ($time, $date, $wdir, $winds, $aux, $intemp,
         $outtemp, $hum, $bp, $raind, $rainm, $rainrate)
            = split;

         # Just in case a Min or Max slips in somehow
         if( $time =~ /[M]/ ) { next; }

         ($hour,$min) = split(/:/,$time);

         # Make basetime be the time in seconds at
         #  first of the day on that recent day in question
         # We want to end up with a Unix-style long integer
         #  time value for each data point.
         $time[0] = $time[1] = $time[2] = 0;
         $basetime[$i] = timelocal(@time);

         $today_time = (((60*$hour)+$min)*60);
         $readingtime = $basetime[$i] + $today_time;
         $readingtime = $readingtime - $basetime[$days];

         # This is the X value for each plot
         $xval[$j] = $readingtime;

         $outtemp[$j] = $outtemp;
         if( $outtemp > $maxouttemp ) { $maxouttemp = $outtemp; }
         if( $outtemp < $minouttemp ) { $minouttemp = $outtemp; }

         $dp[$j] = &dewpoint($outtemp,$hum);

         $bp[$j] = $bp;
         if( $bp > $maxbp ) { $maxbp = $bp; }
         if( $bp < $minbp ) { $minbp = $bp; }

         $raind[$j] = $raind;
         if( $raind > $maxraind ) { $maxraind = $raind; }
         if( $raind < $minraind ) { $minraind = $raind; }

         $rainrate[$j] = $rainrate;
         if( $rainrate > $maxrainrate )
            { $maxrainrate = $rainrate; }
         if( $rainrate < $minrainrate )
            { $minrainrate = $rainrate; }

         $winds[$j] = $winds;
         if( $winds > $maxwinds ) { $maxwinds = $winds; }
         if( $winds < $minwinds ) { $minwinds = $winds; }

         $j++;
      }

      close IN;
   }
}

# Then I just create the plots using a whole bunch
#  of calls to pgplot routines.  Not real pretty, but
#  it gets the job done.

#
# Temp and dewpoint plot
#
$dev = "temp.gif/GIF";
$dev = "?" unless defined $dev;

pgbegin(0,$dev,1,1);  # Open plot device
pgpap(7.0,0.55);
pgscr(1,0.75,0.75,0.75);
pgscr(5,0.40,0.70,0.40);
pgscr(0,1.0,1.0,1.0);
pgscf(1);             # Set character font
pgslw(1);             # Set line width
pgsch(1.2);           # Set character height
pglabel("","",""); # Labels

$top = (int(($maxouttemp)/10)+1) * 10 ;
$bottom = (int(($minouttemp)/10)) * 10 ;
$left = 0;
$right = ($basetime[0] + $twentyfourhours) - $basetime[$days];
pgswin($left,$right,$bottom,$top);
pgbox( 'ABCGI',$twentyfourhours,0,'ABCGINMVS',10,2);
pgsci(0);                # Change colour
pgdraw($xval[0],$outtemp[0]);

pgsci(2);                # Change colour
for( $i=0; $i < $j; $i++)
{
   pgdraw($xval[$i],$outtemp[$i]);
}

pgsci(2);                # Change colour
pgptxt(0, $top+3.5 ,0,0,"Temperature (F)");
pgsci(4);                # Change colour
pgptxt(0, $top+1 ,0,0,"Dewpoint (F)");
pgsci(5);                # Change colour
pgptxt(int(($right-$left)/2), $top+3.5 ,0,0, "$time $daylabel[0] $date ");

pgsci(0);                # Change colour
pgdraw($xval[0],$dp[0]);

pgsci(4);                # Change colour
for( $i=0; $i < $j; $i++)
{
   pgdraw($xval[$i],$dp[$i]);
}

pgsci(5);                # Change colour
pgsch(1.20);           # Set character height

for( $i=$days; $i >= 0; $i-- )
{
   $offset = (($days-$i) * $twentyfourhours) + int($twentyfourhours/2);
   pgptxt($offset, $bottom-2 ,0,0.5,$daylabel[$i]);
   pgptxt($offset, $bottom-4 ,0,0.5,$daydate[$i]);
}
pgend;    # Close plot

###
#
# Atmospheric Pressure Plot
#
$dev = "pressure.gif/GIF";
pgbegin(0,$dev,1,1);  # Open plot device
pgpap(7.0,0.55);
pgscr(1,0.75,0.75,0.75);
pgscr(5,0.40,0.70,0.40);
pgscr(0,1.0,1.0,1.0);
pgscf(1);             # Set character font
pgslw(1);             # Set line width
pgsch(1.2);           # Set character height

pglabel("","",""); # Labels

$top = (int($maxbp * 2) + 1) / 2;
$bottom = (int($minbp * 2)) / 2;
$left = 0;
$right = ($basetime[0] + $twentyfourhours) - $basetime[$days];
pgswin($left,$right,$bottom,$top);
pgbox( 'ABCGI',$twentyfourhours,0,'ABCGINMVS',.5,2);

pgsci(0);                # Change colour
pgdraw($xval[0],$bp[0]);
pgsci(2);                # Change colour
for( $i=0; $i < $j; $i++)
{
   pgdraw($xval[$i],$bp[$i]);
}

pgsci(2);                # Change colour
pgptxt(0, $top+.05 ,0,0,"Pressure (inches of mercury)");
pgsci(5);                # Change colour
pgptxt(int(($right-$left)/2), $top+.05 ,0,0, "$time $daylabel[0] $date ");

pgsci(5);                # Change colour
pgsch(1.20);           # Set character height

for( $i=$days; $i >= 0; $i-- )
{
   $offset = (($days-$i) * $twentyfourhours) + int($twentyfourhours/2);
   pgptxt($offset, $bottom-.05 ,0,0.5,$daylabel[$i]);
   pgptxt($offset, $bottom-.10 ,0,0.5,$daydate[$i]);
}
pgend;    # Close plot

###
#
# Daily Rainfall and Rainfall Rate
#
$dev = "raind.gif/GIF";
pgbegin(0,$dev,1,1);  # Open plot device
pgpap(7.0,0.55);
pgscr(1,0.75,0.75,0.75);
pgscr(5,0.40,0.70,0.40);
pgscr(0,1.0,1.0,1.0);
pgscf(1);             # Set character font
pgslw(1);             # Set line width
pgsch(1.2);           # Set character height

pglabel("","",""); # Labels

if( $maxraind > $maxrainrate ) { $maxrain = $maxraind; }
else { $maxrain = $maxraind; }

if( $minraind > $minrainrate ) { $minrain = $minraind; }
else { $minrain = $minraind; }

$top = (int($maxrain * 2) + 1) / 2;
$bottom = (int($minrain * 2)) / 2;
$left = 0;
$right = ($basetime[0] + $twentyfourhours) - $basetime[$days];
pgswin($left,$right,$bottom,$top);
pgbox( 'ABCGI',$twentyfourhours,0,'ABCGINMVS',.5,2);

pgsci(0);                # Change colour
pgdraw($xval[0],$raind[0]);

pgsci(2);                # Change colour
for( $i=0; $i < $j; $i++)
{
   pgdraw($xval[$i],$raind[$i]);
}

pgsci(4);                # Change colour
for( $i=0; $i < $j; $i++)
{
   # Just print positive data points for rainrate
   if( $rainrate[$i] > 0 )
   {
      $y[0] = $rainrate[$i];
      $x[0] = $xval[$i];
      pgpt(1,\@x,\@y,20);
   }
}

pgsci(2);                # Change colour
pgptxt(0, $top+.08 ,0,0,"Daily Rainfall (inches)");
pgsci(4);                # Change colour
pgptxt(0, $top+.04 ,0,0,"Rainfall Rate (inches per hour)");
pgsci(5);                # Change colour
pgptxt(int(($right-$left)/2), $top+.05 ,0,0, "$time $daylabel[0] $date ");

pgsci(5);                # Change colour
pgsch(1.20);           # Set character height

for( $i=$days; $i >= 0; $i-- )
{
   $offset = (($days-$i) * $twentyfourhours) + int($twentyfourhours/2);
   pgptxt($offset, $bottom-.05 ,0,0.5,$daylabel[$i]);
   pgptxt($offset, $bottom-.10 ,0,0.5,$daydate[$i]);
}
pgend;    # Close plot

###
#
# Windspeed Plot
#
$dev = "winds.gif/GIF";
pgbegin(0,$dev,1,1);  # Open plot device
pgpap(7.0,0.55);
pgscr(1,0.75,0.75,0.75);
pgscr(5,0.40,0.70,0.40);
pgscr(0,1.0,1.0,1.0);
pgscf(1);             # Set character font
pgslw(1);             # Set line width
pgsch(1.2);           # Set character height

pglabel("","",""); # Labels

$top = (int(($maxwinds)/5)+1) * 5 ;
$bottom = (int(($minwinds)/5)) * 5 ;
$left = 0;
$right = ($basetime[0] + $twentyfourhours) - $basetime[$days];
pgswin($left,$right,$bottom,$top);
pgbox( 'ABCGI',$twentyfourhours,0,'ABCGINMVS',5,0);

pgsci(0);                # Change colour
pgdraw($xval[0],$winds[0]);

pgsci(2);                # Change colour
for( $i=0; $i < $j; $i++)
{
   pgdraw($xval[$i],$winds[$i]);
}

pgsci(2);                # Change colour
pgptxt(0, $top+1 ,0,0,"Windspeed (MPH)");
pgsci(5);                # Change colour
pgptxt(int(($right-$left)/2), $top+1 ,0,0, "$time $daylabel[0] $date ");

pgsci(5);                # Change colour
pgsch(1.20);           # Set character height

for( $i=$days; $i >= 0; $i-- )
{
   $offset = (($days-$i) * $twentyfourhours) + int($twentyfourhours/2);
   pgptxt($offset, $bottom-.70 ,0,0.5,$daylabel[$i]);
   pgptxt($offset, $bottom-1.4 ,0,0.5,$daydate[$i]);
}
pgend;    # Close plot

exit;

# Subroutines to do things like calculate dewpoint
#  and corrected atmospheric pressure.
sub cbp
{
   local($in) = @_;
   return(sprintf("%2.2f", $in + $CBP_CORRECTION));
}

sub ctof
{
   local ($C) = @_;
   $F = (9/5 * $C) + 32;
   return $F;
}

sub ftoc
{
   local ($F) = @_;
   $C = 5/9 * ($F - 32);
   return $C;
}

sub dewpoint
{
   local ($F, $H) = @_;

   $rh = $H/100; # RH. Fraction, [0..1] not %.
   $t = ftoc($F); #Celsius Temperature

   $es = 6.112 * exp(17.67 * $t / ($t + 243.5));
   $e = $rh * $es;
   $loge = log($e/6.112);
   $dp = 243.5 * $loge/(17.67 - $loge);
        return ctof($dp);
}

I also wrote a script, plotdays.pl (Listing 3), that plots some of the interesting statistics for the last few days.

Programming for Reliable Periodic FTP Upload

Finally, I have a master program called loop.pl, which I wrote to reliably connect to my ISP, set up my PPP connection, transfer e-mail, set the system time and upload the weather data.

First, we have to look at how to automate an FTP connection. FTP is usually used for interactive network file transfer. To use regular FTP, you need to have an account on the remote machine. In this case, I use my ISP shell account with Front Range Internet (frii.net). A user can set up an automated FTP session by using a file called .netrc in their $HOME directory. I added to my system a user called “weather” dedicated to owning the weather data files and scripts. In the /home/weather/.netrc file, I have the following lines:

machine ftp.frii.net
login:
password:
macdef init
cd public_html
put /home/weather/WWW/wscurrent.html wscurrent.html
put /home/weather/WWW/wsplot.html wsplot.html
put /home/weather/WWW/temp.gif temp.gif
put /home/weather/WWW/pressure.gif pressure.gif
put /home/weather/WWW/raind.gif raind.gif
put /home/weather/WWW/winds.gif winds.gif
quit

The statements in this file define and execute a macro called init. All I have to do is start FTP and this script attempts to run the macro, uploading my HTML and GIF files to the appropriate place on my ISP account.

Listing 4. Upload Script

#!/bin/sh
#
# doup -- DO UPload

# This is just to tell me that things are working right.
echo ####
id
echo ####

# More reassurance
echo uploading latest wx
date

# Go to where the HTML and GIF files are to live
#   on the _local_ machine
cd $HOME/WWW

# Run ws.pl and plotdays.pl
$HOME/bin/ws.pl > $HOME/WWW/wscurrent.html
$HOME/bin/plotdays.pl 6

# Do the actual ftp;
#  rely on $HOME/.netrc to do the real work
ftp ftp.frii.net

date
echo latest wx updated

Both Listing 1 and Listing 3 are called from a short script called doup, Listing 4, which also does the actual FTP call.

Listing 5. loop.pl

#!/usr/bin/perl
#
# loop.pl
#
use POSIX "sys_wait_h";   # Need this for waitpid with WNOHANG

#
# Loop forever
#
while (1)
{
   # Fix the screen (my console sometimes gets wacked out if
   #   the connection doesn't go through quite right)
   # This just prints a console reset
   timed_system("/usr/local/bin/fix",5);

   # Delete default route
   timed_system("route del default",5);

   # Start up the PPP connection
   while  ( &dip() != 0 )
   {
      print "dip failed, try again in 60 seconds\n";
      print "dip -k\n";
      timed_system("dip -k",5);
      sleep 60;
   }
   print "sleep 5\n";
   sleep 5;
   timed_system("/bin/date",5);

   # Ping doesn't always return an error when
   #  the connection fails.  (I should probably
   #  write  a Perl script to do this.)
   if ( `ping -c2 ftp.frii.net` )
   {
      #
      # Fork off a child process to run the ftp upload script
      #
      if (!defined($doup_pid = fork()))
      {
         print "fork error\n";
      }
      elsif ($doup_pid)
      {
         #
         # This is the parent process.
         # Do our other chores while ftp upload is  running.
         #
         print "parent: child pid=$doup_pid\n";

         # Set the clock  (you should probably pick some other
         #    time servers if you are going to use my script!)
         timed_system("ntpdate black-ice.cc.vt.edu clock-2.cs.cmu.edu ntp-0.cso.uiuc.edu",15);

         # Fetch my e-mail
         #   getmail is a script that runs popclient
         timed_system("/root/getmail",30);
         print "sleep 5\n";
         sleep 5;

         # Send any waiting outgoing mail
         timed_system("/usr/sbin/runq -d1",30);

         # Sleep awhile to let the ftp upload finish
         # Catch it if it is done.
         print "sleep 30\n";
         sleep 30;
         waitpid($doup_pid,WNOHANG);

         # If upload is not finished
         # (the - checks all processes in the process group).
         if( kill(0, -$doup_pid) )
         {
            print " doup still running: sleep 60\n";
            sleep 60;

            # Time's up.  Kill the upload and
            #   all processes in it's process group.
            if( kill(0, -$doup_pid) )
            {
               print "doup hung ($doup_pid)-kill it\n";
               kill(9, -$doup_pid);
               sleep 3;
            }
            waitpid($doup_pid,WNOHANG);
         }
      }
      else  # Child
      {
         # This is the child process.
         # Give ourself a new process group.
         # Start the ftp upload script.

         print "child running doup\n";
         setpgrp($$);
         exec("/bin/su -c /home/weather/bin/doup weather");
         print "*********** Should never get here!";
         exit 0;
      }
   }
   #
   # Shutdown the PPP connection
   #
   timed_system("/usr/sbin/dip -k",5);
   timed_system("/bin/date",5);

   # Sleep for a while, about right for
   # calling every 15 minutes or so.
   sleep 840;
}

#
# Subroutine to try each of my two dialout scripts if necessary
#
sub dip
{
   local($ret) = 0;
   print ("Trying to dial out\n");
   $ret = timed_system("/usr/sbin/dip -v /root/frii.dip",90);
   print ("dip return value $ret\n");
   if( $ret != 0 )
   {
      timed_system("/usr/sbin/dip -k",10);
      print ("sleep 10 before retry\n");
      sleep 10;
      $ret = timed_system("/usr/sbin/dip -v /root/frii.dip2",90);
      print ("dip2 return value $ret\n");
   }
   return $ret;
}

#
# timed_system subroutine.
# Fork and exec a child process to run another command.
# Time ourself, and kill the child if it goes overtime.
#
sub timed_system
{
        local($command,$time) = @_;
        local($ret) = 9999;

   if (!defined($child_pid = fork()))
   {
      print("Fork Error\n");
   }
   elsif ( $child_pid )
   {
      # This is the parent.  Wait and kill if necessary.
      # (Eval/alarm structure taken from perlipc man page.)
      eval
      {
         local $SIG{ALRM} = sub { die "timeout!" };
         alarm $time;
         waitpid($child_pid,0);
         $ret = ($? >> 8);
         alarm 0;
      };
      if ($@ and $@ =~ /timeout/)
      {
         # Kill the child and all in it's process group.
         print "TIMEOUT! [$command]  $child_pid\n";
         kill(9,-$child_pid);
         sleep 2;
         waitpid($child_pid,WNOHANG);
         $ret = 999;
      }
      print "End *** $time [$command] $$ <$ret>\n";
   }
   else
   {
      # This is the child.  Give myself a new process group,
      # then run the command.
      print "Start *** $time [$command] $$\n";
      setpgrp($$);
      exec($command);
      print "*********** Should never get here!";
      exit 0;
   }

        return $ret;
}

Last but not least, loop.pl (Listing 5), is executed continuously by the root user. The only tricky part of loop.pl is the necessity to recover if a command gets stuck due to a loss of connection with the ISP. I wrote a replacement for the regular Perl “system” function which allows me to specify a timeout for each command. If that command exceeds the time alloted, it and all of its children are killed. Loop.pl also has other jobs to do, like downloading and uploading my e-mail and setting the system time using ntpdate. While those jobs are happening, I have a child process simultaneously running the doup script. If the ancillary jobs finish first, they give doup a little more time to complete. With a good connection, there is no problem getting everything done in less than one minute.

I have two different dial-up scripts, because there are two possible telephone numbers for the computer to try when dialing my ISP. If the primary number fails, it tries the second number. If both fail, it sleeps for 60 seconds and tries again. The goal of the sleep statements in loop.pl is to have the connection occur approximately every 15 minutes. Otherwise, the comments in the code pretty much cover all of the details.

Unresolved Issues

One of my main concerns is lightning. Fortunately, our site is in a wooded area that is somewhat protected from direct lightning strikes. Still, I would feel even better if the remote weather sensors were electrically isolated from the display unit and my Linux machine. Some sort of optical isolation would probably work.

Also, I may have to develop an automated way to set the clock on the weather display unit.

Otherwise, the system seems to be working pretty well. With weatherd.pl running from the inittab, and with loop.pl in my startup rc files, the weather station monitoring continues after a reboot without manual intervention. Power loss or server failure will interrupt the service, but neither of those are common occurrences.

Figure 3. Weather Web Page

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

My weather station web page (see Figure 3) can be found at http://www.frii.net/~daylight/wscurrent.html. Please stop by and check it out. If you have the opportunity, come to Estes Park and see what a beautiful place it is.

All listings referred to in this article are available by anonymous download in the file ftp.linuxjournal.com/pub/lj/listings/issue56/2538.tgz.

Chris Howard (daylight@frii.net) noodles around with Linux stuff in Estes Park, Colorado, where the winters aren’t too cold and the summers aren’t too hot, and the view out the window is always beautiful