-
Notifications
You must be signed in to change notification settings - Fork 15
/
utils_supersdr.py
2129 lines (1862 loc) · 92.3 KB
/
utils_supersdr.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import random
import struct
import array
import math
from collections import deque, defaultdict
import pickle
import threading, queue
import socket
import time
from datetime import datetime, timedelta
import sys
import urllib
if sys.version_info > (3,):
buffer = memoryview
def bytearray2str(b):
return b.decode('ascii')
else:
def bytearray2str(b):
return str(b)
import numpy as np
from scipy.signal import resample_poly, welch
import sounddevice as sd
import wave
import tkinter
from tkinter import *
from pygame.locals import *
import pygame, pygame.font, pygame.event, pygame.draw, string, pygame.freetype
from qrz_utils import *
from kiwi import wsclient
import mod_pywebsocket.common
from mod_pywebsocket.stream import Stream
from mod_pywebsocket.stream import StreamOptions
from mod_pywebsocket._stream_base import ConnectionTerminatedException
VERSION = "v3.14"
TENMHZ = 10000 # frequency threshold for auto mode (USB/LSB) switch
CW_PITCH = 0.6 # CW offset from carrier in kHz
# Initial KIWI receiver parameters
LOW_CUT_SSB = 30 # Bandpass low end SSB
HIGH_CUT_SSB = 3000 # Bandpass high end
LOW_CUT_CW = int(CW_PITCH*1000-200) # Bandpass for CW
HIGH_CUT_CW = int(CW_PITCH*1000+200) # High end CW
HIGHLOW_CUT_AM = 6000 # Bandpass AM
delta_low, delta_high = 0., 0. # bandpass tuning
default_kiwi_port = 8073
default_kiwi_password = ""
# predefined RGB colors
GREY = (200,200,200)
WHITE = (255,255,255)
BLACK = (0,0,0)
D_GREY = (50,50,50)
D_RED = (200,0,0)
D_BLUE = (0,0,200)
D_GREEN = (0,120,0)
RED = (255,0,0)
BLUE = (0,0,255)
GREEN = (0,255,0)
YELLOW = (200,180,0)
ORANGE = (255,140,0)
ALLOWED_KEYS = [K_0, K_1, K_2, K_3, K_4, K_5, K_6, K_7, K_8, K_9]
ALLOWED_KEYS += [K_KP0, K_KP1, K_KP2, K_KP3, K_KP4, K_KP5, K_KP6, K_KP7, K_KP8, K_KP9]
ALLOWED_KEYS += [K_BACKSPACE, K_RETURN, K_ESCAPE, K_KP_ENTER]
HELP_MESSAGE_LIST = ["SuperSDR %s HELP" % VERSION,
"",
"- LEFT/RIGHT: move KIWI RX freq +/- 1kHz (+SHIFT: x10)",
"- PAGE UP/DOWN: move WF freq +/- SPAN/4",
"- UP/DOWN: zoom in/out by a factor 2X",
"- U/L/C/A: switch to USB, LSB, CW, AM",
"- J/K/O: tune RX low/high cut (SHIFT inverts, try CTRL!), O resets",
"- CTRL+O: reset window size to native 1024 bins",
"- G/H: inc/dec spectrum and WF averaging to improve SNR",
"- ,/.(+SHIFT) change high(low) clip level for spectrum and WF",
"- E: start/stop audio recording",
"- F: enter frequency with keyboard",
"- W/R: Write/Recall quick cyclic memory (up to 10)",
"- SHIFT+W: Save all memories to disk",
"- SHIFT+R: Delete all stored memories",
"- V/B: up/down volume 10%, SHIFT+V mute/unmute",
"- M: S-METER show/hide",
"- Y: activate SUB RX or switch MAIN/SUB RX (+SHIFT kills it)",
"- S: SYNC CAT and KIWI RX ON/OFF -> SPLIT mode for RTX",
"- Z: Center KIWI RX, shift WF instead",
"- SPACE: FORCE SYNC of WF to RX if no CAT, else sync to CAT",
"- X: AUTO MODE ON/OFF depending on amateur/broadcast band",
"- D: connect/disconnect from DXCLUSTER server",
"- I: show/hide EIBI database stations",
"- Q: switch to a different KIWI server",
"- 1/2 & 3: adjust AGC threshold (+SHIFT decay), 3 WF autoscale",
"- 0/9: [LOGGER] add QSO to log / open search QSO dialog",
"- 4: enable/disable spectrum filling",
"- 5/6: pan audio left/right for active RX",
"- SHIFT+ESC: quits"]
font_size_dict = {"small": 12, "medium": 16, "big": 18}
pygame.init()
nanofont = pygame.freetype.Font("TerminusTTF-4.49.1.ttf", 10)
microfont = pygame.freetype.Font("TerminusTTF-4.49.1.ttf", 12)
smallfont = pygame.freetype.Font("TerminusTTF-Bold-4.49.1.ttf", 16)
midfont = pygame.freetype.Font("TerminusTTF-4.49.1.ttf", 16)
bigfont = pygame.freetype.Font("TerminusTTF-Bold-4.49.1.ttf", 20)
hugefont = pygame.freetype.Font("TerminusTTF-4.49.1.ttf", 35)
class flags():
# global mutable flags
auto_mode = True
input_freq_flag = False
input_server_flag = False
show_help_flag = False
s_meter_show_flag = False
show_eibi_flag = False
show_mem_flag = True
show_dxcluster_flag = False
connect_dxcluster_flag = False
input_callsign_flag = False
input_qso_flag = False
dualrx_flag = False
click_drag_flag = False
start_drag_x = None
wf_cat_link_flag = True
wf_snd_link_flag = False
cat_snd_link_flag = True
main_sub_switch_flag = False
tk_log_new_flag = False
tk_log_search_flag = False
tk_kiwi_flag = False
class audio_recording():
CHANNELS = 1
def __init__(self, kiwi_snd):
self.filename = ""
self.audio_buffer = []
self.kiwi_snd = kiwi_snd
self.frames = []
self.recording_flag = False
def start(self):
self.filename = "supersdr_%sUTC.wav"%datetime.utcnow().isoformat().split(".")[0].replace(":", "_")
print("start recording")
self.audio_buffer = []
self.recording_flag = True
def stop(self):
print("stop recording")
self.recording_flag = False
self.save()
def save(self):
self.wave = wave.open(self.filename, 'wb')
self.wave.setnchannels(self.CHANNELS)
self.wave.setsampwidth(2) # two bytes per sample (int16)
self.wave.setframerate(self.kiwi_snd.AUDIO_RATE)
# process audio data here
self.wave.writeframes(b''.join(self.audio_buffer))
self.wave.close()
self.recording = False
class dxcluster():
CLEANUP_TIME = 120
UPDATE_TIME = 10
SPOT_TTL_BASETIME = 600
color_dict = {0: GREEN, SPOT_TTL_BASETIME: YELLOW, SPOT_TTL_BASETIME*2: ORANGE, SPOT_TTL_BASETIME*3: RED, SPOT_TTL_BASETIME*4: GREY}
def __init__(self, mycall_):
if mycall_ == "":
raise
self.mycall = mycall_
host, port = 'dxfun.com', 8000
self.server = (host, port)
self.spot_dict = {}
self.visible_stations = []
self.terminate = False
self.failed_counter = 0
self.update_now = False
def disconnect(self):
self.terminate = True
try:
self.sock.shutdown(1)
self.sock.close()
except:
pass
print("DXCLuster disconnected!")
def connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connected = False
while not connected:
print("Connecting to: %s:%d" % self.server)
try:
self.sock.connect(self.server)
except:
print('Impossibile to connect')
sleep(5)
else:
# self.sock.settimeout(1)
print('Connected!!!')
connected = True
self.send(self.mycall)
self.time_to_live = self.SPOT_TTL_BASETIME*5 # seconds for a spot to live
self.last_update = datetime.utcnow()
self.last_cleanup = datetime.utcnow()
def send(self, msg):
msg = msg + '\n'
self.sock.send(msg.encode())
def keepalive(self):
try:
self.send(chr(8))
self.receive()
except:
print("DX cluster failed to reply to keepalive msg")
def receive(self):
try:
msg = self.sock.recv(2048)
msg = msg.decode("utf-8")
except:
msg = None
#print("DX cluster msg decode failed")
return msg
def decode_spot(self, line):
els = line.split(" ")
els = [x for x in els if x]
spotter = els[0][6:].replace(":", "")
utc = datetime.utcnow()
try:
qrg = float(els[1].strip())
callsign = els[2].strip()
dxde_callsign = els[0][6:].split(":")[0]
print("New SPOT:", utc.strftime('%H:%M:%SZ'), qrg, "kHz", callsign, "DX de", dxde_callsign)
except:
qrg, callsign, utc = None, None, None
print("DX cluster msg decode failed: %s"%els)
return qrg, callsign, utc, els
def clean_old_spots(self):
now = datetime.utcnow()
del_list = []
for spot_id in self.spot_dict.keys():
spot_utc = self.spot_dict[spot_id][2]
duration = now - spot_utc
duration_in_s = duration.total_seconds()
if duration_in_s > self.time_to_live:
del_list.append(spot_id)
for spot_id in del_list:
del self.spot_dict[spot_id]
print("Number of spots in memory:", len(self.spot_dict.keys()))
def run(self, kiwi_wf):
self.connect()
while not self.terminate:
try:
dx_cluster_msg = self.receive()
except:
continue
spot_str = "%s"%dx_cluster_msg
stored_something_flag = False
for line in spot_str.replace("\x07", "").split("\n"):
if "DX de " in line:
qrg, callsign, utc, spot_msg = self.decode_spot(line)
if qrg and callsign:
self.store_spot(qrg, callsign, utc, spot_msg)
stored_something_flag = True
else:
continue
if stored_something_flag:
self.update_now = True
delta_t = (datetime.utcnow() - self.last_cleanup).total_seconds()
if delta_t > self.CLEANUP_TIME: # cleanup db and keepalive msg
self.keepalive()
self.clean_old_spots()
self.last_cleanup = datetime.utcnow()
# print("DXCLUST: cleaned old spots")
delta_t = (datetime.utcnow() - self.last_update).total_seconds()
if delta_t > self.UPDATE_TIME or self.update_now:
self.get_stations(kiwi_wf.start_f_khz, kiwi_wf.end_f_khz)
# print("DXCLUST: updated visible spots")
self.last_update = datetime.utcnow()
self.update_now = False
print("Exited from DXCLUSTER loop")
def store_spot(self, qrg_, callsign_, utc_, spot_msg_):
spot_id = next(self.unique_id()) # create a unique hash for each spot
self.spot_dict[spot_id] = (callsign_, qrg_, utc_, spot_msg_) # save spots as elements of a dictionary with hashes as keys
def get_stations(self, start_f, end_f):
count_dupes_dict = defaultdict(list)
self.visible_stations = []
for spot_id in self.spot_dict.keys():
(callsign_, qrg_, utc_, spot_msg_) = self.spot_dict[spot_id]
if start_f < qrg_ < end_f:
count_dupes_dict[callsign_].append(spot_id)
self.visible_stations.append(spot_id)
self.visible_stations = sorted(self.visible_stations, key=lambda spot_id: self.spot_dict[spot_id][1])
for call in count_dupes_dict.keys():
same_call_list = []
if len(count_dupes_dict[call])>1:
same_call_list = sorted([spot_id for spot_id in count_dupes_dict[call]], key = lambda spot_id: self.spot_dict[spot_id][2])
for spot_id in same_call_list[:-1]:
self.visible_stations.remove(spot_id)
del self.spot_dict[spot_id]
def unique_id(self):
seed = random.getrandbits(32)
while True:
yield seed
seed += 1
class filtering():
def __init__(self, fl, fs):
b = fl/fs
N = int(np.ceil((4 / b)))
if not N % 2: N += 1 # Make sure that N is odd.
self.n_tap = N
self.h = np.sinc(2. * fl / fs * (np.arange(N) - (N - 1) / 2.))
w = np.blackman(N)
# Multiply sinc filter by window.
self.h = self.h * w
# Normalize to get unity gain.
self.h = self.h / np.sum(self.h)
def lowpass(self, signal):
filtered_sig = np.convolve(signal, self.h, mode="valid")
return filtered_sig
class memory():
def __init__(self):
self.mem_list = deque([], 10)
self.index = 0
# try:
# self.load_from_disk()
# except:
# pass
# self.index = len(self.mem_list)
def write_mem(self, freq, radio_mode, delta_low, delta_high):
self.mem_list.append((round(freq, 3), radio_mode, delta_low, delta_high))
def recall_mem(self):
if len(self.mem_list)>0:
self.index += 1
self.index %= len(self.mem_list)
return self.mem_list[self.index]
else:
return None
def reset_all_mem(self):
self.mem_list = deque([], 10)
def save_to_disk(self):
current_mem = self.mem_list
self.load_from_disk()
self.mem_list += current_mem
self.mem_list = list(set(self.mem_list))
try:
with open("supersdr.memory", "wb") as fd:
pickle.dump(self.mem_list, fd)
except:
print("Cannot save memory file!")
def load_from_disk(self):
try:
with open("supersdr.memory", "rb") as fd:
self.mem_list = pickle.load(fd)
except:
print("No memory file found!")
class kiwi_list():
def __init__(self):
self.kiwi_list_filename = "kiwi.list"
self.kiwi_host = ""
self.kiwi_port = ""
self.kiwi_password = ""
self.default_port = 8073
self.default_password = ""
self.connect_new_flag = False
try:
self.load_from_disk()
except:
pass
def save_to_disk(self):
no_file_flag = False
try:
with open(self.kiwi_list_filename, encoding="latin") as fd:
data = fd.readlines()
if len(data) == 0:
no_file_flag = True
except:
no_file_flag = True
try:
with open(self.kiwi_list_filename, "a") as fd:
col_count = self.kiwi_data.count(":")
if no_file_flag:
fd.write("KIWIHOST;KIWIPORT;KIWIPASSWORD;COMMENTS\n")
fd.write(self.kiwi_data.replace(":", ";") + ";"*(3-col_count) + "\n")
self.load_from_disk()
except:
print("Cannot save kiwi list to disk!")
def load_from_disk(self):
self.kiwi_list = []
try:
with open(self.kiwi_list_filename, encoding="latin") as fd:
data = fd.readlines()
label_list = data[0].rstrip().split(";")
for row in data[1:]:
col_count = row.count(";")
if row[0] == "#":
continue
fields = row.rstrip().split(";")
host = fields[0]
if len(host)==0:
continue
try:
port = int(fields[1])
except:
self.default_port
password = fields[2] if col_count>1 else ""
comments = fields[3] if col_count>2 else ""
self.kiwi_list.append((host, port, password, comments))
except:
print("No kiwi list file found!")
return None
def choose_kiwi_dialog(self):
self.root = tkinter.Tk()
self.root.protocol("WM_DELETE_WINDOW", self.root.destroy)
self.root.geometry("400x400+960+450")
self.root.resizable(False,False)
self.root.title("Choose a KiwiSDR")
self.root.bind('<Escape>', lambda event: self.root.destroy())
self.root.bind('<Return>', lambda event: self.connect_new_kiwi())
self.main_dialog = tkinter.Frame(self.root)
self.main_dialog.pack()
l = tkinter.Label(self.root, text = "Choose KiwiSDR")
l.config(font =("Mono", 14))
label_kiwi = tkinter.Label(text="Host:Port:Password")
self.entry_kiwi = tkinter.Entry()
self.entry_kiwi.focus()
l.pack()
label_kiwi.pack()
self.entry_kiwi.pack()
frame_text = tkinter.Frame(self.root, borderwidth=1)
frame_text.pack(fill=BOTH, expand=True)
scrollbar = Scrollbar(frame_text)
self.t = tkinter.Text(frame_text, height=15, width=55, yscrollcommand=scrollbar.set)
scrollbar.config(command=self.t.yview)
scrollbar.pack(side=RIGHT, fill=Y)
self.t.pack(side="left")
frame_bottom = tkinter.Frame(self.root, borderwidth=5)
frame_bottom.pack(fill=BOTH, expand=True)
self.b_connect = tkinter.Button(master=frame_bottom, text = "Connect", command = self.connect_new_kiwi)
self.b_connect_save = tkinter.Button(master=frame_bottom, text = "Save and Connect", command = lambda: self.connect_new_kiwi(True))
self.b_reload = tkinter.Button(master=frame_bottom, text = "Reload", command = self.reload_and_refresh)
self.b_cancel = tkinter.Button(master=frame_bottom, text = "Cancel", command = self.root.destroy)
self.b_connect.pack(side=LEFT)
self.b_connect_save.pack(side=LEFT)
self.b_cancel.pack(side=RIGHT)
self.b_reload.pack(side=RIGHT)
frame_bottom.pack()
print(self.kiwi_list)
self.refresh_list()
def refresh_list(self):
self.t.configure(state='normal')
self.t.delete(1.0, END)
for idx, kiwi_record in enumerate(self.kiwi_list):
kiwi_record = [str(el) for el in kiwi_record]
kiwi_string = ":".join(kiwi_record)+"\n"
kiwi_string = "%d. "%idx + kiwi_string
self.t.insert(END, kiwi_string)
self.t.configure(state='disabled')
def reload_and_refresh(self):
self.load_from_disk()
self.refresh_list()
def connect_new_kiwi(self, save_flag=False):
self.kiwi_host = None
self.kiwi_port = None
self.kiwi_password = None
self.kiwi_data = self.entry_kiwi.get()
if len(self.kiwi_data)<3: # user has chosen a number
try:
idx = int(self.kiwi_data)
self.kiwi_host = self.kiwi_list[idx][0]
self.kiwi_port = self.kiwi_list[idx][1]
self.kiwi_password = self.kiwi_list[idx][2]
self.connect_new_flag = True
self.root.destroy()
except:
print("Kiwi index number not found in list!")
else:
kiwi_data_list = self.kiwi_data.rstrip().split(":")
if len(kiwi_data_list) > 0:
if kiwi_data_list[0] == '':
return
self.kiwi_host = kiwi_data_list[0]
if len(kiwi_data_list) >= 2:
try:
self.kiwi_port = int(kiwi_data_list[1])
except:
self.kiwi_port = None
if len(kiwi_data_list) >= 3:
self.kiwi_password = kiwi_data_list[2]
self.connect_new_flag = True
self.root.destroy()
if save_flag:
self.save_to_disk()
class kiwi_sdr():
kiwi_status_dict = {}
active = True
offline = False
users = 0
users_max = 4
gps = (None, None)
qth = ""
freq_offset = 0
antenna = ""
kiwi_name = ""
min_freq, max_freq = None, None
def __init__(self, host, port, verbose_flag=False):
url = "http://%s:%d/status" % (host, port)
file = urllib.request.urlopen(url)
for line in file:
decoded_line = line.decode("utf-8").rstrip()
# print(decoded_line)
key, value = decoded_line.split("=")[0], decoded_line.split("=")[1]
self.kiwi_status_dict[key] = value
self.users = int(self.kiwi_status_dict["users"])
self.users_max = int(self.kiwi_status_dict["users_max"])
self.antenna = self.kiwi_status_dict["antenna"]
self.kiwi_name = self.kiwi_status_dict["name"]
self.qth = self.kiwi_status_dict["loc"]
self.active = True if self.kiwi_status_dict["status"] in ["active", "private"] else False
self.offline = False if self.kiwi_status_dict["offline"]=="no" else True
self.gps = (float(self.kiwi_status_dict["gps"].split(", ")[0][1:]),
float(self.kiwi_status_dict["gps"].split(", ")[1][:-1] ))
self.min_freq, self.max_freq = float(self.kiwi_status_dict["bands"].split("-")[0]), float(self.kiwi_status_dict["bands"].split("-")[1])
try:
self.freq_offset = float(self.kiwi_status_dict["freq_offset"])
except:
if verbose_flag:
print("Some status parameters not found! Old firmware?")
# self.freq_offset = self.min_freq
self.freq_offset = 0
if verbose_flag:
print (self.kiwi_status_dict)
class kiwi_waterfall():
MAX_FREQ = 30000
CENTER_FREQ = int(MAX_FREQ/2)
MAX_ZOOM = 14
WF_BINS = 1024
MAX_FPS = 23
MIN_DYN_RANGE = 40. # minimum visual dynamic range in dB
CLIP_LOWP, CLIP_HIGHP = 40., 100 # clipping percentile levels for waterfall colors
delta_low_db, delta_high_db = 0, 0
low_clip_db, high_clip_db = -120, -60 # tentative initial values for wf db limits
wf_min_db, wf_max_db = low_clip_db, low_clip_db+MIN_DYN_RANGE
kiwi_wf_timestamp = None
wf_buffer_len = 3
def __init__(self, host_, port_, pass_, zoom_, freq_, eibi, disp):
self.eibi = eibi
# kiwi hostname and port
self.host = host_
self.port = port_
self.password = pass_
print ("KiwiSDR Server: %s:%d" % (self.host, self.port))
self.zoom = zoom_
self.freq = freq_
self.averaging_n = 1
self.wf_auto_scaling = True
self.BINS2PIXEL_RATIO = disp.DISPLAY_WIDTH / self.WF_BINS
self.old_averaging_n = self.averaging_n
self.dynamic_range = self.MIN_DYN_RANGE
self.wf_white_flag = False
self.terminate = False
self.run_index = 0
if not self.freq:
self.freq = 14200
self.tune = self.freq
self.radio_mode = "USB"
print ("Zoom factor:", self.zoom)
self.span_khz = self.zoom_to_span()
self.start_f_khz = self.start_freq()
self.end_f_khz = self.end_freq()
self.div_list = []
self.subdiv_list = []
self.min_bin_spacing = 100 # minimum pixels between major ticks (/10 for minor ticks)
self.space_khz = 10 # initial proposed spacing between major ticks in kHz
self.counter, self.actual_freq = self.start_frequency_to_counter(self.start_f_khz)
# print ("Actual frequency:", self.actual_freq, "kHz")
self.socket = None
self.wf_stream = None
self.wf_color = None
self.freq_offset = 0
kiwi_sdr_status = kiwi_sdr(host_, port_, True)
print(kiwi_sdr_status.users, kiwi_sdr_status.users_max)
if kiwi_sdr_status.users == kiwi_sdr_status.users_max:
print ("Too many users!")
# raise Exception()
elif kiwi_sdr_status.offline or not kiwi_sdr_status.active:
print ("KiwiSDR offline or under maintenance! Failed to connect!")
raise Exception()
else:
self.freq_offset = kiwi_sdr_status.freq_offset/1000.0
# connect to kiwi WF server
print ("Trying to contact %s..."%self.host)
try:
self.socket = socket.socket()
self.socket.connect((self.host, self.port))
print ("Socket open...")
except:
print ("Failed to connect")
raise Exception()
self.start_stream()
while True:
msg = self.wf_stream.receive_message()
# print(msg)
if msg:
if bytearray2str(msg[0:3]) == "W/F":
break
elif "MSG center_freq" in bytearray2str(msg):
els = bytearray2str(msg[4:]).split()
self.MAX_FREQ = int(int(els[1].split("=")[1])/1000)
self.CENTER_FREQ = int(int(self.MAX_FREQ)/2)
self.span_khz = self.zoom_to_span()
self.start_f_khz = self.start_freq()
self.end_f_khz = self.end_freq()
self.counter, self.actual_freq = self.start_frequency_to_counter(self.start_f_khz)
elif "MSG wf_fft_size" in bytearray2str(msg):
els = bytearray2str(msg[4:]).split()
self.MAX_ZOOM = int(els[3].split("=")[1])
self.WF_BINS = int(els[0].split("=")[1])
self.MAX_FPS = int(els[2].split("=")[1])
self.bins_per_khz = self.WF_BINS / self.span_khz
self.wf_data = np.zeros((disp.WF_HEIGHT, self.WF_BINS))
self.wf_data_tmp = deque([], self.wf_buffer_len)
self.avg_spectrum_deque = deque([], self.averaging_n)
def gen_div(self):
self.space_khz = 10
self.div_list = []
self.subdiv_list = []
self.div_list = []
f_s = int(self.start_f_khz)
f_e = int(self.end_f_khz)
while self.div_list == [] and self.subdiv_list == []:
if self.bins_per_khz*self.space_khz > self.min_bin_spacing:
for f in range(f_s, f_e+1):
if not f%self.space_khz:
fbin = int(self.offset_to_bin(f-self.start_f_khz))
self.div_list.append(fbin)
if self.bins_per_khz*self.space_khz/10 > self.min_bin_spacing/10:
for f in range(f_s, f_e+1):
if not f%(self.space_khz/10):
fbin = int(self.offset_to_bin(f-self.start_f_khz))
self.subdiv_list.append(fbin)
self.space_khz *= 10
def start_stream(self):
self.kiwi_wf_timestamp = int(time.time())
uri = '/%d/%s' % (self.kiwi_wf_timestamp, 'W/F')
try:
handshake_wf = wsclient.ClientHandshakeProcessor(self.socket, self.host, self.port)
handshake_wf.handshake(uri)
request_wf = wsclient.ClientRequest(self.socket)
except:
return None
request_wf.ws_version = mod_pywebsocket.common.VERSION_HYBI13
stream_option_wf = StreamOptions()
stream_option_wf.mask_send = True
stream_option_wf.unmask_receive = False
self.wf_stream = Stream(request_wf, stream_option_wf)
print(self.wf_stream)
if self.wf_stream:
print ("Waterfall data stream active...")
# send a sequence of messages to the server, hardcoded for now
# max wf speed, no compression
msg_list = ['SET auth t=kiwi p=%s ipl=%s'%(self.password, self.password), 'SET zoom=%d start=%d'%(self.zoom,self.counter),\
'SET maxdb=-10 mindb=-110', 'SET wf_speed=4', 'SET wf_comp=0', "SET interp=13"]
for msg in msg_list:
self.wf_stream.send_message(msg)
print ("Starting to retrieve waterfall data...")
def zoom_to_span(self):
"""return frequency span in kHz for a given zoom level"""
assert(self.zoom >= 0 and self.zoom <= self.MAX_ZOOM)
self.span_khz = self.MAX_FREQ / 2**self.zoom
return self.span_khz
def start_frequency_to_counter(self, start_frequency_):
"""convert a given start frequency in kHz to the counter value used in _set_zoom_start"""
assert(start_frequency_ >= 0 and start_frequency_ <= self.MAX_FREQ)
self.counter = round(start_frequency_/self.MAX_FREQ * 2**self.MAX_ZOOM * self.WF_BINS)
start_frequency_ = self.counter * self.MAX_FREQ / self.WF_BINS / 2**self.MAX_ZOOM
return self.counter, start_frequency_
def start_freq(self):
self.start_f_khz = self.freq - self.span_khz/2
return self.start_f_khz
def end_freq(self):
self.end_f_khz = self.freq + self.span_khz/2
return self.end_f_khz
def offset_to_bin(self, offset_khz_):
bins_per_khz_ = self.WF_BINS / self.span_khz
return bins_per_khz_ * (offset_khz_)
def bins_to_khz(self, bins_):
bins_per_khz_ = self.WF_BINS / self.span_khz
return (1./bins_per_khz_) * (bins_) + self.start_f_khz
def deltabins_to_khz(self, bins_):
bins_per_khz_ = self.WF_BINS / self.span_khz
return (1./bins_per_khz_) * (bins_)
def receive_spectrum(self):
msg = self.wf_stream.receive_message()
if msg and bytearray2str(msg[0:3]) == "W/F": # this is one waterfall line
msg = msg[16:] # remove some header from each msg AND THE FIRST BIN!
self.spectrum = np.ndarray(len(msg), dtype='B', buffer=msg).astype(np.float32) # convert from binary data
self.keepalive()
def spectrum_db2col(self):
wf = self.spectrum
wf = -(255 - wf) # dBm
wf_db = wf - 13 + (3*self.zoom) # typical Kiwi wf cal and zoom correction
wf_db[0] = wf_db[1] # first bin is broken
if self.wf_auto_scaling:
# compute min/max db of the power distribution at selected percentiles
self.low_clip_db = np.percentile(wf_db, self.CLIP_LOWP)
self.high_clip_db = np.percentile(wf_db, self.CLIP_HIGHP)
self.dynamic_range = max(self.high_clip_db - self.low_clip_db, self.MIN_DYN_RANGE)
# shift chosen min to zero
wf_color_db = (wf_db - (self.low_clip_db+self.delta_low_db))
# standardize the distribution between 0 and 1 (at least MIN_DYN_RANGE dB will be allocated in the colormap if delta=0)
normal_factor_db = self.dynamic_range + self.delta_high_db
self.wf_color = wf_color_db / (normal_factor_db-self.delta_low_db)
self.wf_color = np.clip(self.wf_color, 0.0, 1.0)
self.wf_min_db = self.low_clip_db + self.delta_low_db - (3*self.zoom)
self.wf_max_db = self.low_clip_db + normal_factor_db - (3*self.zoom)
# standardize again between 0 and 255
self.wf_color *= 254
# clip exceeding values
self.wf_color = np.clip(self.wf_color, 0, 255)
def set_freq_zoom(self, freq_, zoom_):
self.freq = freq_
self.zoom = zoom_
self.zoom_to_span()
self.start_freq()
self.end_freq()
if zoom_ == 0: # 30 MHz span, WF freq should be 15 MHz
self.freq = self.CENTER_FREQ
self.start_freq()
self.end_freq()
self.span_khz = self.MAX_FREQ
else: # zoom level > 0
if self.start_f_khz<0: # did we hit the left limit?
#self.freq -= self.start_f_khz
self.freq = self.zoom_to_span()/2
self.start_freq()
self.end_freq()
self.zoom_to_span()
elif self.end_f_khz>self.MAX_FREQ: # did we hit the right limit?
self.freq = self.MAX_FREQ - self.zoom_to_span()/2
self.start_freq()
self.end_freq()
self.zoom_to_span()
self.counter, actual_freq = self.start_frequency_to_counter(self.start_f_khz)
msg = "SET zoom=%d start=%d" % (self.zoom, self.counter)
self.wf_stream.send_message(msg)
self.eibi.get_stations(self.start_f_khz, self.end_f_khz)
self.bins_per_khz = self.WF_BINS / self.span_khz
self.gen_div()
return self.freq
def keepalive(self):
self.wf_stream.send_message("SET keepalive")
def close_connection(self):
if not self.wf_stream:
return
try:
self.wf_stream.close_connection(mod_pywebsocket.common.STATUS_GOING_AWAY)
self.socket.close()
except Exception as e:
print ("exception: %s" % e)
def change_passband(self, delta_low_, delta_high_):
if self.radio_mode == "USB":
lc_ = LOW_CUT_SSB+delta_low_
hc_ = HIGH_CUT_SSB+delta_high_
elif self.radio_mode == "LSB":
lc_ = -HIGH_CUT_SSB-delta_high_
hc_ = -LOW_CUT_SSB-delta_low_
elif self.radio_mode == "AM":
lc_ = -HIGHLOW_CUT_AM-delta_low_
hc_ = HIGHLOW_CUT_AM+delta_high_
elif self.radio_mode == "CW":
lc_ = LOW_CUT_CW+delta_low_
hc_ = HIGH_CUT_CW+delta_high_
self.lc, self.hc = lc_, hc_
return lc_, hc_
def set_white_flag(self):
self.wf_color = np.ones_like(self.wf_color)*255
self.wf_data[0,:] = self.wf_color
def run(self):
while not self.terminate:
if self.averaging_n>1:
self.avg_spectrum_deque = deque([], self.averaging_n)
for avg_idx in range(self.averaging_n):
self.receive_spectrum()
self.avg_spectrum_deque.append(self.spectrum)
self.spectrum = np.mean(self.avg_spectrum_deque, axis=0)
else:
self.receive_spectrum()
self.run_index += 1
self.spectrum_db2col()
# print(len(self.wf_data_tmp))
self.wf_data_tmp.appendleft(self.wf_color)
if len(self.wf_data_tmp) > 0 and self.run_index > self.wf_buffer_len:
self.wf_data[1:,:] = self.wf_data[0:-1,:] # scroll wf array 1 line down
self.wf_data[0,:] = self.wf_data_tmp.pop() # overwrite top line with new data
return
class kiwi_sound():
# Soundedevice options
FORMAT = np.int16
CHANNELS = 2
AUDIO_RATE = 48000
KIWI_RATE = 12000
SAMPLE_RATIO = int(AUDIO_RATE/KIWI_RATE)
CHUNKS = 1
KIWI_SAMPLES_PER_FRAME = 512
def __init__(self, freq_, mode_, lc_, hc_, password_, kiwi_wf, buffer_len, volume_=100, host_=None, port_=None, subrx_=False):
self.subrx = subrx_
# connect to kiwi server
self.kiwi_wf = kiwi_wf
self.host = host_ if host_ else kiwi_wf.host
self.port = port_ if port_ else kiwi_wf.port
self.FULL_BUFF_LEN = max(1, buffer_len)
self.audio_buffer = queue.Queue(maxsize = self.FULL_BUFF_LEN)
self.terminate = False
self.volume = volume_
self.max_rssi_before_mute = -20
self.mute_counter = 0
self.muting_delay = 15
self.adc_overflow_flag = False
self.status = None
self.run_index = 0
self.delta_t = 0.0
self.rssi = -127
self.freq = freq_
self.radio_mode = mode_
self.lc, self.hc = lc_, hc_
# Kiwi parameters
self.on = True # AGC auto mode
self.hang = False # AGC hang
self.thresh = -80 # AGC threshold in dBm
self.slope = 0 # AGC slope decay
self.decay_other = 4000 # AGC decay time constant
self.decay_cw = 1000 # AGC decay time constant
self.gain = 50 # AGC manual gain
self.min_agc_delay, self.max_agc_delay = 400, 8000
self.decay = self.decay_other
self.audio_balance = 0.0
self.freq_offset = 0
kiwi_sdr_status = kiwi_sdr(self.host, self.port)
if kiwi_sdr_status.users == kiwi_sdr_status.users_max:
print ("Too many users! Failed to connect!")
# raise Exception()
elif kiwi_sdr_status.offline or not kiwi_sdr_status.active:
print ("KiwiSDR offline or under maintenance! Failed to connect!")
raise Exception()
else:
self.freq_offset = kiwi_sdr_status.freq_offset/1000.0
print ("Trying to contact server...")
try:
self.socket = socket.socket()
self.socket.connect((self.host, self.port)) # future: allow different kiwiserver for audio stream
new_timestamp = int(time.time())
if new_timestamp - kiwi_wf.kiwi_wf_timestamp > 5:
kiwi_wf.kiwi_wf_timestamp = new_timestamp
uri = '/%d/%s' % (kiwi_wf.kiwi_wf_timestamp, 'SND')
handshake_snd = wsclient.ClientHandshakeProcessor(self.socket, self.host, self.port)
handshake_snd.handshake(uri)
request_snd = wsclient.ClientRequest(self.socket)
request_snd.ws_version = mod_pywebsocket.common.VERSION_HYBI13
stream_option_snd = StreamOptions()
stream_option_snd.mask_send = True
stream_option_snd.unmask_receive = False
self.stream = Stream(request_snd, stream_option_snd)
print ("Audio data stream active...")
msg_list = ["SET auth t=kiwi p=%s ipl=%s"%(password_, password_),
"SET mod=%s low_cut=%d high_cut=%d freq=%.3f" % (self.radio_mode.lower(), self.lc, self.hc, self.freq),
"SET compression=0", "SET ident_user=SuperSDR","SET OVERRIDE inactivity_timeout=1000",
"SET agc=%d hang=%d thresh=%d slope=%d decay=%d manGain=%d" % (self.on, self.hang, self.thresh, self.slope, self.decay, self.gain),
"SET AR OK in=%d out=%d" % (self.KIWI_RATE, self.AUDIO_RATE)]
for msg in msg_list:
self.stream.send_message(msg)
while True:
msg = self.stream.receive_message()
if msg and "SND" == bytearray2str(msg[:3]):
break
elif msg and "MSG audio_init" in bytearray2str(msg):
msg = bytearray2str(msg)
els = msg[4:].split()
self.KIWI_RATE = int(int(els[1].split("=")[1]))
self.KIWI_RATE_TRUE = float(els[2].split("=")[1])
self.delta_t = self.KIWI_RATE_TRUE - self.KIWI_RATE
self.SAMPLE_RATIO = self.AUDIO_RATE/self.KIWI_RATE
except:
print ("Failed to connect to Kiwi audio stream")
raise
self.kiwi_filter = filtering(self.KIWI_RATE/2, self.AUDIO_RATE)
gcd = np.gcd((self.KIWI_RATE),self.AUDIO_RATE)