This week I've been doing lab testing of the wifi link between our base-station:

and our buoy:

because we froze in the cold trying to figure out why we couldn’t get a link a few weeks ago.
There is a nice link budget calculator here.
Here are our specs:
Base Station
Buoy
There are several values I'm not sure about. For example, on the buoy the device is a BnB Electronics 4-port serial server which has an Atheros mini-pci 802.11b card. The specs on the web page say Tx power of 16 dBm, but the configuration page allows one to select 100mW (20 dBm). Also receiver sensitivity for the card appears to be -95 dBm (at least that’s what the driver says if you, uh, hack the server and look. ahem.) But the web page says -82 to -92, and another page with a similar card gives -95 dB for 1 Mbps and -90 dB for 11 Mbps.
Our base stations, which is a Metrix Mark II kit, doesn’t support wireless statistics collection (/proc/net/wireless is all 0’s for link levels). So to monitor the link I'm using the statistics from the one side only – the buoy.
On the buoy, I've added a web page cgi script to capture the wifi statistics. I'd reproduce it here, but embedding html in html is tricky. Here’s the beginning and the rest is all printing html.:
#!/usr/bin/perl
# Wireless monitor script
# Val Schmidt
# CCOM/JHC
# JAN 2009
$stats = `/bin/cat /proc/net/wireless`;
if ($stats){
($line1, $line2,$line3) = split('\n',$stats);
($interface,$status,$snr,$level,$noise,$nwid,$crypt,$frag,$retry,$misc,$beaconmissed) = split(/\s+/,$line3);
}
There is a trick to getting this to work that’s worth noting. In perl, when printing the html, you should use this method:
print <<HTMLEND;
htmlstuff
HTMLEND
I can’t explain why, but if I print the html stuff in separate print statements, apache refuses to serve it up, giving a mal-formed header error on the first line. To my eye, the result is exactly the same.
To long the wifi link levels as seen by the buoy, I'm querying the buoy cgi script above, parsing and plotting the results. All this is in python, of course using matplotlib (my not quite satisfactory attempt in an earlier post). Here’s the code:
#!/usr/bin/python
#
# snrplotter.py
#
# Val Schmidt
# CCOM/JHC
# 2009
#
# A program to capture and plot snr levels from our slightly hacked
# serial wifi server. One can login to the serial-to-wifi server (BnB
# electronics) via telnet (login:root, password:not required> ). I then
# installed a new cgi script in the web pages which captures the wifi
# stats from /proc/net/wireless. Without this in place, none of this is
# possible.
#
# This script will poll the web page at 1-second intervals and display
# the results.
import sys
import os
import matplotlib.pyplot as plt
import numpy as np
import datetime
import time
import urllib2
import re
# This doesn't work yet. Must be 60.
history = 60
sec = range(history)
snr = np.zeros((1,history))
filename = datetime.datetime.now().isoformat().replace(':','-') + '_snr.log'
F = file(filename,'w',0)
#cnt = datetime.datetime.now().second
#secondstodisplay = history/60.0
plt.draw()
plt.show()
time.sleep(1)
while 1:
data = urllib2.urlopen('http://10.10.0.2/cgi-bin/stats.cgi')
for line in data:
if line.find('ath0') != -1:
fields = line.split(',')
dts = datetime.datetime.now()
nowsec = dts.second
value = int(fields[2].replace('.',''))
snr.put(nowsec,value)
print dts.isoformat() + ' '+ str(snr.item(nowsec))
plt.cla()
plt.plot(sec,snr.tolist()[0],'ro')
plt.plot([nowsec],[value],'bo')
plt.xlabel('CURRENT: ' + str(value),fontsize='large')
plt.axis([0, history, 0, 100])
plt.show()
plt.draw()
F.write(dts.strftime('%Y\t%M\t%d\t%H\t%M\t%S').expandtabs() +'\t'+str(snr.tolist()[0][nowsec]))
time.sleep(1)
This produces a plot that looks something like this:

For this data collection the buoy antenna was removed (bar coax) and the base station was pole mounted abut 6 feet higher than the buoy. Hence the relatively low snr levels.
Now that I had this set up I wanted to compare my own results with the theory and see if we'll be able to get the 2-3 km range we desire. The link budget calculation looks something like this:
Fade Margin = TxPwr - CableLoss + Tx Antenna Gain - FreeSpaceLoss
+ Rx Antenna Gain - CableLoss - Receive Sensitivity
The rule of thumb is, all else being equal, a Fade Margin of 10dB minimum is required for a link. Free Space Loss we haven’t talked about yet. Free Space Loss are the losses due to absorption spreading of the signal. Wikipedia has a nice entry for Free Space Loss calculations. In a nutshell:
FSL = (4 pi d / lambda)^2
where d is the distance from the transmitter to receiver and lambda is the wavelength. It is more commonly written in dB like this:
FSL = 20*log10(d) + 20*log10(f) + 32.44
where f is the frequency in MHz and 32.44 combines the constants and unit conversions. Here is a plot of Free Space Loss for 2.4 GHz wifi:

So next turned to the link budget equation and calculated the Fade Margin (here called Link Excess) vs Range from the theoretical specs above.:

Of course, since I was monitoring link level I thought to check to see that the theory works. In my setup the antennas were 3.8 m apart in the same vertical plane. I took the remaining values from the specs above. I found that my received signal level was about 15 dB below what theory would predict. There are lots of reasons this might occur, but for now, I plotted a second curve of Fade Margin vs Range with this 15dB reduction. It shows that for a 10 dB Fade Margin our max range would be about 1600 m. I think a 15-20 dB Fade Margin might be required however reducing our range to about 1000m.
All of this assumes a clear line of sight however. And it’s not just the line of sight but the entire fresnel zone that must be relatively clear. Most documentation I've seen recommends a minimum 20% obstruction of the fresnel zone for reasonable link. The higher the power, the more the fresnel zone can be obstructed (to a point) without loss of signal. I made a plot using the equations (from the link above) for Fresnel Zone Radius:
Fn = sqrt( (n lambda d1 d2) / (d1 + d2) )
where Fn is the fresnel zone radius of the n'th zone, lambda is the wavelength, d1 is the distance from one antenna to a point between them and d2 is the distance from the second antenna to the point between them.

Here the antennas are separated by 1 km. The antenna on the right is the buoy, 1m above the water, and the antenna on the left is the base station, 5.6 m, above the water. The blue line is the line-of-sight between them. The green curves denote the 1st fresnel zone. Disregard the red curve – it doesn’t do what I thought.
In this plot at 300 meters the fresnel zone is approximately 10 m in diameter and about 3 m or 30% is occluded by the water. This brings up (yet) another question for which I don’t know the answer. Over seawater, do RF signals bounce at low grazing angles, as one might expect for other waves, or is salt water sufficiently absorptive that these signals are truly occluded? My guess, I suppose, is that they are scattered by the water’s surface, and absorbed when the wave shape is such that the angle of incidence is not too shallow. Although some signal might be received by such a scattering surface, probably not much and might not be usable by the receiving system.
So what’s the upshot? There’s some 15dB of signal we're loosing that we might be able to restore with careful checking of connectors and antennas. And with a 1 m high antenna on the buoy we're going to need all the signal we can get.