Skip to content

Commit

Permalink
zmq output
Browse files Browse the repository at this point in the history
Signed-off-by: Jeff Long <[email protected]>
  • Loading branch information
willcode committed May 8, 2023
1 parent 3f082c4 commit baf8e95
Show file tree
Hide file tree
Showing 19 changed files with 25,901 additions and 22 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ jobs:
soapysdr-module-remote \
libuhd-dev \
liborc-0.4-dev \
libhidapi-dev
libhidapi-dev \
libzmq3-dev
cd /tmp
git clone https://github.com/Nuand/bladeRF.git
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ jobs:
gr-osmosdr \
liborc-0.4-dev \
appstream \
desktop-file-utils
desktop-file-utils \
libzmq3-dev
- name: Install Qt5
if: matrix.image != 'ubuntu:22.04'
run: |
Expand Down
28 changes: 28 additions & 0 deletions scripts/monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env python

# Subscribe to all topics, print topic and metadata

import zmq


def rx_loop(socket):
try:
while True:
message = socket.recv_multipart()
if len(message) == 2:
print(f"{message[0].decode()}: {message[1].decode()}")
elif len(message) == 3:
print(f"{message[0].decode()}: {message[1].decode()} data({len(message[2])})")
except KeyboardInterrupt:
print()


context = zmq.Context()
socket: zmq.Socket = context.socket(zmq.SUB)
socket.connect('ipc:///tmp/gqrx_data')

# Subscribe to all control and data topics.
socket.setsockopt(zmq.SUBSCRIBE, b'control.')
socket.setsockopt(zmq.SUBSCRIBE, b'data.')

rx_loop(socket)
172 changes: 172 additions & 0 deletions scripts/plot_zmq_fft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/env python

# Read data file and plot.

import sys
import json
import numpy as np
from scipy import signal as sig
from scipy import ndimage as ndi
import matplotlib.pyplot as plt
import matplotlib.colors as colors

URI = 'ipc:///tmp/gqrx_data'
TOPIC = b'data.fft.linear'
META_FILE = '/tmp/fft.meta'
DATA_FILE = '/tmp/fft.data'
STD_FILE = '/tmp/fft.std'

META_FILE = '/tmp/fft.meta'
DATA_FILE = '/tmp/fft.data'

NUM_BINS = 0 # all bins
# NUM_BINS = 2000
START_BIN = 635000

LENGTH = 0 # all
START_TIME = 0

WIDTH = 0
HEIGHT = 0


# Create gqrx colormap for matplotlib, based on code in plotter.cpp
# The viridis, plasma, and turbo maps are already available in NumPy.
def gqrx_colormap():
cmap_data = []
for i in range(256):
if (i < 20):
cmap_data.append((0, 0, 0))
elif ((i >= 20) and (i < 70)):
cmap_data.append((0, 0, 140*(i-20)/50))
elif ((i >= 70) and (i < 100)):
cmap_data.append((60*(i-70)/30, 125*(i-70)/30, 115*(i-70)/30 + 140))
elif ((i >= 100) and (i < 150)):
cmap_data.append((195*(i-100)/50 + 60, 130*(i-100)/50 + 125, 255-(255*(i-100)/50)))
elif ((i >= 150) and (i < 250)):
cmap_data.append((255, 255-255*(i-150)/100, 0))
elif (i >= 250):
cmap_data.append((255, 255*(i-250)/5, 255*(i-250)/5))

cmap_data = [[c/255 for c in x] for x in cmap_data]
cmap = colors.LinearSegmentedColormap.from_list(
name='gqrx',
colors=cmap_data,
N=256,
gamma=1.0)
return cmap


cmap = gqrx_colormap()

# Read in metadata. Use first fftsize and rate found in metadata and assume it
# does not change
fftsize = None
fftdecim = None
rate = None
metas = []
with open(META_FILE, 'r') as metaf:
for line in metaf:
meta = json.loads(line)
if fftsize is None:
fftsize = meta.get('fftsize')
if fftdecim is None:
fftdecim = meta.get('fftdecim')
if rate is None:
rate = meta.get('rate')
metas.append(meta)

if fftdecim:
fftsize //= fftdecim

if len(metas) < 1:
print('No data')
sys.exit(1)

