forked from jimahlstrom/quisk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
configure.py
4509 lines (4452 loc) · 178 KB
/
configure.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 sys, wx, wx.lib, os, re, pickle, traceback, json, copy
# Quisk will alter quisk_conf_defaults to include the user's config file.
import quisk_conf_defaults as conf
import _quisk as QS
from quisk_widgets import QuiskPushbutton, QuiskCheckbutton, QuiskBitField, SliderBoxH, SliderBoxHH
from quisk_widgets import FreqFormatter
from quisk_widgets import wxVersion
if wxVersion in ('2', '3'):
import wx.combo as wxcombo
else:
wxcombo = wx # wxPython Phoenix
try:
from soapypkg import soapy
except:
soapy = None
# Settings is [
# 0: Radio_requested, a string radio name or "Ask me" or "ConfigFileRadio"
# 1: Radio in use and last used, a string radio name or "ConfigFileRadio"
# 2: List of radio names
# 3: Parallel list of radio dicts. These are all the parameters for the corresponding radio. In
# general, they are a subset of all the parameters listed in self.sections and self.receiver_data[radio_name].
# 4: Global data common to all radios. This is similar to the radio dicts. Available as local_conf.globals.
# ]
# radio_dict is a dictionary of variable names and text values for each radio including radio ConfigFileRadio.
# Only variable names from the specified radio and all sections are included. The data comes from the JSON file, and
# may be missing recently added config file items. Use GetValue() to get a configuration datum.
# local_conf is the single instance of class Configuration. conf is the configuration data from quisk_conf_defaults as
# over-writen by JSON data in radio_dict. Items in the radio_dict are generally strings. We convert these strings to Python
# integers, floats, etc. and write them to conf.
# The format for data items is read from quisk_conf_defaults.py, but there are data items not in this file. The
# dictionary name2format has formats and defaults for these additional items.
# The value is a tuple (format name, default value).
name2format = {
"digital_rx1_name":('text', ''), "digital_rx2_name":('text', ''), "digital_rx3_name":('text', ''),
"digital_rx4_name":('text', ''), "digital_rx5_name":('text', ''), "digital_rx6_name":('text', ''),
"digital_rx7_name":('text', ''), "digital_rx8_name":('text', ''), "digital_rx9_name":('text', ''),
"win_digital_rx1_name":('text', ''), "win_digital_rx2_name":('text', ''), "win_digital_rx3_name":('text', ''),
"win_digital_rx4_name":('text', ''), "win_digital_rx5_name":('text', ''), "win_digital_rx6_name":('text', ''),
"win_digital_rx7_name":('text', ''), "win_digital_rx8_name":('text', ''), "win_digital_rx9_name":('text', ''),
"lin_digital_rx1_name":('text', ''), "lin_digital_rx2_name":('text', ''), "lin_digital_rx3_name":('text', ''),
"lin_digital_rx4_name":('text', ''), "lin_digital_rx5_name":('text', ''), "lin_digital_rx6_name":('text', ''),
"lin_digital_rx7_name":('text', ''), "lin_digital_rx8_name":('text', ''), "lin_digital_rx9_name":('text', ''),
}
# Increasing the software version will display a message to re-read the soapy device.
soapy_software_version = 3
def FormatKhz(dnum): # Round to 3 decimal places; remove ending ".000"
t = "%.3f" % dnum
if t[-4:] == '.000':
t = t[0:-4]
return t
def FormatMHz(dnum): # Pretty print in MHz
t = "%.6f" % dnum
for i in range(3):
if t[-1] == '0':
t = t[0:-1]
else:
break
return t
def SortKey(x):
try:
k = float(x)
except:
k = 0.0
return k
class Configuration:
def __init__(self, app, AskMe=False, Radio=''): # Called first
global application, local_conf, Settings, noname_enable, platform_ignore, platform_accept
Settings = ["ConfigFileRadio", "ConfigFileRadio", [], [], {}]
self.globals = Settings[4] # Will be replaced by quisk_settings.json
application = app
local_conf = self
noname_enable = []
if sys.platform == 'win32':
platform_ignore = 'lin_'
platform_accept = 'win_'
else:
platform_accept = 'lin_'
platform_ignore = 'win_'
self.sections = []
self.receiver_data = []
self.StatePath = conf.settings_file_path
if not self.StatePath:
self.StatePath = os.path.join(app.QuiskFilesDir, "quisk_settings.json")
self.ReadState()
if AskMe == 'Same':
pass
elif Radio:
choices = Settings[2] + ["ConfigFileRadio"]
if Radio in choices:
if Settings[1] != Radio:
Settings[1] = Radio
self.settings_changed = True
else:
t = "There is no radio named %s. Radios are " % Radio
for choice in choices:
t = "%s%s, " % (t, choice)
t = t[0:-2] + '.'
dlg = wx.MessageDialog(application.main_frame, t, 'Specify Radio', wx.OK|wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
sys.exit(0)
elif AskMe or Settings[0] == "Ask me":
choices = Settings[2] + ["ConfigFileRadio"]
dlg = wx.SingleChoiceDialog(None, "", "Start Quisk with this Radio",
choices, style=wx.DEFAULT_FRAME_STYLE|wx.OK|wx.CANCEL)
dlg.SetSizeHints(dlg.GetCharWidth() * 36, -1, -1, -1)
try:
n = choices.index(Settings[1]) # Set default to last used radio
except:
pass
else:
dlg.SetSelection(n)
ok = dlg.ShowModal()
if ok != wx.ID_OK:
sys.exit(0)
select = dlg.GetStringSelection()
dlg.Destroy()
if Settings[1] != select:
Settings[1] = select
self.settings_changed = True
else:
Settings[1] = Settings[0]
if Settings[1] == "ConfigFileRadio":
Settings[2].append("ConfigFileRadio")
Settings[3].append({})
self.ParseConfig()
self.originalBandEdge = {} # save original BandEdge
self.originalBandEdge.update(conf.BandEdge)
self.UpdateGlobals()
def RequiredValues(self, radio_dict):
radio_type = radio_dict['hardware_file_type']
# Fill in required values
if radio_type == "SdrIQ":
radio_dict["use_sdriq"] = '1'
if radio_dict['hardware_file_name'] == "sdriqpkg/quisk_hardware.py":
radio_dict['hardware_file_name'] = "quisk_hardware_sdriq.py"
else:
radio_dict["use_sdriq"] = '0'
if radio_type == "Hermes":
radio_dict["hermes_bias_adjust"] = "False"
if radio_type == 'SoapySDR':
radio_dict["use_soapy"] = '1'
self.InitSoapyNames(radio_dict)
if radio_dict.get("soapy_file_version", 0) < soapy_software_version:
text = "Your SoapySDR device parameters are out of date. Please go to the radio configuration screen and re-read the device parameters."
dlg = wx.MessageDialog(None, text, 'Please Re-Read Device', wx.OK|wx.ICON_INFORMATION)
dlg.ShowModal()
dlg.Destroy()
else:
radio_dict["use_soapy"] = '0'
if radio_type not in ("HiQSDR", "Hermes", "Red Pitaya", "Odyssey", "Odyssey2"):
radio_dict["use_rx_udp"] = '0'
if radio_type in ("Hermes", "Red Pitaya", "Odyssey2"):
if "Hermes_BandDict" not in radio_dict:
radio_dict["Hermes_BandDict"] = {}
if "Hermes_BandDictTx" not in radio_dict:
radio_dict["Hermes_BandDictTx"] = {}
def UpdateGlobals(self):
self.RadioName = Settings[1]
application.BandPlanC2T = {}
if self.RadioName == "ConfigFileRadio":
application.BandPlan = conf.BandPlan
for mode, color in conf.BandPlanColors:
application.BandPlanC2T[color] = mode
elif "BandPlanColors" in Settings[4] and "BandPlan" in Settings[4]:
mode_dict = {"End":None}
for mode, color in Settings[4]["BandPlanColors"]:
mode_dict[mode] = color
application.BandPlanC2T[color] = mode
plan = []
for freq, mode in Settings[4]["BandPlan"]:
freq = int(float(freq) * 1E6 + 0.1)
try:
color = mode_dict[mode]
except:
print ("Missing color for mode", mode)
color = '#777777'
plan.append([freq, color])
application.BandPlan = plan
else:
application.BandPlan = conf.BandPlan
for mode, color in conf.BandPlanColors:
application.BandPlanC2T[color] = mode
if "MidiNoteDict" not in Settings[4]:
Settings[4]["MidiNoteDict"] = {}
self.settings_changed = True
self.MidiNoteDict = Settings[4]["MidiNoteDict"]
# Convert old to new format, October, 2021
for txt_note in list(local_conf.MidiNoteDict): # txt_note is a string
int_note = int(txt_note, base=0)
if int_note < 128:
target = local_conf.MidiNoteDict[txt_note]
if len(target) > 3 and target[-3] == " " and target[-2] in "+-" and target[-1] in "0123456789": # Jog wheel
key = "0xB0%02X" % int_note
elif target in ("Vol", "STo", "Rit", "Ys", "Yz", "Zo", "Tune"): # Knob
key = "0xB0%02X" % int_note
else: # Button. Enter as the Note On message.
key = "0x90%02X" % int_note
local_conf.MidiNoteDict[key] = target
del local_conf.MidiNoteDict[txt_note]
self.settings_changed = True
def UpdateConf(self): # Called second to update the configuration for the selected radio
# Items in the radio_dict are generally strings. Convert these strings to Python integers, floats,
# etc. and write them to conf.
if Settings[1] == "ConfigFileRadio":
return
radio_dict = self.GetRadioDict()
# fill in conf from our configuration data; convert text items to Python objects
errors = ''
for k, v in list(radio_dict.items()): # radio_dict may change size during iteration
if k == 'favorites_file_path': # A null string is equivalent to "not entered"
if not v.strip():
continue
if k in ('power_meter_local_calibrations', ): # present in configuration data but not in the config file
continue
if k[0:6] == 'soapy_': # present in configuration data but not in the config file
continue
if k[0:6] == 'Hware_': # contained in hardware file, not in configuration data nor config file
continue
try:
fmt = self.format4name[k]
except:
errors = errors + "Ignore obsolete parameter %s\n" % k
del radio_dict[k]
self.settings_changed = True
continue
k4 = k[0:4]
if k4 == platform_ignore:
continue
elif k4 == platform_accept:
k = k[4:]
fmt4 = fmt[0:4]
if fmt4 not in ('dict', 'list'):
i1 = v.find('#')
if i1 > 0:
v = v[0:i1]
try:
if fmt4 == 'text': # Note: JSON returns Unicode strings !!!
setattr(conf, k, v)
elif fmt4 == 'dict':
if isinstance(v, dict):
setattr(conf, k, v)
else:
raise ValueError()
elif fmt4 == 'list':
if isinstance(v, list):
setattr(conf, k, v)
else:
raise ValueError()
elif fmt4 == 'inte':
setattr(conf, k, int(v, base=0))
elif fmt4 == 'numb':
setattr(conf, k, float(v))
elif fmt4 == 'bool':
if v == "True":
setattr(conf, k, True)
else:
setattr(conf, k, False)
elif fmt4 == 'rfil':
pass
elif fmt4 == 'keyc': # key code
if v == "None":
x = None
else:
x = eval(v)
x = int(x)
if k == 'hot_key_ptt2' and not isinstance(x, int):
setattr(conf, k, wx.ACCEL_NORMAL)
else:
setattr(conf, k, x)
else:
print ("Unknown format for", k, fmt)
except:
del radio_dict[k]
self.settings_changed = True
errors = errors + "Failed to set %s to %s using format %s\n" % (k, v, fmt)
#traceback.print_exc()
if conf.color_scheme != 'A':
conf.__dict__.update(getattr(conf, 'color_scheme_' + conf.color_scheme))
self.RequiredValues(radio_dict) # Why not update conf too??? This only updates the radio_dict.
if errors:
dlg = wx.MessageDialog(None, errors,
'Update Settings', wx.OK|wx.ICON_ERROR)
ret = dlg.ShowModal()
dlg.Destroy()
def InitSoapyNames(self, radio_dict): # Set Soapy data items, but not the hardware available lists and ranges.
if radio_dict.get('soapy_getFullDuplex_rx', 0):
radio_dict["add_fdx_button"] = '1'
else:
radio_dict["add_fdx_button"] = '0'
name = 'soapy_gain_mode_rx'
if name not in radio_dict:
radio_dict[name] = 'total'
name = 'soapy_setAntenna_rx'
if name not in radio_dict:
radio_dict[name] = ''
name = 'soapy_gain_values_rx'
if name not in radio_dict:
radio_dict[name] = {}
name = 'soapy_gain_mode_tx'
if name not in radio_dict:
radio_dict[name] = 'total'
name = 'soapy_setAntenna_tx'
if name not in radio_dict:
radio_dict[name] = ''
name = 'soapy_gain_values_tx'
if name not in radio_dict:
radio_dict[name] = {}
def NormPath(self, path): # Convert between Unix and Window file paths
if sys.platform == 'win32':
path = path.replace('/', '\\')
else:
path = path.replace('\\', '/')
return path
def GetHardware(self): # Called third to open the hardware file
if Settings[1] == "ConfigFileRadio":
return False
path = self.GetRadioDict()["hardware_file_name"]
path = self.NormPath(path)
if not os.path.isfile(path):
dlg = wx.MessageDialog(None,
"Can not find the hardware file %s!" % path,
'Hardware File', wx.OK|wx.ICON_ERROR)
ret = dlg.ShowModal()
dlg.Destroy()
path = 'quisk_hardware_model.py'
dct = {}
dct.update(conf.__dict__) # make items from conf available
if "Hardware" in dct:
del dct["Hardware"]
if 'quisk_hardware' in dct:
del dct["quisk_hardware"]
exec(compile(open(path).read(), path, 'exec'), dct)
if "Hardware" in dct:
application.Hardware = dct['Hardware'](application, conf)
return True
return False
def Initialize(self): # Called fourth to fill in our ConfigFileRadio radio from conf
if Settings[1] == "ConfigFileRadio":
radio_dict = self.GetRadioDict("ConfigFileRadio")
typ = self.GuessType()
radio_dict['hardware_file_type'] = typ
all_data = []
all_data = all_data + self.GetReceiverData(typ)
for name, sdata in self.sections:
all_data = all_data + sdata
for data_name, text, fmt, help_text, values in all_data:
data_name4 = data_name[0:4]
if data_name4 == platform_ignore:
continue
elif data_name4 == platform_accept:
conf_name = data_name[4:]
else:
conf_name = data_name
try:
if fmt in ("dict", "list"):
radio_dict[data_name] = getattr(conf, conf_name)
else:
radio_dict[data_name] = str(getattr(conf, conf_name))
except:
if data_name == 'playback_rate':
pass
else:
print ('No config file value for', data_name)
def GetWidgets(self, app, hardware, conf, frame, gbs, vertBox): # Called fifth
if Settings[1] == "ConfigFileRadio":
return False
path = self.GetRadioDict().get("widgets_file_name", '')
path = self.NormPath(path)
if os.path.isfile(path):
dct = {}
dct.update(conf.__dict__) # make items from conf available
exec(compile(open(path).read(), path, 'exec'), dct)
if "BottomWidgets" in dct:
app.bottom_widgets = dct['BottomWidgets'](app, hardware, conf, frame, gbs, vertBox)
return True
def OnPageChanging(self, event): # Called when the top level page changes (not RadioNotebook pages)
event.Skip()
notebook = event.GetEventObject()
index = event.GetSelection()
page = notebook.GetPage(index)
if isinstance(page, RadioNotebook):
if not page.pages:
page.MakePages()
def AddPages(self, notebk, width): # Called sixth to add pages Help, Radios, all radio names
global win_width
win_width = width
self.notebk = notebk
self.radio_page = Radios(notebk)
notebk.AddPage(self.radio_page, "Radios")
self.radios_page_start = notebk.GetPageCount()
notebk.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanging, notebk)
if Settings[1] in Settings[2]:
page = RadioNotebook(notebk, Settings[1])
notebk.AddPage(page, "*%s*" % Settings[1])
for name in Settings[2]:
if name != Settings[1]:
page = RadioNotebook(notebk, name)
notebk.AddPage(page, name)
def GuessType(self):
udp = conf.use_rx_udp
if conf.use_sdriq:
return 'SdrIQ'
elif udp == 1:
return 'HiQSDR'
elif udp == 2:
return 'HiQSDR'
elif udp == 10:
return 'Hermes'
elif udp > 0:
return 'HiQSDR'
return 'SoftRock USB'
def AddRadio(self, radio_name, typ):
radio_dict = {}
radio_dict['hardware_file_type'] = typ
Settings[2].append(radio_name)
Settings[3].append(radio_dict)
for data_name, text, fmt, help_text, values in self.GetReceiverData(typ):
radio_dict[data_name] = values[0]
for name, data in self.sections:
for data_name, text, fmt, help_text, values in data:
radio_dict[data_name] = values[0]
# Change some default values in quisk_conf_defaults.py based on radio type
if typ in ("HiQSDR", "Hermes", "Red Pitaya", "Odyssey", "Odyssey2"):
radio_dict["add_fdx_button"] = '1'
page = RadioNotebook(self.notebk, radio_name)
self.notebk.AddPage(page, radio_name)
return True
def RenameRadio(self, old, new):
index = Settings[2].index(old)
n = self.radios_page_start + index
if old == Settings[1]:
self.notebk.SetPageText(n, "*%s*" % new)
else:
self.notebk.SetPageText(n, new)
Settings[2][index] = new
self.notebk.GetPage(n).NewName(new)
if old == "ConfigFileRadio":
for ctrl in noname_enable:
ctrl.Enable()
return True
def DeleteRadio(self, name):
index = Settings[2].index(name)
del Settings[2][index]
del Settings[3][index]
try:
n = self.radios_page_start + index
self.notebk.DeletePage(n)
except:
pass
return True
def GetRadioDict(self, radio_name=None): # None radio_name means the current radio
if radio_name:
index = Settings[2].index(radio_name)
else: # index of radio in use
index = Settings[2].index(Settings[1])
return Settings[3][index]
#def GetItem(self, name, deflt=None, accept=None): # return item or default. accept can be "win_" or "lin_"
# dct = self.GetRadioDict()
# if accept:
# return dct.get(accept+name, deflt)
# return dct.get(name, deflt)
def GetSectionData(self, section_name):
for sname, data in self.sections:
if sname == section_name:
return data
return None
def GetReceiverData(self, receiver_name):
for rxname, data in self.receiver_data:
if rxname == receiver_name:
return data
return ()
def GetReceiverDatum(self, receiver_name, item_name):
for rxname, data in self.receiver_data:
if rxname == receiver_name:
for data_name, text, fmt, help_text, values in data:
if item_name == data_name:
return values[0]
break
return ''
def GetReceiverItemTH(self, receiver_name, item_name):
for rxname, data in self.receiver_data:
if rxname == receiver_name:
for data_name, text, fmt, help_text, values in data:
if item_name == data_name:
return text, help_text
break
return '', ''
def ReceiverHasName(self, receiver_name, item_name):
for rxname, data in self.receiver_data:
if rxname == receiver_name:
for data_name, text, fmt, help_text, values in data:
if item_name == data_name:
return True
break
return False
def ReadState(self):
self.settings_changed = False
global Settings
try:
fp = open(self.StatePath, "r")
except:
return
try:
Settings = json.load(fp)
except:
traceback.print_exc()
fp.close()
try: # Do not save settings for radio ConfigFileRadio
index = Settings[2].index("ConfigFileRadio")
except ValueError:
pass
else:
del Settings[2][index]
del Settings[3][index]
for sdict in Settings[3]:
# Fixup for prior errors that save dictionaries as strings
for name in ("tx_level", "HiQSDR_BandDict", "Hermes_BandDict", "Hermes_BandDictTx"):
if name in sdict:
if not isinstance(sdict[name], dict):
print ("Bad dictionary for", name)
sdict[name] = {}
self.settings_changed = True
# Python None is saved as "null"
if "tx_level" in sdict:
if "null" in sdict["tx_level"]:
v = sdict["tx_level"]["null"]
sdict["tx_level"][None] = v
del sdict["tx_level"]["null"]
# Add global section if it is absent
length = len(Settings)
if length < 4: # Serious error
pass
elif length == 4: # Old file without global section
Settings.append({})
self.settings_changed = True
else: # Settings[4] must be a dict
if not isinstance(Settings[4], dict):
Settings[4] = {}
self.settings_changed = True
self.globals = Settings[4]
def SaveState(self):
if not self.settings_changed:
return
try:
fp = open(self.StatePath, "w")
except:
traceback.print_exc()
return
json.dump(Settings, fp, indent=2)
fp.close()
self.settings_changed = False
def ParseConfig(self):
# ParseConfig() fills self.sections, self.receiver_data, and
# self.format4name with the items that Configuration understands.
# Dicts and lists are Python objects. All other items are text, not Python objects.
#
# This parses the quisk_conf_defaults.py file and user-defined radios in ./*pkg/quisk_hardware.py.
#
# Sections start with 16 #, section name
# self.sections is a list of [section_name, section_data]
# section_data is a list of [data_name, text, fmt, help_text, values]
# Receiver sections start with 16 #, "Receivers ", receiver name, explain
# self.receiver_data is a list of [receiver_name, receiver_data]
# receiver_data is a list of [data_name, text, fmt, help_text, values]
# Variable names start with ## variable_name variable_text, format
# The format is integer, number, text, boolean, integer choice, text choice, rfile
# Then some help text starting with "# "
# Then a list of possible value#explain with the default first
# Then a blank line to end.
self.format4name = {}
for name in name2format:
self.format4name[name] = name2format[name][0]
self.format4name['hardware_file_type'] = 'text'
self._ParserConf('quisk_conf_defaults.py')
# Read any user-defined radio types
for dirname in os.listdir('.'):
if not os.path.isdir(dirname) or dirname[-3:] != 'pkg':
continue
if dirname in ('freedvpkg', 'sdriqpkg', 'soapypkg'):
continue
filename = os.path.join(dirname, 'quisk_hardware.py')
if not os.path.isfile(filename):
continue
try:
self._ParserConf(filename)
except:
traceback.print_exc()
def _ParserConf(self, filename):
re_AeqB = re.compile(r"^#?(\w+)\s*=\s*([^#]+)#*(.*)") # item values "a = b"
section = None
data_name = None
multi_line = False
fp = open(filename, "r", encoding="utf8")
for line in fp:
line = line.strip()
if not line:
data_name = None
continue
if line[0:27] == '################ Receivers ':
section = 'Receivers'
args = line[27:].split(',', 1)
rxname = args[0].strip()
section_data = []
self.receiver_data.append((rxname, section_data))
elif line[0:17] == '################ ':
section = line[17:].strip()
if section in ('Colors', 'Obsolete'):
section = None
continue
rxname = None
section_data = []
self.sections.append((section, section_data))
if not section:
continue
if line[0:3] == '## ': # item_name item_text, format
args = line[3:].split(None, 1)
data_name = args[0]
args = args[1].split(',', 1)
dspl = args[0].strip()
fmt = args[1].strip()
value_list = []
if data_name in self.format4name:
if self.format4name[data_name] != fmt:
print (filename, ": Inconsistent format for", data_name, self.format4name[data_name], fmt)
else:
self.format4name[data_name] = fmt
section_data.append([data_name, dspl, fmt, '', value_list])
multi_line = False
if not data_name:
continue
if multi_line:
value += line
#print ("Multi", data_name, type(value), value)
count = self._multi_count(value)
if count == 0:
value = eval(value, conf.__dict__)
value_list.append(value)
#print ("Multi done", data_name, type(value), value)
multi_line = False
continue
mo = re_AeqB.match(line)
if mo:
if data_name != mo.group(1):
print (filename, ": Parse error for", data_name)
continue
value = mo.group(2).strip()
expln = mo.group(3).strip()
if value[0] in ('"', "'"):
value = value[1:-1]
elif value[0] == '{': # item is a dictionary
if self._multi_count(value) == 0: # dictionary is complete
value = eval(value, conf.__dict__)
#print ("Single", data_name, type(value), value)
else:
multi_line = True
#print ("Start multi", data_name, type(value), value)
continue
elif value[0] == '[': # item is a list
if self._multi_count(value) == 0: # list is complete
value = eval(value, conf.__dict__)
#print ("Single", data_name, type(value), value)
else:
multi_line = True
#print ("Start multi", data_name, type(value), value)
continue
if expln:
value_list.append("%s # %s" % (value, expln))
else:
value_list.append(value)
elif line[0:2] == '# ':
section_data[-1][3] = section_data[-1][3] + line[2:] + ' '
fp.close()
def _multi_count(self, value):
char_start = value[0]
if char_start == '{':
char_end = '}'
elif char_start == '[':
char_end = ']'
count = 0
for ch in value:
if ch == char_start:
count += 1
elif ch == char_end:
count -= 1
return count
class xxConfigHelp(wx.html.HtmlWindow): # The "Help with Radios" first-level page
"""Create the help screen for the configuration tabs."""
def __init__(self, parent):
wx.html.HtmlWindow.__init__(self, parent, -1, size=(win_width, 100))
if "gtk2" in wx.PlatformInfo:
self.SetStandardFonts()
self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22])
self.SetBackgroundColour(parent.bg_color)
# read in text from file help_conf.html in the directory of this module
self.LoadFile('help_conf.html')
class QPowerMeterCalibration(wx.Frame):
"""Create a window to enter the power output and corresponding ADC value AIN1/2"""
def __init__(self, parent, local_names):
self.parent = parent
self.local_names = local_names
self.table = [] # calibration table: list of [ADC code, power watts]
try: # may be missing in wxPython 2.x
wx.Frame.__init__(self, application.main_frame, -1, "Power Meter Calibration",
pos=(50, 100), style=wx.CAPTION|wx.FRAME_FLOAT_ON_PARENT)
except AttributeError:
wx.Frame.__init__(self, application.main_frame, -1, "Power Meter Calibration",
pos=(50, 100), style=wx.CAPTION)
panel = wx.Panel(self)
self.MakeControls(panel)
self.Show()
def MakeControls(self, panel):
charx = panel.GetCharWidth()
tab1 = charx * 5
y = 20
# line 1
txt = wx.StaticText(panel, -1, 'Name for new calibration table', pos=(tab1, y))
w, h = txt.GetSize().Get()
tab2 = tab1 + w + tab1 // 2
self.cal_name = wx.TextCtrl(panel, -1, pos=(tab2, h), size=(charx * 16, h * 13 // 10))
y += h * 3
# line 2
txt = wx.StaticText(panel, -1, 'Measured power level in watts', pos=(tab1, y))
self.cal_power = wx.TextCtrl(panel, -1, pos=(tab2, y), size=(charx * 16, h * 13 // 10))
x = tab2 + charx * 20
add = QuiskPushbutton(panel, self.OnBtnAdd, "Add to Table")
add.SetPosition((x, y - h * 3 // 10))
add.SetColorGray()
ww, hh = add.GetSize().Get()
width = x + ww + tab1
y += h * 3
# line 3
sv = QuiskPushbutton(panel, self.OnBtnSave, "Save")
sv.SetColorGray()
cn = QuiskPushbutton(panel, self.OnBtnCancel, "Cancel")
cn.SetColorGray()
w, h = cn.GetSize().Get()
sv.SetPosition((width // 4, y))
cn.SetPosition((width - width // 4 - w, y))
y += h * 12 // 10
# help text at bottom
wx.StaticText(panel, -1, '1. Attach a 50 ohm load and power meter to the antenna connector.', pos=(tab1, y))
w, h = txt.GetSize().Get()
h = h * 12 // 10
y += h
wx.StaticText(panel, -1, '2. Use the Spot button to transmit at a very low power.', pos=(tab1, y))
y += h
wx.StaticText(panel, -1, '3. Enter the measured power in the box above and press "Add to Table".', pos=(tab1, y))
y += h
wx.StaticText(panel, -1, '4. Increase the power a small amount and repeat step 3.', pos=(tab1, y))
y += h
wx.StaticText(panel, -1, '5. Increase power again and repeat step 3.', pos=(tab1, y))
y += h
wx.StaticText(panel, -1, '6. Keep adding measurements to the table until you reach full power.', pos=(tab1, y))
y += h
wx.StaticText(panel, -1, '7. Ten or twelve measurements should be enough. Then press "Save".', pos=(tab1, y))
y += h
wx.StaticText(panel, -1, 'To delete a table, save a table with zero measurements.', pos=(tab1, y))
y += h * 2
self.SetClientSize(wx.Size(width, y))
def OnBtnCancel(self, event=None):
self.parent.ChangePMcalFinished(None, None)
self.Destroy()
def OnBtnSave(self, event):
name = self.cal_name.GetValue().strip()
if not name:
dlg = wx.MessageDialog(self,
'Please enter a name for the new calibration table.',
'Missing Name', wx.OK|wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
elif name in conf.power_meter_std_calibrations: # known calibration names from the config file
dlg = wx.MessageDialog(self,
'That name is reserved. Please enter a different name.',
'Reserved Name', wx.OK|wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
elif name in self.local_names:
if self.table:
dlg = wx.MessageDialog(self,
'That name exists. Replace the existing table?',
'Replace Table', wx.OK|wx.CANCEL|wx.ICON_EXCLAMATION)
ret = dlg.ShowModal()
dlg.Destroy()
if ret == wx.ID_OK:
self.parent.ChangePMcalFinished(name, self.table)
self.Destroy()
else:
dlg = wx.MessageDialog(self,
'That name exists but the table is empty. Delete the existing table?.',
'Delete Table', wx.OK|wx.CANCEL|wx.ICON_EXCLAMATION)
ret = dlg.ShowModal()
dlg.Destroy()
if ret == wx.ID_OK:
self.parent.ChangePMcalFinished(name, None)
self.Destroy()
else:
self.parent.ChangePMcalFinished(name, self.table)
self.Destroy()
def OnBtnAdd(self, event):
power = self.cal_power.GetValue().strip()
self.cal_power.Clear()
try:
power = float(power)
except:
dlg = wx.MessageDialog(self, 'Missing or bad measured power.', 'Error in Power', wx.OK|wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
else:
## Convert measured voltage to power
#power *= 6.388
#power = power**2 / 50.0
fwd = application.Hardware.hermes_fwd_power
rev = application.Hardware.hermes_rev_power
if fwd >= rev:
self.table.append([fwd, power]) # Item must use lists; sort() will fail with mixed lists and tuples
else:
self.table.append([rev, power])
## Note: The amplitude/phase adjustments have ideas provided by Andrew Nilsson, VK6JBL.
## October 2020: changed to make RxTx frequency and VFO two independent variables.
## June 2024: Added a Grid control and automatic receive measurement.
class QAdjustPhase(wx.Frame):
"""Create a window with amplitude and phase adjustment controls"""
def __init__(self, parent, width, rx_tx):
self.rx_tx = rx_tx # Must be "rx" or "tx"
if rx_tx == 'tx':
self.is_tx = 1
t = "Adjust Sound Card Transmit Amplitude and Phase"
else:
self.is_tx = 0
t = "Adjust Sound Card Receive Amplitude and Phase"
wx.Frame.__init__(self, application.main_frame, -1, t, pos=(50, 100), style=wx.CAPTION|wx.RESIZE_BORDER)
self.client_width = application.screen_width * 5 // 10
self.SetClientSize((self.client_width, application.screen_height * 5 // 10))
self.new_amplitude = self.new_phase = 0.0
self.new_tune = 0
self.bandAmplPhase = copy.deepcopy(application.bandAmplPhase)
self.new_cell = None
self.dirty = False
self.manual = True
self.panel = wx.Panel(self)
self.MakeControls()
self.Redraw()
self.Show()
self.Bind(wx.EVT_CLOSE, self.OnBtnExit)
QS.softrock_corrections(1)
QS.set_ampl_phase(0.0, 0.0, self.is_tx)
self.grid.GoToCell(0, 2)
def MakeControls(self): # Make controls for phase/amplitude adjustment
panel = self.panel
sizer = wx.BoxSizer(wx.VERTICAL)
panel.SetSizer(sizer)
sl_max = application.screen_width * 4 // 10 # maximum +/- value for slider
self.ampl_scale = float(conf.rx_max_amplitude_correct) / sl_max
self.phase_scale = float(conf.rx_max_phase_correct) / sl_max
main_font = wx.Font(conf.default_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
self.SetFont(main_font)
panel.SetFont(main_font)
charx = self.GetCharWidth()
chary = self.GetCharHeight()
# Create the grid heading and Destroy button
self.cell_amph = wx.StaticText(panel, -1, "")
font = wx.Font(conf.default_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_BOLD, False, conf.quisk_typeface)
self.cell_amph.SetFont(font)
self.cell_amph.SetLabel("Cell frequency %s, gain %.3f, phase %.3f\u00B0 " % (FreqFormatter(144000000), -1.123, -12.456))
dv = wx.lib.buttons.GenButton(panel, label=" Destroy ")
dv.SetBezelWidth(3)
dv.SetBackgroundColour("#DDD")
dv.Bind(wx.EVT_BUTTON, self.OnBtnDestroy)
hbox = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(hbox, flag=wx.BOTTOM|wx.EXPAND, border=chary * 3 // 10)
hbox.Add(self.cell_amph, flag=wx.ALL|wx.ALIGN_CENTER_VERTICAL, border=chary // 2)
hbox.Add(dv, flag=wx.LEFT|wx.RIGHT|wx.ALIGN_CENTER_VERTICAL, border=charx * 5)
# Create the grid
self.grid = wx.grid.Grid(panel, style=wx.BORDER_SIMPLE)
self.grid.CreateGrid(4, 3)
self.grid.HideRowLabels()
self.selected_row = 0
self.selected_col = 0
self.cell_bg_color = self.grid.GetDefaultCellBackgroundColour()
self.grid.SetDefaultCellFont(main_font)
width, h = self.GetTextExtent("987-24 000")
self.grid.SetDefaultColSize(width, True)
self.grid.EnableEditing(False)
self.grid.SetColLabelValue(0, "Band")
self.grid.SetColLabelValue(1, "VFO")
self.grid.SetColLabelValue(2, "Tune")
self.grid.SetColSize(0, width // 2)
self.grid.SetColSize(1, width * 14 // 10)
num_tune = (self.client_width - width * 2) // width
#print ("num_tune", num_tune)
for col in range(3, num_tune + 2):
self.grid.AppendCols()
self.grid.SetColLabelValue(col, "Tune")
#self.grid.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.OnGridLeftClick)
self.grid.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.OnGridSelectCell)
gbox = wx.BoxSizer(wx.VERTICAL)
sizer.Add(gbox, proportion=1, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=charx)
gbox.Add(self.grid, proportion=1)
# Create the New Cell text, Manual/Measure, AddCell button
sbs = wx.StaticBoxSizer(wx.VERTICAL, panel, " New Cell")
sizer.Add(sbs, flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT|wx.BOTTOM, border=charx*3)
self.add_cell_text = wx.StaticText(panel, -1, "Band")
self.add_cell_text.SetFont(main_font)
sbs.Add(self.add_cell_text, flag=wx.LEFT|wx.TOP, border=charx)
box0 = wx.BoxSizer(wx.HORIZONTAL)
sbs.Add(1, charx)
sbs.Add(box0, flag=wx.EXPAND)
rb_manual = wx.RadioButton(panel, -1, "Manual adjustment", style=wx.RB_GROUP)
self.Bind(wx.EVT_RADIOBUTTON, self.OnBtnManMeasure)
rb_measure = wx.RadioButton(panel, -1, "Measure")
if self.is_tx:
rb_manual.Enable(False)
rb_measure.Enable(False)
b = wx.lib.buttons.GenButton(panel, label=" Add cell ")
b.Bind(wx.EVT_BUTTON, self.OnBtnAddCell)
b.SetBezelWidth(3)
b.SetBackgroundColour("#DDD")
box0.Add(rb_manual, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx)
box0.Add(rb_measure, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*5)
box0.Add(b, flag=wx.LEFT, border=charx*5)
# Create the amplitude slider controls
box1 = wx.BoxSizer(wx.HORIZONTAL)
box2 = wx.BoxSizer(wx.HORIZONTAL)
sbs.Add(box1, flag=wx.EXPAND)
sbs.Add(box2, flag=wx.EXPAND)
# ST_ELLIPSIZE_END needed to work around a layout bug
fine = wx.StaticText(panel, -1, 'Gain Fine', style=wx.ST_ELLIPSIZE_END)
coarse = wx.StaticText(panel, -1, 'Gain Coarse', style=wx.ST_ELLIPSIZE_END)
box1.Add(fine, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*3)
box2.Add(coarse, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*3)
self.ampl1 = wx.Slider(panel, -1, 0, -sl_max, sl_max)
self.ampl2 = wx.Slider(panel, -1, 0, -sl_max, sl_max)
box1.Add(self.ampl1, flag=wx.LEFT, proportion=1, border=0)
box2.Add(self.ampl2, flag=wx.LEFT, proportion=1, border=0)
self.ampl1.Bind(wx.EVT_SCROLL, self.OnAmpl1)
self.ampl2.Bind(wx.EVT_SCROLL, self.OnAmpl2)
# Create the phase slider controls
box3 = wx.BoxSizer(wx.HORIZONTAL)
box4 = wx.BoxSizer(wx.HORIZONTAL)
sbs.Add(box3, flag=wx.EXPAND)
sbs.Add(box4, flag=wx.EXPAND)
fine = wx.StaticText(panel, -1, 'Phase Fine', style=wx.ST_ELLIPSIZE_END)
coarse = wx.StaticText(panel, -1, 'Phase Coarse', style=wx.ST_ELLIPSIZE_END)
box3.Add(fine, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*3)
box4.Add(coarse, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*3)
self.phas1 = wx.Slider(panel, -1, 0, -sl_max, sl_max)
self.phas2 = wx.Slider(panel, -1, 0, -sl_max, sl_max)
box3.Add(self.phas1, flag=wx.LEFT, proportion=1, border=0)
box4.Add(self.phas2, flag=wx.LEFT, proportion=1, border=0)
self.phas1.Bind(wx.EVT_SCROLL, self.OnPhase1)
self.phas2.Bind(wx.EVT_SCROLL, self.OnPhase2)
# Create the button row
btnbox = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(btnbox, flag=wx.EXPAND)
btns = []
btnbox.Add(1, 1, proportion=10)
b = wx.lib.buttons.GenButton(panel, label=" Save ")
btnbox.Add(b)
btnbox.Add(1, 1, proportion=5)
btns.append(b)
size = b.GetSize()
b.Bind(wx.EVT_BUTTON, self.OnBtnSave)
b = wx.lib.buttons.GenButton(panel, label=" Exit ")
btnbox.Add(b)
btnbox.Add(1, 1, proportion=5)
btns.append(b)
b.Bind(wx.EVT_BUTTON, self.OnBtnExit)
b = wx.lib.buttons.GenButton(panel, label=" Help ")
btnbox.Add(b)
btnbox.Add(1, 1, proportion=10)
btns.append(b)
b.Bind(wx.EVT_BUTTON, self.OnBtnHelp)
#b.Bind(wx.EVT_BUTTON, self.OnGraphData)
for b in btns:
b.SetBezelWidth(3)
b.SetBackgroundColour("#DDD")
b.SetSize(size)
sizer.Add(charx, chary)
def Redraw(self):
self.grid.ClearGrid()
# Print available data cells
row = 0
bands = []