Added files, directories and comments.
[sdr-websocket.git] / sdrninja-server / client.py
1 ###############################################################################
2 #
3 # The MIT License (MIT)
4 #
5 # Copyright (c) Tavendo GmbH
6 # Modified by Russell Handorf
7 #
8 # Permission is hereby granted, free of charge, to any person obtaining a copy
9 # of this software and associated documentation files (the "Software"), to deal
10 # in the Software without restriction, including without limitation the rights
11 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 # copies of the Software, and to permit persons to whom the Software is
13 # furnished to do so, subject to the following conditions:
14 #
15 # The above copyright notice and this permission notice shall be included in
16 # all copies or substantial portions of the Software.
17 #
18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 # THE SOFTWARE.
25 #
26 ###############################################################################
27
28 import json, time
29 import sys, math, ctypes, numpy
30
31 from rtlsdr import *
32 from itertools import *
33 from radio_math import *
34 import operator
35
36 from numpy import mean
37 from random import randint
38 from twisted.internet import reactor
39 from autobahn.twisted.websocket import WebSocketClientFactory, \
40     WebSocketClientProtocol, \
41     connectWS
42
43 class SdrWrap(object):
44     "wrap sdr and try to manage tuning"
45     def __init__(self):
46         self.sdr = RtlSdr()
47         self.read_samples = self.sdr.read_samples
48         self.prev_fc = None
49         self.prev_fs = None
50         self.prev_g  = 19
51         self.sdr.gain = 19
52     def tune(self, fc, fs, g):
53         if fc == self.prev_fc and fs == self.prev_fs and g == self.prev_g:
54             return
55         if fc != self.prev_fc:
56             self.sdr.center_freq = fc
57         if fs != self.prev_fs:
58             self.sdr.sample_rate = fs
59         if g != self.prev_g:
60             self.sdr.gain = g
61         self.prev_fc = fc
62         self.prev_fs = fs
63         self.prev_g  = g
64         time.sleep(0.04)  # wait for settle
65         self.sdr.read_samples(2**11)  # clear buffer
66         configure_highlight()
67     def gain_change(self, x):
68         # the whole 10x gain number is annoying
69         real_g = int(self.prev_g * 10)
70         i = self.sdr.GAIN_VALUES.index(real_g)
71         i += x
72         i = min(len(self.sdr.GAIN_VALUES) -1, i)
73         i = max(0, i)
74         new_g = self.sdr.GAIN_VALUES[i]
75         self.sdr.gain = new_g / 10.0
76         self.prev_g   = new_g / 10.0
77
78 sdr = SdrWrap()
79
80 class Stateful(object):
81     "bucket of globals"
82     def __init__(self):
83         self.freq_lower = None
84         self.freq_upper = None
85         self.vertexes   = []   # (timestamp, vertex_list)
86         self.batches    = []
87         self.time_start = None
88         self.viewport   = None
89         self.history    = 60   # seconds
90         self.fps        = 10
91         self.focus      = False
92         self.hover      = 0
93         self.highlight  = False
94         self.hl_mode    = None  # set this to a function!
95         self.hl_lo      = None
96         self.hl_hi      = None
97         self.hl_filter  = None
98         self.hl_pixels  = None
99         self.width      = 1260
100         self.shiftfreq  = time.time() +10
101
102 state = Stateful()
103
104 #state.freq_lower = float(929e6)
105 #state.freq_upper = float(930e6)
106 state.freq_lower = float(100e6)
107 state.freq_upper = float(101e6)
108 state.time_start = time.time()
109 state.viewport = (0,0,1,1)
110
111 def x_to_freq(x):
112     vp = state.viewport
113     delta = state.freq_upper - state.freq_lower
114     return delta * x / state.width + state.freq_lower
115
116 def log2(x):
117     return math.log(x)/math.log(2)
118
119 def acquire_sample(center, bw, detail, samples=8, relay=None):
120     "collect a single frequency"
121     assert bw <= 2.8e6
122     if detail < 8:
123         detail = 8
124     sdr.tune(center, bw, sdr.prev_g)
125     detail = 2**int(math.ceil(log2(detail)))
126     sample_count = samples * detail
127     data = sdr.read_samples(sample_count)
128     ys,xs = psd(data, NFFT=detail, Fs=bw/1e6, Fc=center/1e6)
129     ys = 10 * numpy.log10(ys)
130     if relay:
131         relay(data)
132     return xs, ys
133
134 def mapping(x):
135     "assumes -50 to 0 range, returns color"
136     r = int((x+50) * 255 // 50)
137     r = max(0, r)
138     r = min(255, r)
139     return r,r,100
140
141 def render_sample(now, dt, freqs, powers):
142     #quads = []
143     #colors = []
144     #row = [None]*4096
145     #interval = int(round(4096/state.width)+1)
146     interval = int(round(4096/state.width))
147     row = [None]*(1024)
148     temp=0
149     counter = 0
150     avgrgb=tuple([0,0,0])
151
152     #server side centering
153     #pad = (((4096-state.width)/interval)/interval)/interval
154     #for temp in range(0,pad):
155     #    row[temp]={"r": 0, "g": 0, "b": 100}
156
157     #temp=pad
158
159     for i,f in enumerate(freqs):
160         rgb = mapping(powers[i])
161         if (counter < interval):
162             avgrgb=tuple(map(operator.add, avgrgb, rgb))
163             #print "added {0}".format(rgb)
164             #print "sum {0}".format(avgrgb)
165             counter+=1
166         else:
167             #print "sum {0}".format(avgrgb)
168             #avgrgb=tuple(map(mean, zip(avgrgb)))
169             avgrgb=tuple(x/interval for x in avgrgb)
170             avgrgb=tuple(map(int, avgrgb))
171             #print "average {0}".format(avgrgb)
172             if (temp<(state.width+1)): 
173                 row[temp]={"r": avgrgb[0], "g": avgrgb[1], "b": avgrgb[2]}
174             temp+=1
175             counter=0
176             avgrgb=tuple([0,0,0])
177             #print temp
178             #print "reset {0}".format(avgrgb)
179         #row[i]={"r": rgb[0], "g": rgb[1], "b": rgb[2]}
180
181     #server side centering
182     #for tmp in range(0,(state.width-temp)):
183     #    row[temp+tmp]={"r": 0, "g": 0, "b": 100}
184
185     return(json.dumps(row).encode('utf8'))
186     #self.sendMessage(json.dumps(row).encode('utf8'))
187     #return(json.dumps(row))
188
189 def acquire_range(lower, upper):
190     "automatically juggles frequencies"
191     delta  = upper - lower
192     center = (upper+lower)/2
193     #if delta < 1.4e6:
194     if delta < 2.8e6:
195         # single sample
196         return acquire_sample(center, 2.8e6,
197             detail=state.width*2.8e6/delta,
198             relay=state.hl_mode)
199     xs2 = numpy.array([])
200     ys2 = numpy.array([])
201     detail = state.width // ((delta)/(2.8e6))
202     for f in range(int(lower), int(upper), int(2.8e6)):
203         xs,ys = acquire_sample(f+1.4e6, 2.8e6, detail=detail)
204         xs2 = numpy.append(xs2, xs)
205         ys2 = numpy.append(ys2, ys)
206     return xs2, ys2
207
208 def configure_highlight():
209     if not state.highlight:
210         return
211     pass_fc = (state.hl_lo + state.hl_hi) / 2
212     pass_bw = state.hl_hi - state.hl_lo
213     if pass_bw == 0:
214         return
215     state.hl_filter = Bandpass(sdr.prev_fc, sdr.prev_fs,
216                                pass_fc, pass_bw)
217
218 class BroadcastClientProtocol(WebSocketClientProtocol):
219
220     """
221     Simple client that connects to a WebSocket server, send a HELLO
222     message every 2 seconds and print everything it receives.
223     """
224     canvasWidth = 1260
225
226     def randomrow(self):
227         row=[None]*self.canvasWidth
228         for x in range (0,self.canvasWidth):
229           r=randint(0,255)
230           g=randint(0,255)
231           b=randint(0,255)
232           row[x]={"r": r, "g": g, "b": b}
233         #print(json.dumps(row).encode('utf8'))
234         self.sendMessage(json.dumps(row).encode('utf8'))
235         reactor.callLater(1, self.randomrow)
236         #return(json.dumps(row))
237
238     def update(self):
239         now = time.time() - state.time_start
240         if (state.shiftfreq < time.time()):
241             state.shiftfreq = time.time()+20
242             #state.freq_lower = randint(24e6,1766e6)
243             state.freq_lower = randint(80e6,105e6)
244             state.freq_upper = state.freq_lower + 1e6
245             #print "jumping frequency to {0}".format(state.freq_lower)
246         dt = 1.0/state.fps
247         freqs,power = acquire_range(state.freq_lower, state.freq_upper)
248         self.sendMessage(render_sample(now, dt, freqs, power))
249         reactor.callLater(0.1, self.update)
250
251     def onOpen(self):
252         #self.randomrow()
253         self.update()
254
255
256 if __name__ == '__main__':
257
258     if len(sys.argv) < 2:
259         print("Need the WebSocket server address, i.e. ws://localhost:9000")
260         sys.exit(1)
261
262     factory = WebSocketClientFactory(sys.argv[1]) 
263     #factory = WebSocketClientFactory("ws://localhost:9000")
264     factory.protocol = BroadcastClientProtocol
265     connectWS(factory)
266
267     reactor.run()