Added files, directories and comments.
[sdr-websocket.git] / sdrninja-server / client.py
diff --git a/sdrninja-server/client.py b/sdrninja-server/client.py
new file mode 100644 (file)
index 0000000..beda7a0
--- /dev/null
@@ -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()