PHP: Ultimeter 800 Weather Station (CWOP)


Citizen Weather Observer Program (CWOP)This article discusses how to build a weather station that uses a Peet Brothers’ Ultimeter 800 Weather Station and enable it to send to the Citizen Weather Observation Programme (CWOP).

Why Do This?

The reasons for doing this are as follows:

  1. The unit only has a serial output and is not “Internet” ready
  2. Most systems rely on using a PC and this is not very ecological in terms of power usage
  3. There are almost no working scripts published on the Internet for Ultimeter units

With this in mind, it is hoped this article will give you a script you can use to allow your Ultimeter unit to send data to CWOP.

Physical Architecture

The physical architecture of how this works is shown below.

Ultimeter 800 and NSLU2 Unit

Ultimeter 800 and NSLU2 Unit

To keep this artical shorter, the subject of linux-enabling the NSLU2 unit won’t be discussed (this unit runs SLUGOS and all the instructions for doing this are covered elsewhere).

The following list the items shown:

  • NSLU2 Unit (Lower Left)
  • USB hub adaptor (Upper Left)
  • Serial to USB adaptor (Mid centre)
  • Temperature sensor (coiled up Lower Centre)
  • Ultimeter “hub” (Right of tempertature sensor)
  • Ultimeter 800 unit (Lower Right)
  • CAT5 cable (Yellow coiled cable)

System Pre-requisities


You will need to get some things in place fisrt before you can connect your weather station to the NSLU2.

  • USB to Serial Adaptor
  • USB stick for extra memory (see the NSLU2 build, but put simply there is not enough core memory to run the linux build on the unit)
  • A fully working NSLU2 with SlugOs and using the external USB flash/pen drive
  • A working Peet Unit (i.e. it can be powered up and see its peripherals)

Notes To The Above

While there is a USB hub shown, it is not mandatory as there are two USB ports on the NSLU2. One will connect to the USB to serial adaptor, the other to the extra file system space that linux build requires. It was found that if there was a USB hub, other units could be connected at the same time (e.g. a WS-2350 weather station).

Before adding any scripts, make sure you have a working NSLU2 with linux, with PHP and correctly working cron daemon. How to do this is all covered on the SlugOS web site.

With a working NSLU2 and linux, make sure you can see /dev/ttyS0 (AKA com1 on a PC). Use miniterm from the command line to determine that the Ultimeter unit is sending data regularly in “Complete Record Mode” (every few seconds). Once you can see it in “Complete Record Mode“, reset it back to “Modem Mode” (data on demand). How to do this is described in the handbook for the unit or on the link at the bottom of this article.

Assuming the pre-requisites are in place, we can look at scripts and software.

Software Architecture

The software/script elements are based on the use of PHP script, a shell script and a cron job. The cron job runs the shell script every ten minutes which in turn runs the PHP script.

Cron Job

The cron job to run the shell script is very simple and is as follows:

* 0 * * *         /usr/bin/ntpdate -s -u
0,10,20,30,40,50 * * * * /opt/bin/ cwop

We need to ensure that the NSLU2 unit is time synchronised, hence the use of ntpdate once per day (midnight). We also send the data to the Ultimeter unit (because it seems to be fairly inaccurate), so again to keep things right, the unit must have the right time.

Shell Script

The shell script is used for a few reasons, but importantly to not litter the crontab table. The other reasons for using a shell script are:

  • To allow some pre- and post-amble processing (which we need to do)
  • To apply and system-type checks (e.g. permissions and error checking)

Main script


# set the serial ports to the correct rate (just in case)

# Carry out specific functions when asked to by the system
case "$1" in
 echo "Running ultimeter 800 to cwop"
 /opt/bin/php /opt/bin/wx-silent.php cwop >> /opt/share/www/wx.log
 echo "Running ultimeter 800 to 10 minute local file"
 #Beep so we know it is now running
 beep -f 1300 -l 30
 /opt/bin/php /opt/bin/wx-silent.php >> /opt/share/www/wx-10.log
 beep -f 1300 -l 90 -d 30
 beep -f 1300 -l 30 -d 30
 beep -f 1300 -l 90 -d 30
 echo "Running ultimeter 800 locally with APRS data"
 /opt/bin/php /opt/bin/wx-silent.php >> /opt/share/www/wx.log

The above script initially calls a second script to set the  serial port before anything else, which is discussed below. The above script then checks whether an extra parameter “cwop” or “local” is passed. The reason for this check is that it allows testing without send live data to the Internet.

If the script does have “cwop” as a parameter, it then firstly outputs to the standard output, then calls the php script /opt/bin/wx-silent.php before sending the script output to /opt/share/www/wx.log

