Drawing on the spectrum


Tags: sdr radio dsp
Reading time: about 5 minutes

Software-defined radio software usually comes with a waterfall view (spectrogram) that lets the user quickly inspect the spectrum. The spectrogram plots the amplitude of frequencies over time. This means, by carefully outputting a signal consisting of multiple frequencies, we can draw shapes and pictures on the spectrogram.

A common NFM walkie-talkie is too limited to do this, but a Software Defined Radio that can transmit arbitrary I/Q samples will do the job perfectly. Fortunately I have a HackRF One at hand, so I gave this a try.

In order to transmit from the HackRF, I will be using the hackrf_transfer command. This means all I’ll need to do in my modulator is to output I/Q samples to stdout. Let’s make a quick helper method to do this.

Writing samples

Traditionally, DSP samples are kept between $-1$ and $1$, so we will be using this format internally. In order to give them to hackrf_transfer, we need to encode them as signed 8-bit integers. The format accepted by the program is alternating 8-bit signed I and Q samples.

import struct, os

dsp = os.fdopen(1, 'wb')

def write(i, q):
    i = int(i * 127)
    q = int(q * 127)
    data = struct.pack('bb', i, q)
    dsp.write(data)

Configuration

Let’s also define some constants; such as the output sample rate, the maximum frequency deviation, and how long it should take to transmit the image. The frequency deviation determines how wide our signal will be on the spectrum and the transmission time will determine the height. You should play around with these values until you can get a clear image.

RATE = 2_000_000 # 4M sample rate
TRANSMIT_TIME = 2 # 2 Seconds
FREQ_DEV = 15_000 # 15 KHz

Loading the image

With the configuration out of the way, we are now ready to produce the samples. The first thing we need to do is to read an image file. To do this, I will be using the Pillow library. Let’s get the image file path from the command line arguments, load the image and convert it to a black and white bitmap.

from PIL import Image
import sys

im = Image.open(sys.argv[1])
im.convert('1') # 1 means a 1-bit image

Outputting the image

We need to output the image bottom-to-top because the spectrogram will put the signals received earlier at the bottom, as it scrolls like a waterfall.

t = 0

for y in range(im.height)[::-1]:
    target = t + TRANSMIT_TIME / im.height

    while t < target:
        # Output line...
        pass

Every line, we pick a target time. We will be outputting samples for the current line until we reach target. Each line gets TRANSMIT TIME / IMAGE HEIGHT seconds.

First of all, let’s cache the pixels of the current line since Python is not very fast.

line = [im.getpixel((x, y)) for x in range(im.width)]

When we are outputting the line, we’ll pretend that each pixel of the image is a frequency in out output. So for an image with the width of 300 and frequency deviation of 5000 Hz; $x = 0$ is offset by 0 Hz, $x = 150$ is offset by 2500 Hz and $x = 299$ is offset by 5000 Hz.

Using the mapping we described above, let’s accumulate I and Q values for all the pixels.

i = 0
q = 0

for x, pix in enumerate(line):
    if not pix:
        continue
    offs = x / im.width
    offs *= FREQ_DEV
    i += math.cos(2 * math.pi * offs * t) * 0.01
    q += math.sin(2 * math.pi * offs * t) * 0.01

write(i, q)
t += 1.0 / RATE

We can represent a wave of a particular frequency in time using the well-known formula $2 \pi \cdot \textit{freq} \cdot \textit{time}$. Since I is the cosine of the value and Q is the sine, our final values become sin(2 * pi * f * t) and cos(2 * pi * f * t).

We don’t output anything for lines where the pixel value is 0. We multiply the signals we add to I and Q (i.e. dampen them) by 0.1 in order to prevent the signal from excessive clipping. This approach actually has some downsides, as the signal might still clip for certain images, but for a short demo where we can pick the images and change the dampening factors it won’t be a problem.

Now let’s combine the code snippets so far and try to render a signal. I recommend not transmitting this in real-time as Python is slow, and using PyPy as Python is slow.

$ pypy3 ./pic2spec.py btc.png > btc.raw
... Wait a lot
$ hackrf_transfer -f 433000000 -t btc.raw -s 4000000 -a 1