start_time = metas[0]['timestamp']
end_time = metas[-1]['timestamp']
print(f'{len(metas)} lines in {end_time - start_time:.3f} seconds')

a = np.memmap(DATA_FILE, mode='r', dtype=np.float32)
a = a.reshape((-1, fftsize))


# Use a subset of bins if requested
if NUM_BINS != 0:
print(f'Using bins {START_BIN} to {START_BIN+NUM_BINS-1}')
a = a[:, START_BIN:START_BIN+NUM_BINS]
if LENGTH != 0:
print(f'Using interval {START_TIME} to {START_TIME+LENGTH-1}')
a = a[START_TIME:LENGTH, :]

# Rescale bins if reqested
if WIDTH:
print(f'Resampling to width {WIDTH}')
a = sig.resample(a, WIDTH, axis=0)
if HEIGHT:
print(f'Resampling to height {HEIGHT}')
a = sig.resample(a, HEIGHT, axis=1)

# Apply power scale for units and effects of decimation and take log10.
#
# Uncomment for dBV/RBW
# pwr_scale = 1.0 / (fftsize * fftsize)
# Uncomment for dBm/Hz into 50 ohms
pwr_scale = 1000.0 / (2.0 * fftsize * rate * 50.0)
if fftdecim:
pwr_scale /= fftdecim
a_dBm = 10.0 * np.log10(a * pwr_scale)

# Statistics
avg_db = np.average(a_dBm)
std_db = np.std(a_dBm)
min_db = np.min(a_dBm)
max_db = np.max(a_dBm)

# Set plot min/max using statistics
plot_min_db = avg_db - 3 * std_db
plot_max_db = avg_db + 6 * std_db

print(f'Drawing, min dB {min_db}, max dB {max_db}')
plt.style.use('dark_background')
plt.rcParams['lines.linewidth'] = 0.5
# plt.style.use('Solarize_Light2')
plot = plt.pcolormesh(a_dBm, vmin=plot_min_db, vmax=plot_max_db, cmap=cmap, antialiased=False)
plot = plt.pcolormesh(a_dBm, antialiased=False)
# colorbar(plot)
plt.show()
sys.exit()

fig, ax = plt.subplots(nrows=7)

dbm_avg_v = np.average(a_dBm, axis=0)
dbm_std_v = np.std(a_dBm, axis=0)
dbm_max_v = np.max(a_dBm, axis=0)

diffs = np.diff(a_dBm)
box_width = 5
box = np.ones(box_width) / box_width

# diffs_avg = np.average(diffs, axis=0)
diffs_avg = np.diff(dbm_avg_v)
diffs_avg_smooth = np.convolve(diffs_avg, box, 'same')
diffs_smooth_std = np.std(diffs_avg_smooth)
diffs_thresh = np.abs(diffs_avg_smooth) > diffs_smooth_std
# iterations = int(box_width/2 - 1)
iterations = 2
diffs_eroded = ndi.binary_erosion(diffs_thresh, iterations=iterations)

diff_avg_v = np.convolve(np.average(diffs, axis=0), box, 'same')
diff_ero_v = diffs_eroded * np.sign(diffs_avg_smooth)

ax[0].plot(dbm_max_v)
ax[1].plot(dbm_std_v)
ax[2].plot(dbm_avg_v)
ax[3].plot(diffs_avg)
ax[4].plot(diffs_avg_smooth)
ax[5].plot(diffs_thresh)
ax[6].plot(diff_ero_v)
# ax5.plot(diffs_smooth_std)
# ax6.plot(diffs_std)

plt.show()
52 changes: 52 additions & 0 deletions scripts/record_zmq_fft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env python

# Subscribe to fft data topic. Output separate metadata and data files.

import json
import zmq

import numpy as np

URI = 'ipc:///tmp/gqrx_data'
TOPIC = b'data.fft.linear'
META_FILE = '/tmp/fft.meta'
DATA_FILE = '/tmp/fft.data'


def rx_loop(socket, outf=None, metaf=None):
fftsize = None
while True:
message = socket.recv_multipart()
if (message[0] == TOPIC):
meta = json.loads(message[1].decode())
if fftsize is None:
fftsize = meta['fftsize']

if len(message[2]) != fftsize * 4:
raise ValueError('Data length wrong or changed.')

