Not too long ago, I was at a hardware store and I came across some lights that I wanted to play with because I had a feeling they could be fun for Halloween and make for a decent blog post. Before I purchased the lights, I looked at their online manual and checked to see if they were compliant with Part 15 of the Federal Communications Commission (FCC) rules. This notice is an indication that the lights use radio frequencies to communicate rather than infrared signals. Since these lights use a simple remote and not a mobile application, it was a relatively safe bet that the signal would not use a complicated modulation.
Since the device is FCC-compliant, I popped open the package and looked at the back of the remote for its FCC ID.
In the United States, whenever a device transmits radio frequencies, it has to undergo a series of tests before it can be sold, and the results of the tests can be accessed on the FCC’s website. However, trying to find the information within the FCC’s site can be a bit confusing. Luckily, someone created a site, fcc.io, that performs the search for you. So, we can enter our FCC ID from the transmitting device like in the picture below:
Normally, after you perform this web request, you will be presented with display exhibits that are usually PDFs and images of the tests performed, details of the tests, the frequency range the device occupies, and sometimes internal images of the device. However, this device was originally under a different FCC ID and that ID was found in the Power of Attorney (POA) file.
Another request to fcc.io was made using the newly discovered FCC ID and I had my first piece of valuable information: the target frequency of the device.
The RF Test Report typically documents the type of modulation the transmitting device utilized, the amount of bandwidth the signal utilized, and other useful information. I am not sure what the justification is for claiming this device uses frequency-shift keying (FSK) modulation, since the transmitted signal does not occupy a range of frequencies. Also, the transmitted signal appears to be present and then absent rapidly, which is indicative of amplitude-shift keying (ASK).
Now that we have a lot of basic information about the signal, it is time to capture a transmission and try to replay it. It is generally advised to have your target signal be slightly offset to avoid the DC offset that is present in software-defined radios. In the image below, we can see a large frequency spike about one (1) megahertz above the DC offset, which is at zero (0) hertz.
Now that we have a raw signal saved to a file and we verified that we have a valid capture by replaying the signal, we can start to dissect it. The way I do this is by utilizing Inspectrum to get a clearer picture of the transmitted signal and, more importantly, find the symbol duration or symbol period. If you installed GNURadioCompanion with PyBOMBS, you could do ‘pybombs install inspectrum’ from within your PyBOMBS prefix. In a previous post where I covered getting the LimeSDR Mini setup, I made my prefix the ‘sdr’ directory in my home folder. After the captured signal is loaded in Inspectrum, make sure the sample rate is the same as what was captured and locate the shortest transmission or the smallest amount of time between transmissions. Once this is located, enable cursors and try to align the cursor with the signal.
After you align the cursors, increase the ‘Symbols’ to cover a larger portion of the whole transmission and make sure your cursors are as close to accurate as possible. With simple devices like these lights, they are built to tolerate some inaccuracies, so your cursors do not have to be 100% accurate. One (1) thing you may have noticed is the signal appears to repeat itself a few times. In the case of this device, there are three (3) identical transmissions. It is my belief that this behavior is to help compensate for errors in transmission.
Now that we know the symbol period, we will build a flowgraph to help turn our captured signal into 1s and 0s. Some things to note about this flowgraph are the ‘Add Const’ and the ‘Keep 1 in N’ blocks. The ‘Add Const’ block lowers the signal so the binary slicer is not capturing the noise that was present and the ‘Keep 1 in N’ block multiplies the sample rate and our symbol period (I call it symbolDuration in my flowgraph), which we found with Inspectrum.
With the help of a simple Python script (at the bottom of this article), we can turn the 00s and 01s to 1s and 0s and join them using commas. This output will be used in our transmission flowgraph. Since our signal repeats itself, we only need to copy one (1) instance.
Now that we have our string of 1s and 0s that represents our signal, we can attempt to recreate it. First, we add a ‘Vector Source’ block that contains the binary representation of our signal. The ‘Vector Source’ block has a limitation of 255 symbols. Because of this, it may not be possible to use all of the output from the Python script.
Connected to the ‘Vector Source’ block is a ‘Repeat’ block with an ‘Interpolation’ value that is the product of the sample rate and symbol period. Then we multiply the output of those blocks by a signal source to generate our series of transmission pulses. As far as I know, the waveform does not matter as long as it is not a square wave. I am also unsure what the minimum frequency of the signal source should be, but I have not had an issue using the default frequency of 1k.
Typically, before transmitting a generated signal, I will connect the output to different sinks to view the signal at a glance to make sure it looks close to what the original signal was. If my transmission is still failing, it can be helpful to save the generated signal to a file and then compare the original in Inspectrum.
Hopefully, this post helped to shine some light on a process that, at least when I first was trying to learn, was not covered very well, with information was scattered across multiple incomplete blog posts.
import io import sys from struct import * waveformFileName = "/path/to/your/binOUt.bin" waveformFile = io.open(waveformFileName, 'rb') waveformData = list(waveformFile.read()) waveformFile.close() segmentList =  tempSegment =  for symbolByte in waveformData: if symbolByte == b'\x00': tempSegment.append("0") else: tempSegment.append("1") print ",".join(tempSegment)