Results

Here’s a video of what our signal looks like on Gqrx.

Code

Here’s the full code, if you want to try this on your own.

#!/usr/bin/env python3
import struct
import os
from PIL import Image
import sys
import math

dsp = os.fdopen(1, "wb")


def write(i, q):
    i = int(i * 127)
    q = int(q * 127)
    data = struct.pack("bb", i, q)
    dsp.write(data)


RATE = 4_000_000  # 4M sample rate
TRANSMIT_TIME = 2  # 2 Seconds
FREQ_DEV = 15_000  # 15 KHz


im = Image.open(sys.argv[1])
im.convert("1")  # 1 means 1-bit image


t = 0

for y in range(im.height)[::-1]:
    target = t + TRANSMIT_TIME / im.height

    line = [im.getpixel((x, y)) for x in range(im.width)]
    while t < target:
        i = 0
        q = 0

        for x, pix in enumerate(line):
            if not pix:
                continue
            offs = x / im.width
            offs *= FREQ_DEV
            i += math.cos(2 * math.pi * offs * t) * 0.01
            q += math.sin(2 * math.pi * offs * t) * 0.01
        write(i, q)
        t += 1.0 / RATE

The following pages link here

Citation

If you find this work useful, please cite it as:
@article{yaltirakli,
  title   = "Drawing on the spectrum",
  author  = "Yaltirakli, Gokberk",
  journal = "gkbrk.com",
  year    = "2021",
  url     = "https://www.gkbrk.com/2021/02/spectrum-drawing/"
}
Not using BibTeX? Click here for more citation styles.
IEEE Citation
Gokberk Yaltirakli, "Drawing on the spectrum", February, 2021. [Online]. Available: https://www.gkbrk.com/2021/02/spectrum-drawing/. [Accessed Nov. 12, 2024].
APA Style
Yaltirakli, G. (2021, February 07). Drawing on the spectrum. https://www.gkbrk.com/2021/02/spectrum-drawing/
Bluebook Style
Gokberk Yaltirakli, Drawing on the spectrum, GKBRK.COM (Feb. 07, 2021), https://www.gkbrk.com/2021/02/spectrum-drawing/

Comments

Comment by app4soft
2022-12-28 at 06:04
Spam probability: 0.649%

This method used by Ukrainian radio amateur operators to jam radio channels used by Russian military and counter fight propaganda in the first months of full-scale Russia invasion of Ukraine.[0,1] [0] https://rurik.us/files/ [1] https://ur8lv.com/1621773881

Comment by Melis
2022-05-30 at 13:11
Spam probability: 0.032%

Hei, thank you for the article! Looking to make a different project for my SDR course but handy to have this just in casesies. Teşekkürler <3

Comment by admin
2022-04-24 at 18:17
Spam probability: 0.001%

@mlg Which file do you need? The input file is just a random logo I found on the internet, and the output file is generated by the code in the article. I don't think I still have the raw I/Q file I used to transmit this, but it should be pretty easy to generate it yourself.

Comment by mlg
2022-04-18 at 12:54
Spam probability: 0.482%

where the file

Comment by kanye west
2021-11-18 at 23:45
Spam probability: 0.352%

helpful for epic music makers

Comment by admin
2021-02-28 at 22:02
Spam probability: 0.013%

@Vera Thanks for providing the hardware used in this article. Playing with the HackRF has been very fun, I'll probably publish more HackRF stuff soon.

Comment by Vera
2021-02-28 at 21:49
Spam probability: 0.151%

I am the HackRF sponsor of this article.

Comment by admin
2021-02-28 at 16:22
Spam probability: 0.011%

@Chris Did you encounter an error while trying to run it? I just double-checked and the code looks complete to me. This is exact code I ran to produce the video.

Comment by Chris S
2021-02-27 at 00:49
Spam probability: 0.701%

Code looks incomplete.

Comment by Ahmet
2021-02-24 at 16:29
Spam probability: 0.836%

Very helpful

Comment by Mehtap
2021-02-24 at 12:10
Spam probability: 2.256%

Super

© 2024 Gokberk Yaltirakli