If the script does have “local” as a parameter, it firstly sends a message to the standard output. Then it sounds a beep at 1.3kHz for 30 msec. It then calls the php script, outputs to the standard output, then calls the php script /opt/bin/wx-silent.php before sending the script output to /opt/share/www/wx-10.log. After that it calls three beeps in the sound of a morse code “K”.

If no parameter is passed then same script is called a bit like before to a local file only – you get the idea. Make sure you set the UNIX file permissions to 755 (and for the serial script below) or cron will have trouble.

The above script also calls, which is as follows:

stty -F /dev/ttyUSB0 ispeed 2400
stty -F /dev/ttyUSB0 ospeed 2400
stty -F /dev/ttyUSB0

The sets the serial device to 2400 baud. The reason for this is to make sure that each time the weather station is communicated to, it is not at any other serial baud rate.

Ok, lets take a look at the PHP script that the shell script calls.

PHP Script

The entire PHP script is shown below (we’ll decompose it at the bottom). But if you just cut and paste it in, make sure you:

  1. Put the CWOP ID in or your callsign if you are a radio ham callsign in the “FormatAPRSData ()” function
  2. Put a correct latitude and longitude in the “FormatAPRSData ()” function

Or the data sent to  CWOP will be thrown away.

 "" );

// Don't forget to make sure your Unix serial line
//  has been set using "stty" to the right speed and so on

$handle = fopen("/dev/ttyUSB0", "r+");

