--- /dev/null
+###############################################################################
+#
+# The MIT License (MIT)
+#
+# Copyright (c) Tavendo GmbH
+# Modified by Russell Handorf
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+###############################################################################
+
+import json, time
+import sys, math, ctypes, numpy
+
+from rtlsdr import *
+from itertools import *
+from radio_math import *
+import operator
+
+from numpy import mean
+from random import randint
+from twisted.internet import reactor
+from autobahn.twisted.websocket import WebSocketClientFactory, \
+ WebSocketClientProtocol, \
+ connectWS
+
+class SdrWrap(object):
+ "wrap sdr and try to manage tuning"
+ def __init__(self):
+ self.sdr = RtlSdr()
+ self.read_samples = self.sdr.read_samples
+ self.prev_fc = None
+ self.prev_fs = None
+ self.prev_g = 19
+ self.sdr.gain = 19
+ def tune(self, fc, fs, g):
+ if fc == self.prev_fc and fs == self.prev_fs and g == self.prev_g:
+ return
+ if fc != self.prev_fc:
+ self.sdr.center_freq = fc
+ if fs != self.prev_fs:
+ self.sdr.sample_rate = fs
+ if g != self.prev_g:
+ self.sdr.gain = g
+ self.prev_fc = fc
+ self.prev_fs = fs
+ self.prev_g = g
+ time.sleep(0.04) # wait for settle
+ self.sdr.read_samples(2**11) # clear buffer
+ configure_highlight()
+ def gain_change(self, x):
+ # the whole 10x gain number is annoying
+ real_g = int(self.prev_g * 10)
+ i = self.sdr.GAIN_VALUES.index(real_g)
+ i += x
+ i = min(len(self.sdr.GAIN_VALUES) -1, i)
+ i = max(0, i)
+ new_g = self.sdr.GAIN_VALUES[i]
+ self.sdr.gain = new_g / 10.0
+ self.prev_g = new_g / 10.0
+
+sdr = SdrWrap()
+
+class Stateful(object):
+ "bucket of globals"
+ def __init__(self):
+ self.freq_lower = None
+ self.freq_upper = None
+ self.vertexes = [] # (timestamp, vertex_list)
+ self.batches = []
+ self.time_start = None
+ self.viewport = None
+ self.history = 60 # seconds
+ self.fps = 10
+ self.focus = False
+ self.hover = 0
+ self.highlight = False
+ self.hl_mode = None # set this to a function!
+ self.hl_lo = None
+ self.hl_hi = None
+ self.hl_filter = None
+ self.hl_pixels = None
+ self.width = 1260
+ self.shiftfreq = time.time() +10
+
+state = Stateful()
+
+#state.freq_lower = float(929e6)
+#state.freq_upper = float(930e6)
+state.freq_lower = float(100e6)
+state.freq_upper = float(101e6)
+state.time_start = time.time()
+state.viewport = (0,0,1,1)
+
+def x_to_freq(x):
+ vp = state.viewport
+ delta = state.freq_upper - state.freq_lower
+ return delta * x / state.width + state.freq_lower
+
+def log2(x):
+ return math.log(x)/math.log(2)
+
+def acquire_sample(center, bw, detail, samples=8, relay=None):
+ "collect a single frequency"
+ assert bw <= 2.8e6
+ if detail < 8:
+ detail = 8
+ sdr.tune(center, bw, sdr.prev_g)
+ detail = 2**int(math.ceil(log2(detail)))
+ sample_count = samples * detail
+ data = sdr.read_samples(sample_count)
+ ys,xs = psd(data, NFFT=detail, Fs=bw/1e6, Fc=center/1e6)
+ ys = 10 * numpy.log10(ys)
+ if relay:
+ relay(data)
+ return xs, ys
+
+def mapping(x):
+ "assumes -50 to 0 range, returns color"
+ r = int((x+50) * 255 // 50)
+ r = max(0, r)
+ r = min(255, r)
+ return r,r,100
+
+def render_sample(now, dt, freqs, powers):
+ #quads = []
+ #colors = []
+ #row = [None]*4096
+ #interval = int(round(4096/state.width)+1)
+ interval = int(round(4096/state.width))
+ row = [None]*(1024)
+ temp=0
+ counter = 0
+ avgrgb=tuple([0,0,0])
+
+ #server side centering
+ #pad = (((4096-state.width)/interval)/interval)/interval
+ #for temp in range(0,pad):
+ # row[temp]={"r": 0, "g": 0, "b": 100}
+
+ #temp=pad
+
+ for i,f in enumerate(freqs):
+ rgb = mapping(powers[i])
+ if (counter < interval):
+ avgrgb=tuple(map(operator.add, avgrgb, rgb))
+ #print "added {0}".format(rgb)
+ #print "sum {0}".format(avgrgb)
+ counter+=1
+ else:
+ #print "sum {0}".format(avgrgb)
+ #avgrgb=tuple(map(mean, zip(avgrgb)))
+ avgrgb=tuple(x/interval for x in avgrgb)
+ avgrgb=tuple(map(int, avgrgb))
+ #print "average {0}".format(avgrgb)
+ if (temp<(state.width+1)):
+ row[temp]={"r": avgrgb[0], "g": avgrgb[1], "b": avgrgb[2]}
+ temp+=1
+ counter=0
+ avgrgb=tuple([0,0,0])
+ #print temp
+ #print "reset {0}".format(avgrgb)
+ #row[i]={"r": rgb[0], "g": rgb[1], "b": rgb[2]}
+
+ #server side centering
+ #for tmp in range(0,(state.width-temp)):
+ # row[temp+tmp]={"r": 0, "g": 0, "b": 100}
+
+ return(json.dumps(row).encode('utf8'))
+ #self.sendMessage(json.dumps(row).encode('utf8'))
+ #return(json.dumps(row))
+
+def acquire_range(lower, upper):
+ "automatically juggles frequencies"
+ delta = upper - lower
+ center = (upper+lower)/2
+ #if delta < 1.4e6:
+ if delta < 2.8e6:
+ # single sample
+ return acquire_sample(center, 2.8e6,
+ detail=state.width*2.8e6/delta,
+ relay=state.hl_mode)
+ xs2 = numpy.array([])
+ ys2 = numpy.array([])
+ detail = state.width // ((delta)/(2.8e6))
+ for f in range(int(lower), int(upper), int(2.8e6)):
+ xs,ys = acquire_sample(f+1.4e6, 2.8e6, detail=detail)
+ xs2 = numpy.append(xs2, xs)
+ ys2 = numpy.append(ys2, ys)
+ return xs2, ys2
+
+def configure_highlight():
+ if not state.highlight:
+ return
+ pass_fc = (state.hl_lo + state.hl_hi) / 2
+ pass_bw = state.hl_hi - state.hl_lo
+ if pass_bw == 0:
+ return
+ state.hl_filter = Bandpass(sdr.prev_fc, sdr.prev_fs,
+ pass_fc, pass_bw)
+
+class BroadcastClientProtocol(WebSocketClientProtocol):
+
+ """
+ Simple client that connects to a WebSocket server, send a HELLO
+ message every 2 seconds and print everything it receives.
+ """
+ canvasWidth = 1260
+
+ def randomrow(self):
+ row=[None]*self.canvasWidth
+ for x in range (0,self.canvasWidth):
+ r=randint(0,255)
+ g=randint(0,255)
+ b=randint(0,255)
+ row[x]={"r": r, "g": g, "b": b}
+ #print(json.dumps(row).encode('utf8'))
+ self.sendMessage(json.dumps(row).encode('utf8'))
+ reactor.callLater(1, self.randomrow)
+ #return(json.dumps(row))
+
+ def update(self):
+ now = time.time() - state.time_start
+ if (state.shiftfreq < time.time()):
+ state.shiftfreq = time.time()+20
+ #state.freq_lower = randint(24e6,1766e6)
+ state.freq_lower = randint(80e6,105e6)
+ state.freq_upper = state.freq_lower + 1e6
+ #print "jumping frequency to {0}".format(state.freq_lower)
+ dt = 1.0/state.fps
+ freqs,power = acquire_range(state.freq_lower, state.freq_upper)
+ self.sendMessage(render_sample(now, dt, freqs, power))
+ reactor.callLater(0.1, self.update)
+
+ def onOpen(self):
+ #self.randomrow()
+ self.update()
+
+
+if __name__ == '__main__':
+
+ if len(sys.argv) < 2:
+ print("Need the WebSocket server address, i.e. ws://localhost:9000")
+ sys.exit(1)
+
+ factory = WebSocketClientFactory(sys.argv[1])
+ #factory = WebSocketClientFactory("ws://localhost:9000")
+ factory.protocol = BroadcastClientProtocol
+ connectWS(factory)
+
+ reactor.run()