X-Git-Url: http://russells-world.com/code/?p=sdr-websocket.git;a=blobdiff_plain;f=sdrninja-server%2Fclient.py;fp=sdrninja-server%2Fclient.py;h=beda7a021c60d05fbeb3747092510c58e5fce29a;hp=0000000000000000000000000000000000000000;hb=ee61e3f582cb400a66be85d2bd4cb0570ba24b9b;hpb=61664285a6f1544c938a4879e74a921914930de9 diff --git a/sdrninja-server/client.py b/sdrninja-server/client.py new file mode 100644 index 0000000..beda7a0 --- /dev/null +++ b/sdrninja-server/client.py @@ -0,0 +1,267 @@ +############################################################################### +# +# 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()