if ($handle) {
 // The U800 used for development had a very poor clock ... losing
 // 10-15 mins per day. Hence it needs setting on each read to keep in step

 // If you want to send the current time use:
 // $command = ">A" follwed by
 //    4 bytes for day of year in dec and 4 bytes time in minutes today in de
 //    e.g. March 28th at 0922z is ">A00560562"
 $tm = localtime(); // this assumes we're in GMT - amend accordingly
 $tm_h = gmdate("H");
 $tm_m = gmdate("i");
 $dt = sprintf( "%04d%04d",$tm[7], ($tm_h*60)+$tm_m ); // days since Jan 1st
and then minutes since 00:00
 $command = ">A$dt\n";

 //assumes were in ultimeter modem mode
 // send a get command to the ultimeter
 $command = ">H\n";

 //now get any results
 $data = stream_get_line ($handle, 452, "\n");

 // lets split this out ...
 SplitDataOut ($data);

 // Now as an APRS string
 $APRS = FormatAPRSData();
 echo "\n".$APRS;

 // Send APRS data to outside world
 if ($argc == 2){
 $option = $argv[1];
 if ($option == "cwop"){
 echo "\n\nSending to CWOP\n";
 SendAPRSData( $APRS );

function OutputData( )
 global $gWxData;
 echo "\n";

function SplitDataOut ($inputdata )
 global $gWxData;
 // lets split this out ... Note we ignore the &CR&t the start
 $iPtr = 4;
 for ($i = 1; $i <= 110; $i++) {
 $gWxData[$i] = substr( $inputdata, $iPtr, 4 );
 $iPtr = $iPtr+4;
 for ($i = 111; $i <= 114; $i++) {
 $gWxData[$i] = substr( $inputdata, $iPtr, 2 );
 $iPtr = $iPtr+2;
 $gWxData[115] = substr( $inputdata, $iPtr, 40 );


// This function will telenet to APRS-land and send a report.

/* The following applies:

Once you have formed this record, open a connection to port 14580
on '' and then send the following:

user CW0003 pass -1 vers linux-1wire 1.00

(substitute your CW number), followed by CR,LF (i.e. two characters),
followed by your data record. Then disconnect. Also, it works best
with a three second delay between the USER command and the data packet.
Also wait three seconds after sending the data to close the connection.

function SendAPRSData( $data )
    global $gWxData;

    $fp = fsockopen("", 14580, $errno, $errstr, 30);
    if (!$fp) {
       echo "$errstr ($errno)\n";
    } else {
       $out = "user CW0003 pass -1 vers linux-1wire 1.00\r\n";
       fwrite($fp, $out);
       $out = "$data\r\n`";
       // echo "\n".$data;
       fwrite($fp, $out);

/* This function will build a string up from the data collected
 Note that there are rules on the format of this here:

 An example of a good one is:


 Which is built up as follows:

Field                   Meaning
----------------------- -----------------------
CW0003                  Your CW number
>APRS,TCPXX*:           Boilerplate
/241505z                The ddhhmm in UTC of the time that you generate the report. However, the timestamp is pretty much ignored by everybody as it is assumed that your clock is not set correctly! If you want to omit this field, then just send an exclamation mark '!' instead.
4220.45N/07128.59W      Your location. This is ddmm.hh -- i.e. degrees, minutes and hundreths of minutes. The Longitude has three digits of degrees and leading zero digits cannot be omitted.
_032                    The direction of the wind from true north (in degrees).
/005                    The average windspeed in mph
g008                    The maximum gust windspeed in mph (over the last five minutes)
t054                    The temperature in degrees Farenheit -- if not available, then use '...' Temperatures below zero are expressed as -01 to -99.
r001                    The rain in the last 1 hour (in hundreths of an inch) -- this can be omitted
p078                    Rain in the last 24 hours (in hundreths of an inch) -- this can be omitted
P044                    The rain since the local midnight (in hundreths of an inch) -- this can be omitted
h50                     The humidity in percent. '00' => 100%. -- this can be omitted.
b10245                  The barometric pressure in tenths of millbars -- this can be omitted. This is a corrected pressure and not the actual (station) pressure as measured at your weatherstation. The pressure is adjusted according to altimeter rules -- i.e. the adjustment is purely based on station elevation and does not include temperature compensation.
e1w                     The equipment you are using. This is the string that I use to identify my software. Please use something different. In particular, please include the version number of your software and the type of hardware sensors attached. For example: eMyWx123DVP -- MyWx version 1.2.3 with Davis Vantage Pro hardware. Being explicit here allows other software to automatically determine the type of station software in use. It can also be used to encourage people to upgrade to the current version! Weather Display has a table of codes that cover most weather station types and it would be helpful to use the same codes.

Note that most fields are fixed width. This constraint means that use
of C-like formatting strings such as 'h%02d' is not really appropriate
as they do not deal with out of range values. In this particular case,
it does not deal with the special case of 100% humidity either!

The letters are all case sensitive. The fields should be sent in the
order that they appear in the table above.

function FormatAPRSData ()
 global $gWxData;

 $APRSdata ="";

 $APRSdata = "NOCALL"; // CWOP ID or callsign -> change this to yours!!!!
 $APRSdata = $APRSdata . ">APRS,TCPXX*:"; // network type
 $time = DateFromWxStn();
 $APRSdata = $APRSdata . "@".$time; // time of report
 $APRSdata = $APRSdata . "0000.00N/00000.00E"; //  lat long -> change this to yours!!!!
 $APRSdata = $APRSdata . "_".Wind5MinAveDirectionFromWxStn();
 $APRSdata = $APRSdata . "/".WindCurrentSpeedFromWxStn();
 $APRSdata = $APRSdata . "g".Wind5MinPeakSpeedFromWxStn();
 $APRSdata = $APRSdata . "t".CurrentOutTempFromWxStn();
 $APRSdata = $APRSdata . "r..."; // The U800 doesn't have last rain in hour
 $APRSdata = $APRSdata . "p..."; // The U800 doesn't have last rain in 24
 $APRSdata = $APRSdata . "P..."; /// Add this if you have a rain sensor -> .RainSinceMidnightInches();
 $APRSdata = $APRSdata . "h".CurrentOutHumidityFromWxStn();
 $APRSdata = $APRSdata . "b".CurrentOutsidePressure();
 $APRSdata = $APRSdata . "ehomebrew"; 

 return $APRSdata;
function DateFromWxStn()
 // The U800 puts the date (day of year) in field 16
 //  and the time in field 17 (minute of day)
 //  And for this application we don't report the year anyway
 //  we only need the day of the month

 global $gWxData;

 $day = hexdec($gWxData[16]) + 1; // Day zero is Jan 1st!
 $time = hexdec($gWxData[17]);

 // ok this will depend on your locale as to where your day is
 $date = jdtojulian($day);
 $day_no = explode("/",$date); // split the bits out

 // now the time
 $hours = explode(".",($time/60) );   // we want the bit before the decimal
 $minute = sprintf("%02s",bcmod($time,"60"));

 $result = sprintf("%02d",$day_no[1]).gmdate('Hi')."z";
 return $result;
function WindCurrentSpeedFromWxStn()
 // The U800 puts the current wind speed in field 1

 global $gWxData;

 $wind = round(hexdec($gWxData[1])*0.1); //Note this is in steps of 0.1

 $result = sprintf("%03s", $wind );

 return $result;
function Wind5MinAveDirectionFromWxStn()
 // The U800 puts the average wind in field 4 for last 5 mins

 global $gWxData;

 $wind = hexdec($gWxData[4]); // This is 0 to 255 and we need in degrees

 $result = sprintf("%03s",round( (360/255) * $wind ));

 return $result;
function CurrentOutTempFromWxStn()
 // The U800 puts the outdoor temperature in field 6 in F x 10

 global $gWxData;

 $temp = hexdec($gWxData[6]);

 if ( $temp == 0 ) {
 } else {
 $result = sprintf("%03s", round($temp/10) );

 return $result;
function Wind5MinPeakSpeedFromWxStn()
 // The U800 puts the ave 5 min wind speed in field 3

 global $gWxData;

 $wind = round(hexdec($gWxData[3]) * 0.1); //note this is in lumps of 0.1

 $result = sprintf("%03s", $wind );

 return $result;
function RainSinceMidnightInches()
 // The U800 puts the rain today in field 7

 global $gWxData;

 $rain = hexdec($gWxData[7]);

 $result = sprintf("%03s", $rain );

 return $result;
function CurrentOutHumidityFromWxStn()
 // The U800 puts the outdoor humidity  in field 13

 global $gWxData;

 if (strcmp($gWxData[13], "----") ==0) { // then there's no data
 return "..";
 $hum = hexdec($gWxData[13]);

 if ($hum == 100) {
 $hum = 0; // this is because APRS can only do two digits
 echo "\nIs 100% - setting to zero";
 $result = sprintf("%02s", $hum );

 return $result;
function CurrentOutsidePressure()
 // The U800 puts the rain today in field 8

 global $gWxData;

 if (strcmp($gWxData[8], "----") ==0) { // then there's no data
 return ".....";
 $pressure = hexdec($gWxData[8]);

 $result = sprintf("%05s", $pressure );

 return $result;

Right lets take a look at the script.

  • The first few lines are a big comment explaining a bit about the data received from the weather unit and also reminding to set the serial port first.
  • After that we set a global called $gWxData. We use this to send the information to CWOP – more on that function later.
  • Then we open a file (handle) for the serial port in us. Select the right one you use here if it isn’t “/dev/ttyS0”. If the file open fails, we simply exit the PHP script – you may want to add something here to alert you.
  • The comments in the script tell you what’s going on, but the first job is to build the date/time based on what the NSLU2 has and then send it to the weather unit. As long as you have set the unit properly, the time should be sent to the weather unit. If you have problems here, use miniterm and try setting the unit manually from the command line.
  • Ok, having set the time, we now go into “modem mode” and then ask the unit to send us all the data back.
  • Providing all has gone well, we get all the data back with the PHP “$data = stream_get_line ($handle, 452, “\n”);”
  • That grabs 452 bytes or unless a “\n” is read in. That should be all the data we need.
  • We then close the file handle to the weather unit so we can then process it and finally send it to CWOP.
  • The first thing after getting the data from the weather station is to carve it up into useful bits. This is done in “SplitDataOut ($data);”
  • Providing we have split it up ok (and we assume it’s worked for now as there is no error checking), we build an APRS string (discussed in a bit), and then send it to the standard output.
  • We then check to see if we had a parameter passed to the PHP script. If a string was passed and was “cwop”, then we will try to send the weather data to the Internet. If not, then we’ll do nothing – obviously you can add something here if you want to send it somewhere else.
  • If we were passed “cwop”, send a message to the standard output followed by a call to the function “SendAPRSData( $APRS );” This last function we’ll also cover in a bit.
  • Now providing at all worked, by now, the weather station data should be with CWOP on the Internet.

Overview of Functions

The following describe a few of the important functions.

function SendAPRSData( $data )

This is the key PHP function that sends the formatted data to CWOP. Very simply, it opens a socket to the cwop main server, logs in (and make sure your ID is in upper case – took ages to work that one out), writes the data we’ve built up in $gWxData, and then closes the socket, after waiting for three seconds (for the transmission to settle down).

function SplitDataOut ($inputdata )

This function splits the large input string into 114 pieces into a global array which we use elsewhere.

function FormatAPRSData ()

This function builds a string that it returns to the caller based on the APRS format rules. The function gives an overview as a comment at the start so you get an idea what it should look like. Use the embedded URL if you want to know more.

function DateFromWxStn()

This function builds an APRS date based on the APRS format rules.

APRS Data Formatting Functions

The following functions build a single data item and return it. If you know a bit of C or PHP, you can work it out. These are all used in FormatAPRSData().

  • WindCurrentSpeedFromWxStn()
  • Wind5MinAveDirectionFromWxStn()
  • CurrentOutTempFromWxStn()
  • Wind5MinPeakSpeedFromWxStn()
  • RainSinceMidnightInches()
  • CurrentOutHumidityFromWxStn() – Note this does a decimal / hexadecimal conversion (using hexdec() )
  • CurrentOutsidePressure()

There is a lot of data still available from the weather station that isn’t processed. If you want all that, use some of the above functions as a guide and go from there. Note that for APRS purposes, the ones you need have already been done.

And that’s it. The script should just run as shown above, but note there isn’t a lot of error handling as the script has been found to be reliable month after month without change.

Suggestions for Generic Linux Usage

It is possible to use the above on any linux system that has PHP, a cron daemon and both a serial port to the weather station and online access to the Internet. Try the script out and see how you go (but make sure you set the CWOP ID and the latitude/longitude).

Links and Useful Pointers

The following gives locations of useful infromation, used during the development of this application.