# Copy out data.
if outf:
x = np.frombuffer(message[2], np.float32).tobytes()
outf.write(x)

# Copy out metadata. Rewrite json in case we want to add fields.
if metaf:
metaf.write(json.dumps(meta))
metaf.write('\n')


context = zmq.Context()
socket: zmq.Socket = context.socket(zmq.SUB)
socket.connect(URI)

# Subscribe to single topic.
socket.setsockopt(zmq.SUBSCRIBE, TOPIC)

with open(DATA_FILE, 'wb') as dataf, open(META_FILE, 'w') as metaf:
try:
rx_loop(socket, dataf, metaf)
except KeyboardInterrupt:
print()
except Exception as e:
print(str(e))
120 changes: 120 additions & 0 deletions scripts/record_zmq_fft_decim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env python

# Subscribe to fft data topic. Output separate metadata and data files.

import json
import zmq

import numpy as np

NUM_RECORDS = 10000
TIME_DECIM = 1
FFT_SIZE_OUT = 4096
URI = 'ipc:///tmp/gqrx_data'
TOPIC = b'data.fft.linear'
META_FILE = '/tmp/fft.meta'
DATA_FILE = '/tmp/fft.data'
STD_FILE = '/tmp/fft.std'


def rx_loop(socket, outf=None, metaf=None, stdf=None):
win = None
i = 0
count = NUM_RECORDS

prevFftDecimFactor = 0

# Time decimation circular buffer
win = np.zeros((TIME_DECIM, FFT_SIZE_OUT))

while True:
if NUM_RECORDS:
if count == 0:
break
else:
count -= 1
message = socket.recv_multipart()
if (message[0] == TOPIC):
meta = json.loads(message[1].decode())
fftsize = meta['fftsize']
fftDecimFactor = fftsize // FFT_SIZE_OUT

# Check that data length matches fftsize, bail if not
if len(message[2]) != fftsize * 4:
raise ValueError('Received incorrect data size')

# fftsize shorter than output size, ignore data
if (fftDecimFactor < 1):
print(f'Received length {fftsize}, require at least {FFT_SIZE_OUT}')
continue

a = np.frombuffer(message[2], np.float32)

# fftsize larger than output size, decimate using rectangular
# window and stride
if (fftDecimFactor > 1):
if (fftDecimFactor != prevFftDecimFactor):
fftDecimWindow = np.ones(fftDecimFactor) / (fftDecimFactor * fftDecimFactor)
prevFftDecimFactor = fftDecimFactor
a = np.convolve(a, fftDecimWindow, 'valid')[::fftDecimFactor]

win[i] = a

# Every TIME_DECIM vectors, write out decimated data and metadata.
if i == (TIME_DECIM - 1):

# Use maximum values from window. This is what Gqrx does.
if outf or metaf:
xmax = np.max(win, 0)

# Copy out data vector first, so metadata always refers to available data.
if outf:
x = xmax.astype(np.float32).tobytes()
outf.write(x)
outf.flush()

if stdf or metaf:
xstd = np.std(win, 0)

# Write std dev vector
if stdf:
x = xstd.astype(np.float32).tobytes()
stdf.write(x)
stdf.flush()

# Add more fields that readers would otherwise need to get from
# the data file. Write out metadata.
if metaf:
xavg = np.average(win, 0)
meta['avg'] = np.average(xavg)
meta['maxmax'] = max(xmax)
xmin = np.min(win, 0)
meta['minmin'] = min(xmin)
meta['minstd'] = min(xstd)
meta['maxstd'] = max(xstd)
meta['fftdecim'] = fftDecimFactor

metaf.write(json.dumps(meta))
metaf.write('\n')
metaf.flush()

i = 0

else:
i += 1


context = zmq.Context()
socket: zmq.Socket = context.socket(zmq.SUB)
socket.connect(URI)

# Subscribe to single topic.
socket.setsockopt(zmq.SUBSCRIBE, TOPIC)

with open(DATA_FILE, 'wb') as dataf, open(META_FILE, 'w') as metaf, open(STD_FILE, 'wb') as stdf:
try:
rx_loop(socket, dataf, metaf, stdf)
except KeyboardInterrupt:
print()
# except Exception as e:
# print(str(e))
Loading

0 comments on commit baf8e95

Please sign in to comment.