Radio propagation is a complex process in the real world, elaborate models are often used to predict expected propagation effects over known terrain, but in many cases there is no substitute for measuring ground truth. This can now be accomplished quite easily, quickly, and cheaply using GNU Radio for a wide range of protocols. Using gr-uhdgps (https://github.com/osh/gr-uhdgps), gr-eventstream (https://github.com/osh/gr-eventstream) and Google Earth, we demonstrate a tool to measure broadcast VHF FM Radio received signal strength over a metropolitan area. In this case we use WAMU 88.5 MHz, the local NPR radio station in Washington D.C., to demonstrate the tool and plot the results in Google Earth.
Building the GNU Radio Flowgraph
Looking at the WAMU FM Broadcast channel in a QtGui frequency and time sink plot is a good first sanity check. Since FM broadcast is typically allocated a 200khz channel, we set up a USRP B210 to tune to 88.5 MHz with a complex sample rate of 200 ksamp/s shown below, the expected time and frequency spectrum behavior of the FM audio signal can be seen below.
In this flow graph we extract and event of 512 samples exactly every 1 second’s worth of samples from the incoming complex sample stream off the UHD source block. Since we are throwing away the rest of the samples not extracted, this should be a low duty cycle, low CPU load process to run for the most part. The trigger sample timer event allows us to trigger events at this sample-periodicity of a fixed length and pass them downstream to the “Handler PDU” block, this block simply converts the eventstream events into normal complex float PDUs which can be handled by any message passing block.
At this point we introduce a simple python message block called “CPDU Average Power” which computes the average sample power within the event’s samples, tags a “power” key and value to the PDU’s metadata structue and passes it downstream. The functionality of this python block is contained within this simple python/numpy handler function which computes the power, adds the tag, and sends the output message — for now we are throwing away the PDU data field and sending PMT_NIL in its place since we don’t do anything with it downstream.
def handler(self, pdu):
data = pmt.to_python(pmt.cdr(pdu))
meta = pmt.car(pdu)
data = data - numpy.mean(data) # remove DC
mag_sq = numpy.mean(numpy.real(data*numpy.conj(data))) #compute average Mag Sq
p = self.k + 10*numpy.log10(mag_sq)
meta = pmt.dict_add(meta, pmt.intern("power"), pmt.from_float( p ) )
self.message_port_pub( pmt.intern("cpdus"), pmt.cons( meta, pmt.PMT_NIL ) );
At this point we remove the “event_buffer” item from each PDU’s metadata structure using the “PDU Remove” block since it is also large and we don’t really want to keep it around downstream for any reason — trying to serialize this into our JSON storage would waste a lot of space and makes the message debug print outs massive.
Since the B210 is set up with an internal GPSDO board, we have both a GPS disciplined clock and oscillator as well as GPS NMEA fix data available from the UHD host driver. By using the uhdgps gps probe block, we can poll the USRP source block for the NMEA fix data every time we need to store an updated signal strength record as shown in the flow graph above. Passing our CPDUS through this GPS Probe block appends the NMEA data and a bunch of current USRP state information from the device which we can look at later.
Finally, we store a sequence of the JSON’ized PDUs to two files, one which has been filtered for records where “gps_locked” is “true” and one which simply stores all records regardless of GPS lock state.
Each JSON’ized record corresponding to a single PDU from the flowgraph and a single eventstream extracted sample event, now looks like this in our output files.
We see that this structure now contains gps state information pulled from the GPS probe block, the stream tags which were added to the event/PDU by eventstream, and our power value computed by the python CPDU power block.
"gps_servo": "15-03-29 307 47943 -469.10 -3.21E-09 15 13 2 0x20",
To test this graph I securely fasten a macbook into the passenger seat, wedge a B210 between the center console and passenger seat of the Subaru test vehicle, and plunk a GPS antenna and a poorly band matched magnetic duck antenna onto the trunk of the car and go for a drive. After launching the application, it takes a while for the GPSDO to obtain GPS lock since it is not network assisted like many devices these days. After a few minutes of waiting and impatiently moving the GPS antenna to the roof of the car, I obtained lock and pretty much kept it for the entirety of my drive.
After driving a thorough route around town and past the broadcast tower, I dump JSON file through uhggps’s json_to_kml.py tool to convert the rss measurements into a Google Earth friendly KML file with vertical lines of height proportional to the RSS at each event location. This provides a nice intuitive visual way to look at and understand received signal strength effects over various terrain changes, obstacles, urban canyons and other such phenomena. An overview of the route can be seen here.
A few notable spots around the route are shown below.
Driving through Rosslyn, VA we can see massive variation in received signal strength at each sample point likely due to massive amounts of urban multi-path fading resulting in large amounts of constructive and destructive interference.
Driving from the George Washington Parkway North onto the US 1 memorial bridge on the far side of the river, we can see that going from a lower elevation shadowed by a small hill and a handful of trees up onto the elevated and unobstructed bridge resulted in a significant sustained climb in signal strength level.
Driving north on Connecticut Avenue, taking the tunnel underneath Dupont Circle, we briefly lose GPS lock, but upon reacquiring show a very low received signal strength down in the lowered, partially covered, and heavily shadowed roadway coming out of the tunnel. To deal with this kind of GPS loss of lock we may need to implement some kind of host based interpolation of fixes occurring on either side of the positioning outage, but for now the samples are discarded.
There are numerous other effects that can be observed in the KML plot from the drive which is linked below.
One issue that I’m still considering is how exactly to map RSSI to amplitude, there are two modes which KML supports and each has their own issue. These plots are using “aboveGroundLevel” which maps “0” elevation to the ground elevation at a specific LAT/LON, this is good because we can just use 0 for our base height and not worry about putting a line underground somewhere, however its annoying in that the line elevation tracks with the ground contours distorting your perception of RSSI a bit. Absolute doesnt have this problem, since 0 is (I think) locked at sea level, however in places farther from the coast this is annoying because low elevations wind up underground. Ultimately it might be neccisary to do some kind of aboveAverageGroundLevel for a local area, but this is as far as I know not supported in Google Earth / KML.
The resulting KML file can be opened in Google Earth from https://github.com/osh/kml/blob/master/WAMU_RSS.locked_1427589112.17.json.kml. Countless hours of fun can be had zooming around inspecting fades that occur passing various obstacles along the way and signal strength variation going up and down hills in various areas.
This flow graph is available at https://github.com/osh/gr-uhdgps/blob/master/examples/uhdgps_rssi_log.grc To use it make sure you have gr-eventstream, gr-uhdgps, and relevent python modules installed.