Quisk version 4.2.28
This commit is contained in:
Binary file not shown.
@@ -0,0 +1 @@
|
||||
#
|
||||
@@ -0,0 +1,408 @@
|
||||
# THIS IS THE ENTIRE "RADIO" FOR QUISK RUNNING AS A CONTROL HEAD
|
||||
# No real radio hardware is attached to the control_head computer.
|
||||
#
|
||||
# This software is Copyright (C) 2021-2022 by Ben Cahill and 2006-2022 by James C. Ahlstrom.,
|
||||
# and is licensed for use under the GNU General Public License (GPL).
|
||||
# See http://www.opensource.org.
|
||||
# Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!!
|
||||
#
|
||||
# This file, control_common.py, along with a radio-specific file, e.g.
|
||||
# control_softrock.py, control_hermes.py, or similar, allows a radio-less (control_head) Quisk,
|
||||
# running on this computer, to connect to a remote (remote_radio) instance of Quisk
|
||||
# that runs on a separate computer.
|
||||
#
|
||||
# The remote_radio Quisk controls an attached real radio, and uses the hardware file
|
||||
# remote_common.py, along with a radio-specific file, e.g. remote_softrock.py,
|
||||
# remote_hermes.py, or similar, to communicate with this control_head Quisk
|
||||
# computer via a network connection.
|
||||
#
|
||||
# The remote_radio computer should be set up with a static IP address, so that you know
|
||||
# where to point the control_head. The control head computer may, however, use dynamic
|
||||
# addressing; the remote radio computer will read the control head address when the
|
||||
# remote control connection is made.
|
||||
#
|
||||
# The main control interface between control_head and remote_radio is via a TCP port;
|
||||
# this uses very low bandwidth. All functional control, including CW keying, is done via this port.
|
||||
#
|
||||
# There are 2 additional ports, both UDP, using low to moderate bandwidth:
|
||||
# -- Receive graph/waterfall data from the remote_radio
|
||||
# -- Receive radio sound from the remote_radio and send mic samples
|
||||
# These use sequential port numbers based on the TCP port number self.remote_ctl_base_port.
|
||||
# If you need to change the default base port number, you can edit the line in this file
|
||||
# that looks like (without the #):
|
||||
#
|
||||
# self.remote_ctl_base_port = 4585 # Remote Control base TCP port
|
||||
# self.graph_data_port = self.remote_ctl_base_port + 1 # UDP port for graph data
|
||||
# self.remote_radio_sound_port = self.remote_ctl_base_port + 2 # UDP port for radio sound and mic
|
||||
#
|
||||
# Make sure to edit the corresponding line in remote_common.py to match ports!!
|
||||
#
|
||||
# You should be able to use the Quisk control_head along with any/all means of control that
|
||||
# you normally use to control Quisk, including serial ports, MIDI, and hamlib/rigctl interfaces.
|
||||
#
|
||||
# The remote_radio Quisk/computer is assumed to track the local control_head Quisk/computer;
|
||||
# no attempt is made to verify the remote_radio Quisk's tuning frequency, mode, etc.
|
||||
# Snap-to Rx tuning for CW works on the control_head Quisk by virtue of graph/waterfall data
|
||||
# received from the remote_radio Quisk.
|
||||
#
|
||||
# To test CW key timing, set DEBUG_CW_SEND_DITS = 1. This issues "perfect" bursts of dits,
|
||||
# configurable in terms of dit length, dit space, number of dits per burst, and pause between
|
||||
# bursts (phrases). Search in this file for "DEBUG_CW_SEND_DITS" to find configurable variables.
|
||||
# To get log output from remote_radio end, in quisk_hardware_remote_radio.py, set DEBUG_CW_JITTER = 1.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
|
||||
DEBUG_CW_JITTER = 0
|
||||
DEBUG_CW_SEND_DITS = 0
|
||||
DEBUG = 0
|
||||
|
||||
import socket, time, traceback, hmac, threading, select
|
||||
import _quisk as QS # Access Quisk C functions via PyMethodDef QuiskMethods[] in quisk.c
|
||||
import wx
|
||||
|
||||
from quisk_hardware_model import Hardware as BaseHardware
|
||||
|
||||
class ControlCommon(BaseHardware): # This is the Hardware class for the control head
|
||||
def __init__(self, app, conf):
|
||||
BaseHardware.__init__(self, app, conf)
|
||||
self.app = app # Access Quisk class App (Python) functions
|
||||
app.remote_control_head = True
|
||||
self.remote_ctl_base_port = 4585 # Base of ports for remote connection (maybe edit this)
|
||||
self.remote_ctl_socket = None
|
||||
self.remote_ctl_connected = False
|
||||
self.remote_ctl_timestamp = None
|
||||
self.graph_data_port = self.remote_ctl_base_port + 1
|
||||
self.remote_radio_sound_port = self.remote_ctl_base_port + 2
|
||||
self.thread_lock = threading.Lock()
|
||||
self.remote_radio_ip = socket.gethostbyname(self.conf.remote_radio_ip) # Allow either host name or IP address
|
||||
self.first_heartbeat = True
|
||||
|
||||
self.cw_keydown = 0
|
||||
self.cw_phrase_begin_ts = None # timestamp of beginning of cw phrase
|
||||
self.cw_phrase_end_ts = None
|
||||
self.cw_phrase_break_duration_secs = 1.0 # cw timestamps will reset to 0
|
||||
self.cw_poll_started_ts = None
|
||||
self.cw_poll_started = False
|
||||
|
||||
if DEBUG_CW_SEND_DITS:
|
||||
self.dit_width = 100 # msec (configurable)
|
||||
self.space_width = 100 # msec (configurable)
|
||||
self.phrase_gap = 1000 # msec (configurable)
|
||||
self.num_dits_in_phrase = 5 # number (configurable)
|
||||
self.num_dits_cur_count = 0
|
||||
self.key_was_down = False
|
||||
self.send_cw_dits = False
|
||||
self.cw_test_next_ts = None
|
||||
self.cw_test_next_msec = None
|
||||
self.cw_phrase_start_ts = None
|
||||
|
||||
self.smeter_text = ''
|
||||
self.received = ''
|
||||
self.closing = False
|
||||
QS.set_sparams(remote_control_head=1, remote_control_slave=0)
|
||||
|
||||
def open(self):
|
||||
ret = BaseHardware.open(self)
|
||||
self.remote_ctl_timestamp = time.time()
|
||||
passw = self.app.local_conf.globals.get("remote_radio_password", "")
|
||||
passw = passw.strip()
|
||||
if passw:
|
||||
del passw
|
||||
return "Not yet connected to " + self.conf.remote_radio_ip
|
||||
else:
|
||||
return "Not yet connected to %s -- Missing Password Here" % self.conf.remote_radio_ip
|
||||
|
||||
def close(self):
|
||||
print('Closing Remote Control connection')
|
||||
self.closing = True
|
||||
t = f'QUIT\n' # Tell Remote Radio we are quitting
|
||||
self.RemoteCtlSend(t)
|
||||
self.RemoteCtlClose()
|
||||
return BaseHardware.close(self)
|
||||
|
||||
def RemoteCtlClose(self):
|
||||
if self.remote_ctl_socket:
|
||||
self.remote_ctl_socket.close()
|
||||
else:
|
||||
print(' Remote Control TCP socket already closed')
|
||||
self.remote_ctl_socket = None
|
||||
self.remote_ctl_connected = False
|
||||
QS.stop_control_head_remote_sound()
|
||||
self.app.main_frame.SetConfigText("Disconnected from remote radio " + self.conf.remote_radio_ip)
|
||||
self.first_heartbeat = True
|
||||
|
||||
def RemoteCtlConnect(self):
|
||||
if self.remote_ctl_connected:
|
||||
return True
|
||||
if not self.remote_ctl_socket:
|
||||
self.remote_ctl_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.remote_ctl_socket.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, 184) # DSCP "Expedite" (46)
|
||||
if DEBUG: print("Default timeout for remote_ctl_socket = ", self.remote_ctl_socket.gettimeout())
|
||||
self.remote_ctl_socket.settimeout(0.1) # Allow some time for connection response
|
||||
if DEBUG: print("Our set timeout for remote_ctl_socket = ", self.remote_ctl_socket.gettimeout())
|
||||
try:
|
||||
self.remote_ctl_socket.connect((self.remote_radio_ip, self.remote_ctl_base_port))
|
||||
except OSError as err:
|
||||
if str(err).startswith('[WinError 10056]'):
|
||||
# Connected, in spite of "error" --
|
||||
# This can occur in Windows; Windows networking infrastructure continues attempting to connect,
|
||||
# even after the connect() call times out (as set in settimeout(), above).
|
||||
# If Windows connects after the timeout, then, the next time we call connect(),
|
||||
# it returns WinError 10056, "A connect request was made on an already connected socket".
|
||||
# Let's accept this "error" as success!
|
||||
if DEBUG: print('connect() returned WinError 10056; Windows already connected! Good!')
|
||||
pass
|
||||
else:
|
||||
# Not yet connected; errors may be expected/normal, or unexpected.
|
||||
if str(err).startswith('time'):
|
||||
# Timeout "error" is normal when we are waiting for Remote Radio server to become available.
|
||||
if DEBUG: print('connect() returned timeout; still waiting for remote radio server')
|
||||
elif str(err).startswith('[WinError 10022]'):
|
||||
# This can occur in Windows; Windows networking infrastructure continues attempting to connect,
|
||||
# even after the connect() call times out (as set in settimeout(), above).
|
||||
# If, by the next time we call connect(), it has not yet connected, but is still trying to do so
|
||||
# as a continuation of the prior call to connect(), it returns WinError 10022,
|
||||
# "An invalid argument was supplied", which is a little misleading, but okay for us.
|
||||
# We have not yet connected, but this is an "expected behavior".
|
||||
if DEBUG: print('connect() returned WinError 10022; invalid argument; still waiting for remote radio server')
|
||||
elif str(err).startswith('[Errno 103]'):
|
||||
# This can occur in Linux; still attempting to connect
|
||||
if DEBUG: print('connect() returned Errno 103; software connection abort; still waiting for remote radio server')
|
||||
elif str(err).startswith('[Errno 111]'):
|
||||
# This can occur in Linux; still attempting to connect
|
||||
if DEBUG: print('connect() returned Errno 111; connection refused; still waiting for remote radio server')
|
||||
else:
|
||||
# Unexpected error. Print error info, regardless of DEBUG status.
|
||||
print("Remote Control socket.connect() error: {0}".format(err))
|
||||
return False # Failure to connect
|
||||
self.remote_ctl_connected = True
|
||||
self.remote_ctl_socket.settimeout(0.0) # Now that we're connected, don't wait if nothing there
|
||||
if DEBUG: print("Remote Control connected")
|
||||
self.app.main_frame.SetConfigText("Connecting to remote radio " + self.conf.remote_radio_ip)
|
||||
# We have a TCP connection, and the remote will send a challenge token. If we give a valid response
|
||||
# we will receive "TOKEN_OK" and we can start communication.
|
||||
return True # Success
|
||||
|
||||
def ChangeFrequency(self, tune, vfo, source='', band='', event=None):
|
||||
# TODO: Try to get any modifications of freq or vfo from remote Quisk (?)
|
||||
t = f'FREQ;{tune};{vfo};{source};{band};{self.app.rxFreq};{self.VarDecimGetIndex()}\n'
|
||||
#print (t)
|
||||
self.RemoteCtlSend(t)
|
||||
#BMC if DEBUG: print('Change', source, tune, vfo, band)
|
||||
return tune, vfo
|
||||
|
||||
def OnSpot(self, level):
|
||||
pass
|
||||
|
||||
def SendCwDits(self):
|
||||
ts = time.time()
|
||||
# Use CW key to start/stop stream of dit phrases
|
||||
key_down = QS.is_cwkey_down()
|
||||
if not key_down:
|
||||
self.key_was_down = False
|
||||
else:
|
||||
if not self.key_was_down: # Leading edge detector for key down
|
||||
self.key_was_down = True
|
||||
if not self.send_cw_dits:
|
||||
self.send_cw_dits = True # Start sending dits
|
||||
self.cw_phrase_start_ts = ts # Beginning of phrase
|
||||
self.cw_test_next_ts = ts
|
||||
self.cw_test_next_msec = 0 # Start phrase at 0 msec
|
||||
self.num_dits_cur_count = 0
|
||||
else:
|
||||
self.send_cw_dits = False # Stop sending dits
|
||||
if self.send_cw_dits and ts >= self.cw_test_next_ts:
|
||||
# Send one on/off pair of CW commands (with msec timestamps since start of CW phrase)
|
||||
t = f'CW;1;{self.cw_test_next_msec}\n'
|
||||
self.RemoteCtlSend(t)
|
||||
self.cw_test_next_msec += self.dit_width
|
||||
t = f'CW;0;{self.cw_test_next_msec}\n'
|
||||
self.RemoteCtlSend(t)
|
||||
self.cw_test_next_msec += self.space_width
|
||||
self.cw_test_next_ts = self.cw_phrase_start_ts + (float(self.cw_test_next_msec) / 1000)
|
||||
self.num_dits_cur_count += 1
|
||||
if self.num_dits_cur_count >= self.num_dits_in_phrase:
|
||||
# Set up for next phrase
|
||||
self.cw_test_next_ts = ts + (float(self.phrase_gap) / 1000)
|
||||
self.cw_phrase_start_ts = self.cw_test_next_ts # Re-start beginning of phrase
|
||||
self.cw_test_next_msec = 0 # Start phrase at 0 msec
|
||||
self.num_dits_cur_count = 0
|
||||
|
||||
def ThreadPrinter(self, *args, **kw):
|
||||
# Call this to print from (possibly) the sound thread, which must not be slowed down by a print.
|
||||
# example: print (x, y, end=' ') ----> self.ThreadPrinter(x, y, end=' ')
|
||||
with self.thread_lock: # Call thread_lock only once; twice will deadlock.
|
||||
# If print request is from within sound thread, pass the print request to the GUI thread.
|
||||
if threading.current_thread().name == "QuiskSound":
|
||||
wx.CallAfter(print, *args, **kw)
|
||||
else:
|
||||
print(*args, **kw)
|
||||
|
||||
def PollCwKey(self): # Called by the sound thread
|
||||
if DEBUG_CW_SEND_DITS:
|
||||
self.SendCwDits()
|
||||
return
|
||||
# Check Quisk key state, send to Remote Radio if change.
|
||||
# NOTE: Timestamps enable Remote Radio to overcome WiFi/network jitter
|
||||
ts = time.time()
|
||||
if not self.cw_phrase_end_ts:
|
||||
self.cw_phrase_end_ts = ts
|
||||
self.cw_poll_started_ts = ts
|
||||
|
||||
key_down = QS.is_cwkey_down()
|
||||
if not self.cw_poll_started:
|
||||
# Detect Quisk startup with CW key down
|
||||
if ts - self.cw_poll_started_ts > 0.1:
|
||||
# Check for key down only within first 1/10 second of running
|
||||
self.cw_poll_started = True
|
||||
elif key_down == 1:
|
||||
# Quisk startup with key down
|
||||
t = f'Quisk is starting with CW key down! Tx is on, and Rx is blocked until you release CW key.'
|
||||
self.ThreadPrinter(t)
|
||||
#dlg = wx.MessageDialog(self.app.main_frame, t, "Quisk start, CW key down", style=wx.OK)
|
||||
#wx.CallAfter(dlg.ShowModal)
|
||||
self.cw_phrase_begin_ts = ts
|
||||
self.cw_poll_started = True
|
||||
if key_down != self.cw_keydown:
|
||||
if key_down == 1 and (ts - self.cw_phrase_end_ts) > self.cw_phrase_break_duration_secs:
|
||||
# First CW key-down since a while ago, re-start timestamp sequence for new CW phrase
|
||||
self.cw_phrase_begin_ts = ts
|
||||
cw_event_ts_msecs = int((ts - self.cw_phrase_begin_ts) * 1000) # float secs to int msecs
|
||||
self.cw_keydown = key_down
|
||||
t = f'CW;{key_down};{cw_event_ts_msecs}\n'
|
||||
self.RemoteCtlSend(t)
|
||||
self.cw_phrase_end_ts = ts # End-of-cw-phrase-detection
|
||||
if DEBUG_CW_JITTER: self.ThreadPrinter(f'{ts:10.4f} {key_down}, {cw_event_ts_msecs}')
|
||||
|
||||
def HeartBeat(self): # Called at about 10 Hz by the main
|
||||
if self.closing: # Don't try to connect if we are closing
|
||||
return
|
||||
ts = time.time()
|
||||
if (ts - self.remote_ctl_timestamp) > 1.0 or self.first_heartbeat:
|
||||
self.remote_ctl_timestamp = ts
|
||||
if self.remote_ctl_connected:
|
||||
# Send keep-alive heartbeat command
|
||||
t = f'HEARTBEAT\n'
|
||||
self.RemoteCtlSend(t)
|
||||
else:
|
||||
# Else continually try to connect
|
||||
if DEBUG: print('Heartbeat Connect Attempt')
|
||||
self.RemoteCtlConnect()
|
||||
self.first_heartbeat = False
|
||||
self.RemoteCtlRead()
|
||||
|
||||
def RemoteCtlSend(self, text):
|
||||
# RemoteCtlSend() may be called from sound thread or GUI thread!
|
||||
# self.thread lock (also used in self.ThreadPrinter) protects against thread collisions.
|
||||
if not self.remote_ctl_connected:
|
||||
if DEBUG: self.ThreadPrinter('Cannot send if not TCP connected:', text)
|
||||
return
|
||||
if DEBUG: self.ThreadPrinter('Send: ', text, end=' ')
|
||||
with self.thread_lock: # Do not call ThreadPrinter() from another thread lock!
|
||||
try:
|
||||
self.remote_ctl_socket.sendall(text.encode('utf-8', errors='ignore'))
|
||||
except OSError as err:
|
||||
errtxt = err
|
||||
pass
|
||||
else:
|
||||
return
|
||||
self.ThreadPrinter("Closing remote control socket; error in RemoteCtlSend(): {0}".format(errtxt))
|
||||
self.RemoteCtlClose()
|
||||
|
||||
def GetSmeter(self):
|
||||
return self.smeter_text
|
||||
|
||||
def RemoteCtlRead(self):
|
||||
if not self.remote_ctl_connected:
|
||||
return
|
||||
try: # Read any data from the socket
|
||||
text = self.remote_ctl_socket.recv(1024).decode('utf-8', errors='replace')
|
||||
except socket.timeout: # This does not work
|
||||
pass
|
||||
except socket.error: # Nothing to read
|
||||
pass
|
||||
else: # We got some characters
|
||||
self.received += text
|
||||
while '\n' in self.received: # A complete response ending with newline is available
|
||||
reply, self.received = self.received.split('\n', 1) # Split off the reply, save any further characters
|
||||
reply = reply.strip() # Here is our reply
|
||||
if DEBUG: print('Rcvd: ', reply)
|
||||
if reply[0] in 'Qq':
|
||||
print('Closing Remote Control socket: Q (Quit) from remote radio')
|
||||
self.RemoteCtlClose()
|
||||
return
|
||||
elif reply[0] in 'Mm':
|
||||
# S-meter text from remote_radio
|
||||
self.smeter_text = reply[2:]
|
||||
#print ("Receive smeter", reply[2:])
|
||||
elif reply[0:6] == "TOKEN;":
|
||||
passw = self.app.local_conf.globals.get("remote_radio_password", "")
|
||||
passw = passw.strip()
|
||||
if passw:
|
||||
passw = passw.encode('utf-8')
|
||||
H = hmac.new(passw, reply[6:].encode('utf-8'), 'sha3_256')
|
||||
del passw
|
||||
self.RemoteCtlSend("TOKEN;%s;%d\n" % (H.hexdigest(), self.app.data_width))
|
||||
else:
|
||||
print ("Error: Missing password on control head")
|
||||
elif reply[0:8] == "TOKEN_OK":
|
||||
self.app.main_frame.SetConfigText("Connected to remote radio " + self.conf.remote_radio_ip)
|
||||
QS.start_control_head_remote_sound(self.remote_radio_ip, self.remote_radio_sound_port, self.graph_data_port)
|
||||
self.CommonInit() # Send initial parameters common to all radios
|
||||
self.RadioInit() # Send initial parameters peculiar to a given radio
|
||||
elif reply[0:9] == "TOKEN_BAD":
|
||||
self.app.main_frame.SetConfigText("Error: Remote radio %s: Security challenge failed" % self.conf.remote_radio_ip)
|
||||
elif reply[0:13] == "TOKEN_MISSING":
|
||||
self.app.main_frame.SetConfigText("Error: Remote radio %s has no password" % self.conf.remote_radio_ip)
|
||||
elif reply[0:9] == "HL2_TEMP;":
|
||||
setattr(self, "HL2_TEMP", reply[9:])
|
||||
elif reply[:3] == 'ERR':
|
||||
print('Remote Radio returned ' + reply)
|
||||
else:
|
||||
print ("Control head received unrecognized command", reply)
|
||||
|
||||
def CommonInit(self): # Send initial frequencies, band, sample rate, etc. to remote
|
||||
app = self.app
|
||||
# Frequency and decimation
|
||||
self.ChangeFrequency(app.txFreq + app.VFO, app.VFO, "NewDecim")
|
||||
# Band
|
||||
self.RemoteCtlSend("%s;1\n" % app.lastBand)
|
||||
# Mode
|
||||
btn = app.modeButns.GetSelectedButton()
|
||||
if btn:
|
||||
self.RemoteCtlSend("%s;%d\n" % (btn.idName, btn.GetIndex()))
|
||||
# Filter and adjustable bandwidth
|
||||
name = "Filter 6Slider"
|
||||
value = app.midiControls[name][0].button.slider_value
|
||||
self.RemoteCtlSend("%s;%d\n" % (name, value))
|
||||
btn = app.filterButns.GetSelectedButton()
|
||||
if btn:
|
||||
self.RemoteCtlSend("%s;%d\n" % (btn.idName, btn.GetIndex()))
|
||||
# AGC and Squelch levels, split offset
|
||||
self.RemoteCtlSend("Split;0\n")
|
||||
btn = app.BtnAGC
|
||||
self.RemoteCtlSend("AGCSQLCH;%d;%d;%d;%d;%d\n" % (btn.slider_value_off, btn.slider_value_on,
|
||||
app.levelSquelch, app.levelSquelchSSB, app.split_offset))
|
||||
idName = "SqlchSlider"
|
||||
value = app.midiControls[idName][0].button.slider_value
|
||||
self.RemoteCtlSend("%s;%d\n" % (idName, value))
|
||||
# Spot slider
|
||||
idName = "SpotSlider"
|
||||
value = app.midiControls[idName][0].button.slider_value
|
||||
self.RemoteCtlSend("%s;%d\n" % (idName, value))
|
||||
# Various buttons
|
||||
for idName in ("Mute", "NR2", "AGC", "Sqlch", "NB 1", "Notch", "Test 1", "Spot", "FDX", "PTT", "VOX"):
|
||||
self.RemoteCtlSend("%s;%d\n" % (idName, app.idName2Button[idName].GetIndex()))
|
||||
# Menus
|
||||
for menu in (app.NB_menu, app.split_menu, app.freedv_menu, app.smeter_menu):
|
||||
if menu:
|
||||
for nid in menu.id2data:
|
||||
menu_item = menu.FindItemById(nid)
|
||||
kind = menu_item.GetKind()
|
||||
if kind == wx.ITEM_RADIO:
|
||||
if menu_item.IsChecked():
|
||||
self.RemoteCtlSend('MENU;%s;%s;1\n' % (menu.menu_name, menu_item.GetItemLabelText()))
|
||||
elif kind == wx.ITEM_CHECK:
|
||||
checked = menu_item.IsChecked()
|
||||
self.RemoteCtlSend('MENU;%s;%s;%d\n' % (menu.menu_name, menu_item.GetItemLabelText(), int(checked)))
|
||||
@@ -0,0 +1,54 @@
|
||||
# This provides access to a remote radio. See ac2yd/remote_common.py and .pdf files for documentation.
|
||||
|
||||
from ac2yd.control_common import ControlCommon
|
||||
|
||||
class Hardware(ControlCommon):
|
||||
def __init__(self, app, conf):
|
||||
ControlCommon.__init__(self, app, conf)
|
||||
self.hermes_code_version = 40
|
||||
self.HL2_TEMP = ";;;"
|
||||
self.var_rates = ['48', '96', '192', '384']
|
||||
self.var_index = 0
|
||||
#app.bandscope_clock = conf.rx_udp_clock
|
||||
def ChangeLNA(self, value):
|
||||
pass
|
||||
def ChangeAGC(self, value):
|
||||
pass
|
||||
def HeartBeat(self):
|
||||
ControlCommon.HeartBeat(self)
|
||||
args = self.HL2_TEMP.split(';')
|
||||
widg = self.app.bottom_widgets
|
||||
if widg:
|
||||
widg.text_temperature.SetLabel(args[0])
|
||||
widg.text_pa_current.SetLabel(args[1])
|
||||
widg.text_fwd_power.SetLabel(args[2])
|
||||
widg.text_swr.SetLabel(args[3])
|
||||
def RadioInit(self): # Send initial parameters not covered by CommonInit()
|
||||
idName = "RfLna"
|
||||
value = self.app.midiControls[idName][0].GetValue()
|
||||
self.RemoteCtlSend("%s;%d\n" % (idName, value))
|
||||
def VarDecimGetChoices(self): # return text labels for the control
|
||||
return self.var_rates
|
||||
def VarDecimGetLabel(self): # return a text label for the control
|
||||
return "Sample rate ksps"
|
||||
def VarDecimGetIndex(self): # return the current index
|
||||
return self.var_index
|
||||
def VarDecimSet(self, index=None): # set decimation, return sample rate
|
||||
if index is None: # initial call to set rate before the call to open()
|
||||
rate = self.app.vardecim_set # May be None or from different hardware
|
||||
else:
|
||||
rate = int(self.var_rates[index]) * 1000
|
||||
if rate == 48000:
|
||||
self.var_index = 0
|
||||
elif rate == 96000:
|
||||
self.var_index = 1
|
||||
elif rate == 192000:
|
||||
self.var_index = 2
|
||||
elif rate == 384000:
|
||||
self.var_index = 3
|
||||
else:
|
||||
self.var_index = 0
|
||||
rate = 48000
|
||||
return rate
|
||||
def VarDecimRange(self):
|
||||
return (48000, 384000)
|
||||
@@ -0,0 +1,40 @@
|
||||
# This provides access to a remote radio. See ac3yd/remote_common.py and .pdf files for documentation.
|
||||
|
||||
from ac2yd.control_common import ControlCommon
|
||||
|
||||
class Hardware(ControlCommon):
|
||||
def __init__(self, app, conf):
|
||||
ControlCommon.__init__(self, app, conf)
|
||||
self.index = 0
|
||||
self.rx_udp_clock = 122880000
|
||||
self.decimations = [] # supported decimation rates
|
||||
for dec in (40, 20, 10, 8, 5, 4, 2):
|
||||
self.decimations.append(dec * 64)
|
||||
self.decimations.append(80)
|
||||
self.decimations.append(64)
|
||||
def RadioInit(self): # Send initial parameters not covered by CommonInit()
|
||||
pass
|
||||
def VarDecimGetChoices(self): # return text labels for the control
|
||||
clock = self.rx_udp_clock
|
||||
l = [] # a list of sample rates
|
||||
for dec in self.decimations:
|
||||
l.append(str(int(float(clock) / dec / 1e3 + 0.5)))
|
||||
return l
|
||||
def VarDecimGetLabel(self): # return a text label for the control
|
||||
return "Sample rate ksps"
|
||||
def VarDecimGetIndex(self): # return the current index
|
||||
return self.index
|
||||
def VarDecimSet(self, index=None): # set decimation, return sample rate
|
||||
if index is None: # initial call to set decimation before the call to open()
|
||||
rate = self.application.vardecim_set # May be None or from different hardware
|
||||
try:
|
||||
dec = int(float(self.rx_udp_clock // rate + 0.5))
|
||||
self.index = self.decimations.index(dec)
|
||||
except:
|
||||
self.index = 0
|
||||
else:
|
||||
self.index = index
|
||||
dec = self.decimations[self.index]
|
||||
return int(float(self.rx_udp_clock) / dec + 0.5)
|
||||
def VarDecimRange(self):
|
||||
return (48000, 960000)
|
||||
@@ -0,0 +1,9 @@
|
||||
# This provides access to a remote radio. See ac2yd/remote_common.py and .pdf files for documentation.
|
||||
|
||||
from ac2yd.control_common import ControlCommon
|
||||
|
||||
class Hardware(ControlCommon):
|
||||
def __init__(self, app, conf):
|
||||
ControlCommon.__init__(self, app, conf)
|
||||
def RadioInit(self): # Send initial parameters not covered by CommonInit()
|
||||
pass
|
||||
+748
@@ -0,0 +1,748 @@
|
||||
/*
|
||||
This software is Copyright (C) 2021-2022 by Ben Cahill and 2006-2022 by James C. Ahlstrom,
|
||||
and is licensed for use under the GNU General Public License (GPL).
|
||||
See http://www.opensource.org.
|
||||
Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!!
|
||||
*/
|
||||
|
||||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
#include <complex.h>
|
||||
#include <math.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
|
||||
#ifdef MS_WINDOWS
|
||||
#include <winsock2.h>
|
||||
#include <stdint.h>
|
||||
#else
|
||||
|
||||
#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))
|
||||
#include <sys/param.h>
|
||||
#if defined(BSD)
|
||||
#include <netinet/in.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/ip.h>
|
||||
#include <fcntl.h>
|
||||
#endif
|
||||
|
||||
#include "../quisk.h"
|
||||
#include "../filter.h"
|
||||
|
||||
#define GRAPH_DATA_SCALE 163
|
||||
#define MAX_UDP_INT16_T 600
|
||||
#define UDP_SEND_INT16 200
|
||||
|
||||
#define REMOTE_DEBUG 0 //BMC TODO: Make this a configuration option
|
||||
|
||||
static SOCKET remote_radio_sound_socket = INVALID_SOCKET; // send radio sound to control_head, receive mic samples
|
||||
static SOCKET control_head_sound_socket = INVALID_SOCKET; // receive radio sound from remote_radio, send mic samples
|
||||
static SOCKET remote_radio_graph_socket = INVALID_SOCKET; // send graph data to control_head
|
||||
static SOCKET control_head_graph_socket = INVALID_SOCKET; // receive graph data from remote_radio
|
||||
static int control_head_sound_socket_started = 0; // sound stream started on the control head
|
||||
static int remote_radio_sound_socket_started = 0; // sound stream started on the remote radio
|
||||
static int control_head_graph_socket_started = 0; // graph data stream started on the control head
|
||||
static int remote_radio_graph_socket_started = 0; // graph data stream started on the remote radio
|
||||
static int control_head_data_width; // app.data_width of control head for graph data
|
||||
static int packets_sent;
|
||||
static int packets_recd;
|
||||
|
||||
// Receive stereo 16-bit pcm radio speaker sound on the control head via UDP
|
||||
int read_remote_radio_sound_socket(complex double * cSamples)
|
||||
{
|
||||
int i, bytes, nInt16, nSamples;
|
||||
int16_t buf[UDP_SEND_INT16];
|
||||
double samp_r, samp_l;
|
||||
struct timeval tm_wait;
|
||||
fd_set fds;
|
||||
static struct quisk_cHB45Filter HalfBand;
|
||||
static struct quisk_cFilter cFiltInterp3;
|
||||
static int init_filters=1;
|
||||
|
||||
if (control_head_sound_socket == INVALID_SOCKET)
|
||||
return 0;
|
||||
if (init_filters) {
|
||||
init_filters = 0;
|
||||
memset(&HalfBand, 0, sizeof(struct quisk_cHB45Filter));
|
||||
quisk_filt_cInit(&cFiltInterp3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double));
|
||||
}
|
||||
// Signal far end (server) that we're ready (this sends our address/port to far end)
|
||||
if (!control_head_sound_socket_started) {
|
||||
QuiskPrintf("read_remote_radio_sound_socket() sending 'rr'\n");
|
||||
bytes = send(control_head_sound_socket, "rr\n", 3, 0);
|
||||
if (bytes != 3)
|
||||
QuiskPrintf("read_remote_radio_sound_socket(), sendto(): %s\n", strerror(errno));
|
||||
}
|
||||
|
||||
// read all available packets, one per loop
|
||||
nSamples = 0;
|
||||
while (1) {
|
||||
tm_wait.tv_sec = 0;
|
||||
tm_wait.tv_usec = 0;
|
||||
FD_ZERO (&fds);
|
||||
FD_SET (control_head_sound_socket, &fds);
|
||||
if (select(control_head_sound_socket + 1, &fds, NULL, NULL, &tm_wait) != 1) {
|
||||
//BMC QuiskPrintf("read_remote_radio_sound_socket(): select returned %i\n", retval);
|
||||
break;
|
||||
}
|
||||
bytes = recv(control_head_sound_socket, (char *)buf, UDP_SEND_INT16 * 2, 0);
|
||||
if (bytes < 0) {
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK)
|
||||
QuiskPrintf("read_remote_radio_sound_socket(), recv(): %s\n", strerror(errno));
|
||||
break;
|
||||
}
|
||||
if (bytes > 0) {
|
||||
control_head_sound_socket_started = 1;
|
||||
nInt16 = bytes / 2;
|
||||
for (i = 0; i < nInt16; i += 2) {
|
||||
samp_r = buf[i];
|
||||
samp_l = buf[i + 1];
|
||||
cSamples[nSamples++] = (samp_r + I * samp_l) / CLIP16 * CLIP32;
|
||||
}
|
||||
}
|
||||
} // while(1)
|
||||
|
||||
nSamples = quisk_cInterpolate(cSamples, nSamples, &cFiltInterp3, 3);
|
||||
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand);
|
||||
|
||||
return nSamples;
|
||||
}
|
||||
|
||||
// Receive stereo 16-bit pcm microphone samples at the remote radio via UDP
|
||||
int read_remote_mic_sound_socket(complex double * cSamples)
|
||||
{
|
||||
int i, bytes, nInt16, nSamples;
|
||||
int16_t buf[UDP_SEND_INT16];
|
||||
double samp_r, samp_l;
|
||||
struct timeval tm_wait;
|
||||
fd_set fds;
|
||||
static struct quisk_cHB45Filter HalfBand;
|
||||
static struct quisk_cFilter cFiltInterp3;
|
||||
static int init_filters=1;
|
||||
|
||||
if (remote_radio_sound_socket == INVALID_SOCKET)
|
||||
return 0;
|
||||
if (init_filters) {
|
||||
init_filters = 0;
|
||||
memset(&HalfBand, 0, sizeof(struct quisk_cHB45Filter));
|
||||
quisk_filt_cInit(&cFiltInterp3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double));
|
||||
}
|
||||
|
||||
// read all available packets, one per loop
|
||||
nSamples = 0;
|
||||
while (1) {
|
||||
tm_wait.tv_sec = 0;
|
||||
tm_wait.tv_usec = 0;
|
||||
FD_ZERO (&fds);
|
||||
FD_SET (remote_radio_sound_socket, &fds);
|
||||
if (select(remote_radio_sound_socket + 1, &fds, NULL, NULL, &tm_wait) != 1) {
|
||||
//BMC QuiskPrintf("read_remote_mic_sound_socket(): select returned %i\n", retval);
|
||||
break;
|
||||
}
|
||||
bytes = recv(remote_radio_sound_socket, (char *)buf, UDP_SEND_INT16 * 2, 0);
|
||||
if (bytes < 0) {
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK)
|
||||
QuiskPrintf("read_remote_mic_sound_socket(), recv(): %s\n", strerror(errno));
|
||||
break;
|
||||
}
|
||||
if (bytes > 0) {
|
||||
nInt16 = bytes / 2;
|
||||
for (i = 0; i < nInt16; i += 2) {
|
||||
samp_r = buf[i];
|
||||
samp_l = buf[i + 1];
|
||||
cSamples[nSamples++] = (samp_r + I * samp_l) / CLIP16 * CLIP32;
|
||||
}
|
||||
}
|
||||
} // while(1)
|
||||
|
||||
nSamples = quisk_cInterpolate(cSamples, nSamples, &cFiltInterp3, 3);
|
||||
nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand);
|
||||
|
||||
return nSamples;
|
||||
}
|
||||
// Send stereo 16-bit pcm sound via UDP
|
||||
// This code acts as UDP server for radio sound (on remote radio) or mic sound (on control head)
|
||||
#define MAX_SAMPLES_FOR_REMOTE_SOUND 15000
|
||||
#define RX_BUFFER_SIZE 64
|
||||
|
||||
// Send microphone samples from the control head to the remote radio
|
||||
void send_remote_mic_sound_socket(complex double * cSamples, int nSamples)
|
||||
{
|
||||
int i;
|
||||
ssize_t sent;
|
||||
static int buffer_index=0;
|
||||
static int16_t buffer[UDP_SEND_INT16];
|
||||
static struct quisk_cHB45Filter HalfBand;
|
||||
static struct quisk_cFilter cFiltDecim3;
|
||||
static int init_filters=1, size_cBuf=0;
|
||||
static complex double * cBuf=NULL;
|
||||
|
||||
if (init_filters) {
|
||||
init_filters = 0;
|
||||
memset(&HalfBand, 0, sizeof(struct quisk_cHB45Filter));
|
||||
quisk_filt_cInit(&cFiltDecim3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double));
|
||||
}
|
||||
if (nSamples > size_cBuf) {
|
||||
size_cBuf = nSamples;
|
||||
cBuf = (complex double *)realloc(cBuf, size_cBuf * sizeof(complex double));
|
||||
}
|
||||
if (control_head_sound_socket == INVALID_SOCKET)
|
||||
return;
|
||||
|
||||
if (!control_head_sound_socket_started)
|
||||
return;
|
||||
memcpy(cBuf, cSamples, nSamples * sizeof(complex double)); // Do not alter cSamples
|
||||
// Reduce sample rate from 48 to 8 ksps
|
||||
nSamples = quisk_cDecim2HB45(cBuf, nSamples, &HalfBand);
|
||||
nSamples = quisk_cDecimate(cBuf, nSamples, &cFiltDecim3, 3);
|
||||
// Convert format from complex double to stereo pairs of 16-bit PCM samples.
|
||||
// Buffer samples until UDP_SEND_INT16 are available, and then send the block.
|
||||
for (i = 0; i < nSamples; i++) {
|
||||
buffer[buffer_index++] = (int16_t)(creal(cBuf[i]) * (double)CLIP16 / CLIP32);
|
||||
buffer[buffer_index++] = (int16_t)(cimag(cBuf[i]) * (double)CLIP16 / CLIP32);
|
||||
if (buffer_index >= UDP_SEND_INT16) {
|
||||
sent = send(control_head_sound_socket, (const char *)buffer, buffer_index * 2, 0);
|
||||
if (sent != buffer_index * 2)
|
||||
QuiskPrintf("send_remote_mic_sound_socket(), send(): %s\n", strerror(errno));
|
||||
buffer_index = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send radio speaker sound from the remote radio to the control head
|
||||
void send_remote_radio_sound_socket(complex double * cSamples, int nSamples)
|
||||
{ // Send nSamples samples. Each sample is sent as two shorts (4 bytes) of L/R audio data.
|
||||
int i, N, sent;
|
||||
static int16_t sound_lr[UDP_SEND_INT16];
|
||||
static int udp_size = 0; // Keep track of UDP payload size, in shorts
|
||||
char buf[RX_BUFFER_SIZE]; // For startup message
|
||||
int recv_len;
|
||||
int retval;
|
||||
SOCKET * sock = &remote_radio_sound_socket;
|
||||
static struct quisk_cHB45Filter HalfBand;
|
||||
static struct quisk_cFilter cFiltDecim3;
|
||||
static int init_filters=1, size_cBuf=0;
|
||||
static complex double * cBuf=NULL;
|
||||
|
||||
#if REMOTE_DEBUG > 0 //BMC debug
|
||||
// measure/monitor tools:
|
||||
static float callcount = 0;
|
||||
static float sampcount = 0;
|
||||
static uint64_t bunchcount = 0;
|
||||
static float callcount_total = 0;
|
||||
static float sampcount_total = 0;
|
||||
static double prior_ts = 0;
|
||||
static double delta_total = 0;
|
||||
#if REMOTE_DEBUG > 1 //BMC debug
|
||||
static double prior_packet_ts = 0.0;
|
||||
double now;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
if (*sock == INVALID_SOCKET)
|
||||
return;
|
||||
|
||||
if (init_filters) {
|
||||
init_filters = 0;
|
||||
memset(&HalfBand, 0, sizeof(struct quisk_cHB45Filter));
|
||||
quisk_filt_cInit(&cFiltDecim3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double));
|
||||
}
|
||||
if (nSamples > size_cBuf) {
|
||||
size_cBuf = nSamples;
|
||||
cBuf = (complex double *)realloc(cBuf, size_cBuf * sizeof(complex double));
|
||||
}
|
||||
|
||||
#if REMOTE_DEBUG > 0 //BMC debug
|
||||
callcount++;
|
||||
#endif
|
||||
// Wait for far end (client) to send its opening greetings, so we can grab its network address/port
|
||||
if (!remote_radio_sound_socket_started) {
|
||||
struct sockaddr_in far_addr;
|
||||
#ifdef MS_WINDOWS
|
||||
int addr_len = sizeof(struct sockaddr_in);
|
||||
#else
|
||||
socklen_t addr_len = sizeof(struct sockaddr_in);
|
||||
#endif
|
||||
struct timeval tm_wait;
|
||||
fd_set fds;
|
||||
tm_wait.tv_sec = 0;
|
||||
tm_wait.tv_usec = 0;
|
||||
FD_ZERO (&fds);
|
||||
FD_SET (*sock, &fds);
|
||||
if ((retval = select(*sock + 1, &fds, NULL, NULL, &tm_wait)) != 1) {
|
||||
//BMC QuiskPrintf("send_remote_sound_socket(): select returned %i\n", retval);
|
||||
return;
|
||||
}
|
||||
// Receive short msg, grab far end address
|
||||
if ((recv_len = recvfrom(*sock, buf, RX_BUFFER_SIZE, 0, (struct sockaddr *) &far_addr, &addr_len)) == -1) {
|
||||
QuiskPrintf("send_remote_sound_socket(), recvfrom(): %s\n", strerror(errno));
|
||||
return;
|
||||
}
|
||||
else if(recv_len > 0) {
|
||||
if (recv_len >= RX_BUFFER_SIZE)
|
||||
buf[RX_BUFFER_SIZE - 1] = '\n';
|
||||
else
|
||||
buf[recv_len] = '\n';
|
||||
QuiskPrintf("send_remote_sound_socket(): recv_len = %i, %s", recv_len, buf);
|
||||
if (connect(*sock, (const struct sockaddr *)&far_addr, sizeof(far_addr)) != 0) {
|
||||
QuiskPrintf("send_remote_sound_socket), connect(): %s\n", strerror(errno));
|
||||
close(*sock);
|
||||
*sock = INVALID_SOCKET;
|
||||
}
|
||||
else
|
||||
remote_radio_sound_socket_started = 1;
|
||||
}
|
||||
}
|
||||
|
||||
memcpy(cBuf, cSamples, nSamples * sizeof(complex double)); // Do not alter cSamples
|
||||
// Reduce sample rate from 48 to 8 ksps
|
||||
nSamples = quisk_cDecim2HB45(cBuf, nSamples, &HalfBand);
|
||||
nSamples = quisk_cDecimate(cBuf, nSamples, &cFiltDecim3, 3);
|
||||
// Convert format from complex double to stereo pairs of 16-bit PCM samples, send to client
|
||||
for (i = 0; i < nSamples; i++) {
|
||||
sound_lr[udp_size++] = (int16_t)(creal(cBuf[i]) * (double)CLIP16 / CLIP32);
|
||||
sound_lr[udp_size++] = (int16_t)(cimag(cBuf[i]) * (double)CLIP16 / CLIP32);
|
||||
if (udp_size >= UDP_SEND_INT16) {
|
||||
N = UDP_SEND_INT16;
|
||||
udp_size = 0;
|
||||
sent = send(*sock, (char *)sound_lr, N * 2, 0);
|
||||
if (sent != N * 2)
|
||||
QuiskPrintf("send_remote_sound_socket(), send(): %s\n", strerror(errno));
|
||||
#if REMOTE_DEBUG > 0 //BMC debug
|
||||
else {
|
||||
sampcount += sent/4;
|
||||
packets_sent++;
|
||||
#if REMOTE_DEBUG > 1 //BMC debug
|
||||
now = QuiskTimeSec();
|
||||
QuiskPrintf("%f, send_remote_sound_socket(): now - prior = %f, samples = %u, sampcount = %lu\n",
|
||||
now, now - prior_packet_ts, sent / 4, sampcount);
|
||||
prior_packet_ts = now;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if REMOTE_DEBUG > 1 //BMC debug
|
||||
else {
|
||||
now = QuiskTimeSec();
|
||||
QuiskPrintf("%f, send_remote_sound_socket(): nSamples = %i\n", now, nSamples);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if REMOTE_DEBUG > 0 //BMC debug
|
||||
if (callcount >= 200) {
|
||||
double new_ts = QuiskTimeSec();
|
||||
double delta = new_ts - prior_ts;
|
||||
prior_ts = new_ts;
|
||||
#if REMOTE_DEBUG > 1 //BMC every 200
|
||||
QuiskPrintf("send_remote_sound_socket CURRENT calls: %lu, samples %lu, deltasec %f\n", callcount, sampcount, delta);
|
||||
QuiskPrintf("%f: send_remote_sound_socket CURRENT RATES (HZ): calls %f, samples %f\n", new_ts, callcount / delta, sampcount / delta);
|
||||
#endif
|
||||
if (bunchcount > 0) { // skip the initial bunch; prebuf may distort some numbers(?)
|
||||
callcount_total += callcount;
|
||||
sampcount_total += sampcount;
|
||||
delta_total += delta;
|
||||
}
|
||||
if (bunchcount % 10 == 0 && bunchcount > 0) {
|
||||
QuiskPrintf("%f: send_remote_sound_socket SUMMARY calls: %f, samples %f, deltasec %f\n",
|
||||
new_ts, callcount_total, sampcount_total, delta_total);
|
||||
QuiskPrintf("%f: send_remote_sound_socket SUMMARY RATES (HZ): calls %f, samples %f\n",
|
||||
new_ts, callcount_total / delta_total, sampcount_total / delta_total);
|
||||
}
|
||||
bunchcount++;
|
||||
callcount = 0;
|
||||
sampcount = 0;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Send graph data via UDP from the remote radio to the control head
|
||||
void send_graph_data(double * fft_avg, int fft_size, double zoom, double deltaf, int fft_sample_rate, double scale)
|
||||
{
|
||||
static double * pixels = NULL;
|
||||
static int n_pixels = 0;
|
||||
static uint8_t sequence = 0;
|
||||
uint8_t flags;
|
||||
int16_t buffer[MAX_UDP_INT16_T];
|
||||
int16_t block;
|
||||
int pixel_index, buffer_index;
|
||||
double d1, d2;
|
||||
ssize_t sent;
|
||||
char buf[RX_BUFFER_SIZE]; // For startup message
|
||||
int recv_len;
|
||||
struct sockaddr_in far_addr;
|
||||
#ifdef MS_WINDOWS
|
||||
int addr_len = sizeof(struct sockaddr_in);
|
||||
#else
|
||||
socklen_t addr_len = sizeof(struct sockaddr_in);
|
||||
#endif
|
||||
|
||||
if (remote_radio_graph_socket == INVALID_SOCKET)
|
||||
return;
|
||||
if ( !control_head_data_width)
|
||||
return;
|
||||
if ( !remote_radio_graph_socket_started) {
|
||||
// Receive short msg, grab far end address
|
||||
// Receive from control head is necessary to establish a path through NAT
|
||||
if ((recv_len = recvfrom(remote_radio_graph_socket, buf, RX_BUFFER_SIZE, 0, (struct sockaddr *) &far_addr, &addr_len)) < 2) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (connect(remote_radio_graph_socket, (const struct sockaddr *)&far_addr, sizeof(far_addr)) != 0) {
|
||||
QuiskPrintf("send_remote_graph_socket), connect(): %s\n", strerror(errno));
|
||||
close(remote_radio_graph_socket);
|
||||
remote_radio_graph_socket = INVALID_SOCKET;
|
||||
return;
|
||||
}
|
||||
else {
|
||||
remote_radio_graph_socket_started = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (control_head_data_width > n_pixels) {
|
||||
n_pixels = control_head_data_width;
|
||||
if (pixels)
|
||||
free(pixels);
|
||||
pixels = (double *)malloc(n_pixels * sizeof(double));
|
||||
}
|
||||
if ( ! fft_avg) { // send dummy graph data
|
||||
send(remote_radio_graph_socket, "dum", 3, 0);
|
||||
return;
|
||||
}
|
||||
copy2pixels(pixels, control_head_data_width, fft_avg, fft_size, zoom, deltaf, fft_sample_rate);
|
||||
// Send multiple 16-bit data blocks: {flags, sequence}, block number, 16-bit graph data
|
||||
// 8-bit flags:
|
||||
// bit 0: clip indicator
|
||||
// 8-bit sequence: 0, 1, 2, ..., 255
|
||||
block = 0;
|
||||
pixel_index = 0;
|
||||
while (pixel_index < control_head_data_width) {
|
||||
if (quisk_get_overrange())
|
||||
flags = 0x01;
|
||||
else
|
||||
flags = 0x00;
|
||||
buffer[0] = flags << 8 | sequence;
|
||||
buffer[1] = block;
|
||||
buffer_index = 2;
|
||||
while (buffer_index < MAX_UDP_INT16_T && pixel_index < control_head_data_width) {
|
||||
d1 = pixels[pixel_index++];
|
||||
if (fabs(d1) < 1e-40) // avoid log10(0)
|
||||
d1 = 1E-40;
|
||||
d2 = 20.0 * log10(d1) - scale;
|
||||
if (d2 < -200)
|
||||
d2 = -200;
|
||||
else if (d2 > 0)
|
||||
d2 = 0;
|
||||
buffer[buffer_index++] = (int16_t)lround(d2 * GRAPH_DATA_SCALE);
|
||||
}
|
||||
sent = send(remote_radio_graph_socket, (const char *)buffer, buffer_index * 2, 0);
|
||||
if (sent != buffer_index * 2)
|
||||
QuiskPrintf("send_graph_data(), send(): %s\n", strerror(errno));
|
||||
block++;
|
||||
}
|
||||
sequence += 1;
|
||||
}
|
||||
|
||||
// Receive graph data via UDP on the control head
|
||||
int receive_graph_data(double * fft_avg)
|
||||
{
|
||||
int i, i1, i2;
|
||||
ssize_t count;
|
||||
uint8_t seq, flags;
|
||||
int16_t buffer[MAX_UDP_INT16_T];
|
||||
int16_t block;
|
||||
static int16_t * pixels = NULL;
|
||||
static int n_pixels = 0;
|
||||
static int total = 0;
|
||||
static int16_t sequence = 0;
|
||||
|
||||
if (control_head_graph_socket == INVALID_SOCKET)
|
||||
return 0;
|
||||
// Signal far end (server) that we're ready (this sends our address/port to far end)
|
||||
if ( !control_head_graph_socket_started) {
|
||||
i = send(control_head_graph_socket, "rr\n", 3, 0);
|
||||
if (i != 3)
|
||||
QuiskPrintf("receive_graph_data(), send(): %s\n", strerror(errno));
|
||||
}
|
||||
if (n_pixels < data_width) {
|
||||
n_pixels = data_width;
|
||||
if (pixels)
|
||||
free(pixels);
|
||||
pixels = (int16_t *)malloc(n_pixels * sizeof(int16_t));
|
||||
}
|
||||
count = recv(control_head_graph_socket, (char *)buffer, MAX_UDP_INT16_T * 2, 0);
|
||||
count /= 2; // convert to int16_t
|
||||
if (count > 2) {
|
||||
control_head_graph_socket_started = 1;
|
||||
flags = buffer[0] >> 8;
|
||||
if (flags & 0x01) // Clip
|
||||
quisk_sound_state.overrange++;
|
||||
seq = buffer[0] & 0xFF;
|
||||
if (seq != sequence) { // new graph data
|
||||
sequence = seq;
|
||||
total = 0;
|
||||
}
|
||||
block = buffer[1];
|
||||
count -= 2; // number of 16-bit graph data items
|
||||
i1 = block * (MAX_UDP_INT16_T - 2);
|
||||
i2 = i1 + count;
|
||||
if (i1 >= 0 && i2 <= data_width) {
|
||||
memcpy(pixels + i1, buffer + 2, count * 2);
|
||||
total += count;
|
||||
if (total == data_width) {
|
||||
for (i = 0; i < data_width; i++)
|
||||
fft_avg[i] = (double)pixels[i] / GRAPH_DATA_SCALE;
|
||||
return data_width;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int start_winsock()
|
||||
{
|
||||
#ifdef MS_WINDOWS
|
||||
WORD wVersionRequested = MAKEWORD(2, 2);
|
||||
WSADATA wsaData;
|
||||
|
||||
if (WSAStartup(wVersionRequested, &wsaData) != 0) {
|
||||
QuiskPrintf("start_winsock(): %s\n", strerror(errno));
|
||||
return 0; // failure to start winsock
|
||||
}
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
static void open_and_bind_socket(SOCKET * sock, char * ip, int port, int sndsize, char * name, int non_block)
|
||||
{
|
||||
struct sockaddr_in bind_addr;
|
||||
const char enable = 1; // for sockopt
|
||||
#ifndef MS_WINDOWS
|
||||
int tos = 184; // DSCP "Expedite" (46)
|
||||
#endif
|
||||
|
||||
if (!start_winsock()) {
|
||||
QuiskPrintf("open_and_bind_socket for %s: Failure to start WinSock\n", name);
|
||||
return;
|
||||
}
|
||||
|
||||
*sock = socket(PF_INET, SOCK_DGRAM, 0);
|
||||
if (*sock != INVALID_SOCKET) {
|
||||
setsockopt(*sock, SOL_SOCKET, SO_SNDBUF, (char *)&sndsize, sizeof(sndsize));
|
||||
setsockopt(*sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable));
|
||||
#ifndef MS_WINDOWS
|
||||
setsockopt(*sock, IPPROTO_IP, IP_TOS, &tos, sizeof(tos));
|
||||
#endif
|
||||
|
||||
// bind to this computer for receiving (and reading far-end address from client)
|
||||
memset((char *) &bind_addr, 0, sizeof(bind_addr));
|
||||
bind_addr.sin_family = AF_INET;
|
||||
bind_addr.sin_port = htons(port);
|
||||
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
if (bind(*sock, (const struct sockaddr *)&bind_addr, sizeof(bind_addr)) != 0) {
|
||||
QuiskPrintf("open_and_bind_socket(), bind(): %s\n", strerror(errno));
|
||||
close(*sock);
|
||||
*sock = INVALID_SOCKET;
|
||||
}
|
||||
else if (non_block) {
|
||||
#ifdef MS_WINDOWS
|
||||
unsigned long one = 1;
|
||||
ioctlsocket(*sock, FIONBIO, &one); // set non-blocking
|
||||
#else
|
||||
int flags;
|
||||
flags = fcntl(*sock, F_GETFL, 0); // set non-blocking
|
||||
fcntl(*sock, F_SETFL, flags | O_NONBLOCK);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (*sock == INVALID_SOCKET) {
|
||||
QuiskPrintf("open server %s: Failure to open socket\n", name);
|
||||
}
|
||||
else {
|
||||
QuiskPrintf("open server %s: opened socket %s port %i\n", name, ip, port);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void open_and_connect_socket(SOCKET * sock, char * ip, int port, int sndsize, char * name, int non_block)
|
||||
{
|
||||
struct sockaddr_in Addr;
|
||||
const char enable = 1; // for sockopt
|
||||
|
||||
if (!start_winsock()) {
|
||||
QuiskPrintf("open_and_connect_socket for %s: Failure to start WinSock\n", name);
|
||||
return;
|
||||
}
|
||||
|
||||
*sock = socket(PF_INET, SOCK_DGRAM, 0);
|
||||
if (*sock != INVALID_SOCKET) {
|
||||
setsockopt(*sock, SOL_SOCKET, SO_RCVBUF, (char *)&sndsize, sizeof(sndsize));
|
||||
setsockopt(*sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable));
|
||||
|
||||
// set far-end address structure to enable sending initial packet to get things started
|
||||
Addr.sin_family = AF_INET;
|
||||
Addr.sin_port = htons(port);
|
||||
#ifdef MS_WINDOWS
|
||||
Addr.sin_addr.S_un.S_addr = inet_addr(ip);
|
||||
#else
|
||||
inet_aton(ip, &Addr.sin_addr);
|
||||
#endif
|
||||
if (connect(*sock, (const struct sockaddr *)&Addr, sizeof(Addr)) != 0) {
|
||||
close(*sock);
|
||||
*sock = INVALID_SOCKET;
|
||||
}
|
||||
else if (non_block) {
|
||||
#ifdef MS_WINDOWS
|
||||
unsigned long one = 1;
|
||||
ioctlsocket(*sock, FIONBIO, &one); // set non-blocking
|
||||
#else
|
||||
int flags;
|
||||
flags = fcntl(*sock, F_GETFL, 0); // set non-blocking
|
||||
fcntl(*sock, F_SETFL, flags | O_NONBLOCK);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (*sock == INVALID_SOCKET) {
|
||||
QuiskPrintf("open client %s: Failure to open socket\n", name);
|
||||
}
|
||||
else {
|
||||
QuiskPrintf("open client %s: opened socket %s port %i\n", name, ip, port);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void close_socket(SOCKET * sock, char * name)
|
||||
{
|
||||
if (*sock != INVALID_SOCKET) {
|
||||
close(*sock);
|
||||
*sock = INVALID_SOCKET;
|
||||
#ifdef MS_WINDOWS
|
||||
WSACleanup();
|
||||
#endif
|
||||
QuiskPrintf("%s: closed socket\n", name);
|
||||
}
|
||||
else {
|
||||
QuiskPrintf("%s: socket already closed\n", name);
|
||||
}
|
||||
}
|
||||
|
||||
// start running UDP remote sound on control_head ...
|
||||
// ... receive radio sound from remote_radio, send mic sound to remote_radio
|
||||
PyObject * quisk_start_control_head_remote_sound(PyObject * self, PyObject * args)
|
||||
{
|
||||
int radio_sound_port;
|
||||
int graph_data_port;
|
||||
int sndsize = 48000;
|
||||
char * remote_radio_ip; // IP address of far end
|
||||
char * name;
|
||||
SOCKET * sock;
|
||||
|
||||
if (!PyArg_ParseTuple (args, "sii", &remote_radio_ip, &radio_sound_port, &graph_data_port))
|
||||
return NULL;
|
||||
|
||||
name = "radio sound from remote_radio";
|
||||
sock = &control_head_sound_socket;
|
||||
open_and_connect_socket(sock, remote_radio_ip, radio_sound_port, sndsize, name, 0);
|
||||
|
||||
name = "graph data from remote_radio";
|
||||
sock = &control_head_graph_socket;
|
||||
open_and_connect_socket(sock, remote_radio_ip, graph_data_port, 1024 * 8, name, 1);
|
||||
|
||||
packets_sent = 0;
|
||||
packets_recd = 0;
|
||||
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
// stop running UDP remote sound on control_head
|
||||
PyObject * quisk_stop_control_head_remote_sound(PyObject * self, PyObject * args)
|
||||
{
|
||||
char * name;
|
||||
SOCKET * sock;
|
||||
|
||||
if (!PyArg_ParseTuple (args, ""))
|
||||
return NULL;
|
||||
|
||||
name = "radio sound from remote_radio";
|
||||
sock = &control_head_sound_socket;
|
||||
close_socket(sock, name);
|
||||
|
||||
name = "graph data from remote_radio";
|
||||
sock = &control_head_graph_socket;
|
||||
close_socket(sock, name);
|
||||
|
||||
control_head_sound_socket_started = 0; // reset for next time
|
||||
remote_radio_sound_socket_started = 0;
|
||||
control_head_graph_socket_started = 0;
|
||||
remote_radio_graph_socket_started = 0;
|
||||
|
||||
QuiskPrintf("total packets sent = %i, recd = %i\n", packets_sent, packets_recd);
|
||||
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
// start running UDP remote sound on remote_radio ...
|
||||
// ... send radio sound to control_head, receive mic sound from control_head
|
||||
PyObject * quisk_start_remote_radio_remote_sound(PyObject * self, PyObject * args)
|
||||
{
|
||||
int radio_sound_port;
|
||||
int graph_data_port;
|
||||
int sndsize = 48000;
|
||||
char * control_head_ip; // IP address of far end
|
||||
char * name;
|
||||
SOCKET * sock;
|
||||
|
||||
if (!PyArg_ParseTuple (args, "siii", &control_head_ip, &radio_sound_port,
|
||||
&graph_data_port, &control_head_data_width))
|
||||
return NULL;
|
||||
|
||||
name = "radio sound to control_head";
|
||||
sock = &remote_radio_sound_socket;
|
||||
open_and_bind_socket(sock, control_head_ip, radio_sound_port, sndsize, name, 0);
|
||||
|
||||
name = "graph data to control_head";
|
||||
sock = &remote_radio_graph_socket;
|
||||
open_and_bind_socket(sock, control_head_ip, graph_data_port, 1024 * 8, name, 1);
|
||||
|
||||
packets_sent = 0;
|
||||
packets_recd = 0;
|
||||
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
// stop running UDP remote sound on remote_radio
|
||||
PyObject * quisk_stop_remote_radio_remote_sound(PyObject * self, PyObject * args)
|
||||
{
|
||||
char * name;
|
||||
SOCKET * sock;
|
||||
|
||||
if (!PyArg_ParseTuple (args, ""))
|
||||
return NULL;
|
||||
|
||||
name = "radio sound to control_head";
|
||||
sock = &remote_radio_sound_socket;
|
||||
close_socket(sock, name);
|
||||
|
||||
name = "graph data to control_head";
|
||||
sock = &remote_radio_graph_socket;
|
||||
close_socket(sock, name);
|
||||
|
||||
control_head_sound_socket_started = 0; // reset for next time
|
||||
remote_radio_sound_socket_started = 0;
|
||||
control_head_graph_socket_started = 0;
|
||||
remote_radio_graph_socket_started = 0;
|
||||
control_head_data_width = 0;
|
||||
|
||||
QuiskPrintf("total packets sent = %i, recd = %i\n", packets_sent, packets_recd);
|
||||
|
||||
return Py_None;
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
# THIS FILE IS FOR USE ON A REMOTELY CONTROLLED "REMOTE_RADIO" COMPUTER
|
||||
# RUNNING QUISK TO CONTROL ATTACHED ACTUAL/REAL RADIO HARDWARE.
|
||||
# IT CONNECTS BY NETWORK TO A SEPARATE "CONTROL_HEAD" COMPUTER ALSO RUNNING QUISK.
|
||||
#
|
||||
# This software is Copyright (C) 2021-2022 by Ben Cahill and 2006-2022 by James C. Ahlstrom.,
|
||||
# and is licensed for use under the GNU General Public License (GPL).
|
||||
# See http://www.opensource.org.
|
||||
# Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!!
|
||||
#
|
||||
# This file, remote_common.py, along with a radio-specific file, e.g.
|
||||
# remote_softrock.py, remote_hermes.py, or similar, allows a radio-less (control_head) Quisk,
|
||||
# running on a separate computer, to connect to this (remote_radio) instance of Quisk,
|
||||
# which has an actual, real radio attached.
|
||||
#
|
||||
# The control_head Quisk must use control_common.py, along with a radio-specific file,
|
||||
# e.g. control_softrock.py, control_hermes.py, or similar, to communicate with this
|
||||
# remote_radio Quisk via a network connection.
|
||||
#
|
||||
# This remote_radio computer should be set up with a static IP address, so that you know
|
||||
# where to point the control_head. The control head computer may, however, use dynamic
|
||||
# addressing; this remote_radio computer will read the control head address when the
|
||||
# remote control connection is made.
|
||||
#
|
||||
# The main control interface between control_head and remote_radio is via a TCP port;
|
||||
# this uses very low bandwidth. All functional control, including CW keying, is done via this port.
|
||||
#
|
||||
# There are 2 additional ports, both UDP, using low to moderate bandwidth:
|
||||
# -- Send graph/waterfall data to the control_head
|
||||
# -- Send radio sound to the control_head, and receive mic sound
|
||||
# These use sequential port numbers based on the TCP port number self.remote_ctl_base_port.
|
||||
# If you need to change the default base port number, you can edit the line in this file
|
||||
# that looks like (without the #):
|
||||
#
|
||||
# self.remote_ctl_base_port = 4585 # Remote Control base TCP port
|
||||
# self.graph_data_port = self.remote_ctl_base_port + 1 # UDP port for graph data from the remote
|
||||
# self.remote_radio_sound_port = self.remote_ctl_base_port + 2 # UDP port for radio sound and mic samples
|
||||
#
|
||||
#
|
||||
# Make sure to edit the corresponding line in control_common.py to match ports!!
|
||||
#
|
||||
# This remote_radio Quisk/computer is assumed to track the connected control_head Quisk/computer;
|
||||
# no attempt is made by the control_head to verify the remote_radio Quisk's tuning frequency, mode, etc.
|
||||
# Snap-to Rx tuning for CW works on the control_head Quisk by virtue of graph/waterfall data
|
||||
# received from the remote_radio Quisk.
|
||||
#
|
||||
# To see detailed log output of CW key timing, set DEBUG_CW_JITTER = 1.
|
||||
# To additionally see, when CW commands are pending, timestamps of when PollCwKey is called
|
||||
# (e.g. to check thread scheduling behavior), set DEBUG_CW_JITTER = 2.
|
||||
# To send "perfect" bursts of CW dits from the control_head, set DEBUG_CW_SEND_DITS = 1
|
||||
# in the control_head's quisk_hardware_control_head.py.
|
||||
|
||||
DEBUG_CW_JITTER = 0
|
||||
DEBUG = 0
|
||||
|
||||
from collections import deque # for CW event queue
|
||||
import socket, time, traceback, string, hmac, secrets, json
|
||||
import _quisk as QS
|
||||
from quisk_widgets import *
|
||||
|
||||
class Remot: # Remote comtrol base class
|
||||
def __init__(self, app, conf):
|
||||
self.app = app # Access Quisk class App (Python) functions
|
||||
self.conf = conf
|
||||
self.token = "abc"
|
||||
self.token_time = 0
|
||||
|
||||
self.control_head_ip = None # IP of control_head compter (read upon connection)
|
||||
self.remote_ctl_base_port = 4585 # Base of ports for remote connection (maybe edit this)
|
||||
self.remote_ctl_socket = None
|
||||
self.remote_ctl_connection = None
|
||||
self.remote_ctl_heartbeat_ts = None
|
||||
self.remote_ctl_heartbeat_timeout = 10.0 # Close our connection if we don't hear heartbeat from Control Head
|
||||
self.graph_data_port = self.remote_ctl_base_port + 1
|
||||
self.remote_radio_sound_port = self.remote_ctl_base_port + 2
|
||||
|
||||
self.cw_delay_secs = 0.020 # time delay to absorb WiFi jitter, in secs
|
||||
self.cw_phrase_begin_ts = None # timestamp of beginning of cw phrase
|
||||
self.cw_next_event_ts = None
|
||||
self.cw_next_keydown = None
|
||||
self.cw_event_queue = deque()
|
||||
self.cw_key_down = 0 # Tx-enable management
|
||||
self.cw_tx_enable = 0
|
||||
|
||||
self.received = ''
|
||||
self.cmd_text = None # cmd received from client (remote head)
|
||||
self.cmd = None # cmd received from client (remote head)
|
||||
self.params = None # params = the string following the command
|
||||
self.extended = None
|
||||
self.split_mode = 0
|
||||
|
||||
print('Remote Overlay Initialized!')
|
||||
|
||||
def open(self):
|
||||
self.token = "abc"
|
||||
self.remote_ctl_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.remote_ctl_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.remote_ctl_socket.bind(('', self.remote_ctl_base_port)) # '' == INADDR_ANY
|
||||
self.remote_ctl_socket.settimeout(0.0)
|
||||
self.remote_ctl_socket.listen(1) # listen for TCP connection from just one client
|
||||
print('Remote Overlay Opened!')
|
||||
# Return an informative message for the config screen.
|
||||
# This method must return a string showing whether the open succeeded or failed.
|
||||
# Here, we are over-writing the string set by quisk_hardware_model.py
|
||||
t = f'Quisk Remote Controlled Radio {self.app.width}x{self.app.height} {self.app.graph_width} {self.app.data_width}'
|
||||
return t
|
||||
#BMC return ret
|
||||
|
||||
def close(self): # Close the listening socket, then the connection socket
|
||||
if self.remote_ctl_socket:
|
||||
self.remote_ctl_socket.close()
|
||||
self.remote_ctl_socket = None
|
||||
self.token = "abc"
|
||||
if self.remote_ctl_connection:
|
||||
print('Closing Remote Control connection: close')
|
||||
self.RemoteCtlClose(True)
|
||||
|
||||
def RemoteCtlOpen(self):
|
||||
try:
|
||||
self.remote_ctl_connection, address = self.remote_ctl_socket.accept()
|
||||
except:
|
||||
return
|
||||
else:
|
||||
self.app.remote_control_slave = True
|
||||
QS.set_sparams(remote_control_slave=1)
|
||||
self.token = secrets.token_hex(32)
|
||||
self.remote_ctl_connection.settimeout(0.0)
|
||||
self.remote_ctl_heartbeat_ts = time.time()
|
||||
if DEBUG: print('Remote Control connection: ', self.remote_ctl_connection, ' address: ', address)
|
||||
self.control_head_ip = address[0]
|
||||
print ("Remote control connection from", self.control_head_ip)
|
||||
self.RemoteCtlSend("TOKEN;" + self.token + "\n")
|
||||
self.token_time = time.time()
|
||||
|
||||
def RemoteCtlClose(self, send_quit):
|
||||
self.StopTransmit()
|
||||
if self.remote_ctl_connection:
|
||||
if send_quit:
|
||||
self.RemoteCtlSend('Q\n')
|
||||
if self.remote_ctl_connection:
|
||||
self.remote_ctl_connection.close()
|
||||
self.remote_ctl_connection = None
|
||||
QS.stop_remote_radio_remote_sound()
|
||||
self.app.remote_control_slave = False
|
||||
QS.set_sparams(remote_control_slave=0)
|
||||
|
||||
def RemoteCtlSend(self, text):
|
||||
# Send text back to the client
|
||||
if not self.remote_ctl_connection:
|
||||
return
|
||||
if isinstance(text, str):
|
||||
text = text.encode('utf-8', errors='ignore')
|
||||
try:
|
||||
self.remote_ctl_connection.sendall(text)
|
||||
except socket.error:
|
||||
print('Closing Remote Control connection: sendall() failed. Sent text:\n ' + text.decode('utf-8'))
|
||||
# NOTE: Cannot send 'Q' to Control Head here; sendall() isn't working!
|
||||
self.RemoteCtlClose(False)
|
||||
|
||||
def ErrParam(self): # Invalid parameter
|
||||
t = 'ERR_PARAM: ' + self.cmd_text + '\n'
|
||||
print(t)
|
||||
self.RemoteCtlSend(t)
|
||||
def ErrUnsupported(self): # Command recognized but not supported (because of either H/W or configuration)
|
||||
t = 'ERR_UNSUPPORTED: ' + self.cmd_text + '\n'
|
||||
print(t)
|
||||
self.RemoteCtlSend(t)
|
||||
def ErrUnrecognized(self): # Unrecognized command
|
||||
t = 'ERR_UNRECOGNIZED_CMD: ' + self.cmd_text + '\n'
|
||||
print(t)
|
||||
self.RemoteCtlSend(t)
|
||||
def ErrBadFormat(self): # Something wrong with format of command
|
||||
t = 'ERR_BADFORMAT: ' + self.cmd_text + '\n'
|
||||
print(t)
|
||||
self.RemoteCtlSend(t)
|
||||
|
||||
def HeartBeat(self): # Called at about 10 Hz by the GUI thread
|
||||
if self.remote_ctl_connection:
|
||||
# Monitor the remote connection via periodic heartbeat from Control Head
|
||||
ts = time.time()
|
||||
if (ts - self.remote_ctl_heartbeat_ts) > self.remote_ctl_heartbeat_timeout:
|
||||
print('Closing Remote Control connection: Lost HEARTBEAT from Control Head')
|
||||
self.RemoteCtlClose(True)
|
||||
else:
|
||||
# Continually try to connect with Control Head
|
||||
self.RemoteCtlOpen()
|
||||
|
||||
def FastHeartBeat(self): # Called frequently by the GUI thread
|
||||
"""This is the remote slave processing loop, and is called frequently. It reads and satisfies requests."""
|
||||
if not self.remote_ctl_connection:
|
||||
return
|
||||
try: # Read any data from the socket
|
||||
text = self.remote_ctl_connection.recv(1024)
|
||||
except:
|
||||
#traceback.print_exc()
|
||||
return
|
||||
else: # We got some characters
|
||||
if not isinstance(text, str):
|
||||
text = text.decode('utf-8')
|
||||
self.received += text
|
||||
if not '\n' in self.received: # A complete command ending with newline is not available
|
||||
return
|
||||
while '\n' in self.received: # At least one complete command ending with newline *is* available
|
||||
cmd_text, self.received = self.received.split('\n', 1) # Split off the command, save any further characters
|
||||
cmd_text = cmd_text.strip() # Here is our command
|
||||
if not cmd_text:
|
||||
continue
|
||||
self.cmd_text = cmd_text
|
||||
args = cmd_text.split(';') # Split at ';' because some control names have blanks
|
||||
command = args[0]
|
||||
params = args[1:]
|
||||
# TOKEN
|
||||
if self.token:
|
||||
if command == "TOKEN":
|
||||
passw = self.app.local_conf.globals.get("remote_radio_password", "")
|
||||
passw = passw.strip()
|
||||
if not passw:
|
||||
self.RemoteCtlSend("TOKEN_MISSING\n")
|
||||
print ("Error: Missing password on remote radio")
|
||||
continue
|
||||
H = hmac.new(passw.encode('utf-8'), self.token.encode('utf-8'), 'sha3_256')
|
||||
del passw
|
||||
if hmac.compare_digest(H.hexdigest(), args[1]):
|
||||
self.token = None
|
||||
print ("Security challenge passed", args[2])
|
||||
self.control_head_data_width = int(args[2])
|
||||
self.RemoteCtlSend("TOKEN_OK\n")
|
||||
self.remote_ctl_heartbeat_ts = time.time()
|
||||
QS.start_remote_radio_remote_sound(self.control_head_ip, self.remote_radio_sound_port,
|
||||
self.graph_data_port, self.control_head_data_width)
|
||||
else:
|
||||
time.sleep(1)
|
||||
elif time.time() - self.token_time > 5:
|
||||
self.RemoteCtlSend("TOKEN_BAD\n")
|
||||
self.RemoteCtlClose(True)
|
||||
print ("Security failed")
|
||||
continue
|
||||
# Check for Quit and Heartbeat before any other commands
|
||||
if command == 'QUIT':
|
||||
print('Closing Remote Control connection: QUIT from Control Head')
|
||||
# NOTE: Do not send 'Q' to Control Head; sendall() will fail because Control Head already disconnected
|
||||
self.RemoteCtlClose(False)
|
||||
continue
|
||||
# HEARTBEAT
|
||||
if command == 'HEARTBEAT':
|
||||
self.remote_ctl_heartbeat_ts = time.time()
|
||||
continue
|
||||
# Ignore the On/Off button, Help buttons, Small window pop buttons
|
||||
if command in ("On", "..", "bandBtnGroup", "screenBtnGroup", "modeButns", "Scope", "Config", "RX Filter", "Help"):
|
||||
continue
|
||||
if DEBUG: print("Remote receive:", cmd_text)
|
||||
# Look for radio buttons
|
||||
if self.ProcessRadioBtn(command, self.cmd_text):
|
||||
continue
|
||||
# buttons in idName2Button
|
||||
btn = self.app.idName2Button.get(args[0], None)
|
||||
if btn:
|
||||
#print ("Slave process button", cmd_text, btn.__class__)
|
||||
value = int(args[1])
|
||||
btn.SetIndex(value, True)
|
||||
continue
|
||||
# controls in midiControls
|
||||
if command in self.app.midiControls:
|
||||
ctrl, func = self.app.midiControls[command]
|
||||
#print ("Slave Process control", cmd_text, ctrl.__class__, func)
|
||||
value = int(args[1])
|
||||
if isinstance(ctrl, WrapSlider):
|
||||
ctrl.ChangeSlider(value)
|
||||
else:
|
||||
ctrl.SetValue(value)
|
||||
func()
|
||||
continue
|
||||
# FREQ
|
||||
if command == 'FREQ':
|
||||
freq, vfo, source, band, rxFreq, var_decim_index = args[1:]
|
||||
freq = int(freq)
|
||||
vfo = int(vfo)
|
||||
rxFreq = int(rxFreq)
|
||||
if rxFreq == self.app.rxFreq:
|
||||
rxFreq = None
|
||||
var_decim_index = int(var_decim_index)
|
||||
self.app.ChangeHwFrequency(freq - vfo, vfo, source, band, None, rxFreq)
|
||||
if source == "NewDecim":
|
||||
self.app.config_screen.config.btn_decimation.SetSelection(var_decim_index)
|
||||
sample_rate = self.VarDecimSet(var_decim_index)
|
||||
self.app.OnBtnDecimation(rate=sample_rate)
|
||||
# Json function call
|
||||
elif command == 'JsonAppFunc':
|
||||
# Call the function self.app. + func with the specified arguments
|
||||
# func = "%s.SetLabel" % self.idName
|
||||
# application.Hardware.RemoteCtlSend(f'JsonAppFunc;{json.dumps((func, label, do_cmd, direction))}\n')
|
||||
jargs = json.loads(params[0])
|
||||
pyobj = self.app
|
||||
for nam in jargs[0].split('.'):
|
||||
pyobj = getattr(pyobj, nam)
|
||||
pyobj(*jargs[1:])
|
||||
# AGC and Squelch levels
|
||||
elif command == 'AGCSQLCH':
|
||||
ctrl = self.app.midiControls["AGCSlider"][0]
|
||||
ctrl.SetSlider(value_off=int(args[1]), value_on=int(args[2]))
|
||||
self.app.levelSquelch = int(args[3])
|
||||
self.app.levelSquelchSSB = int(args[4])
|
||||
self.app.split_offset = int(args[5])
|
||||
# CW Keying
|
||||
elif command == 'CW':
|
||||
ts = time.time()
|
||||
if len(params) < 2:
|
||||
self.ErrParam()
|
||||
return
|
||||
if params[0] in '01':
|
||||
keydown = int(params[0])
|
||||
else:
|
||||
print('Bad keydown value in CW command:', params[0])
|
||||
self.ErrParam()
|
||||
return
|
||||
cw_event_ts = float(params[1]) / 1000.0 # int msecs to float secs
|
||||
if cw_event_ts == 0.0:
|
||||
if keydown != 1:
|
||||
# 'CW 0 0' == "Force Stop of CW"; clear all queued CW commands, and force CW key up
|
||||
print('Forcing stop of CW')
|
||||
while len(self.cw_event_queue):
|
||||
self.cw_event_queue.popleft()
|
||||
self.cw_next_event_ts = None
|
||||
self.cw_next_keydown = None
|
||||
self.cw_key_down = 0
|
||||
QS.set_remote_cwkey(0)
|
||||
else:
|
||||
# Begin new cw phrase; any prior phrase should be done by now.
|
||||
# Set up first cw event to be ready to execute.
|
||||
self.cw_begin_phrase_ts = ts + self.cw_delay_secs
|
||||
cw_new_event_ts = self.cw_begin_phrase_ts + cw_event_ts
|
||||
if not self.cw_next_event_ts:
|
||||
self.cw_next_event_ts = cw_new_event_ts
|
||||
self.cw_next_keydown = keydown
|
||||
if DEBUG_CW_JITTER: print(f'{ts:10.4f} setting: {keydown} {cw_event_ts:2.3f} {cw_new_event_ts:10.4f}')
|
||||
else:
|
||||
self.cw_event_queue.append((cw_new_event_ts, keydown))
|
||||
if DEBUG_CW_JITTER: print(f'{ts:10.4f} queing: {keydown} {cw_event_ts:2.3f} {cw_new_event_ts:10.4f}')
|
||||
# Menu
|
||||
elif command == 'MENU':
|
||||
menu_name, item_text, checked = args[1:]
|
||||
if item_text == 'Reverse Rx and Tx':
|
||||
continue # No need to call handler, as rxFreq and txFreq are already handled
|
||||
menu = getattr(self.app, menu_name)
|
||||
nid = menu.item_text2id[item_text]
|
||||
menu_item = menu.FindItemById(nid)
|
||||
if menu_item.IsCheckable():
|
||||
menu_item.Check(int(checked))
|
||||
menu.Handler(None, nid)
|
||||
else:
|
||||
t = 'ERR_UNRECOGNIZED_CMD: %s\n' % cmd_text
|
||||
print(t)
|
||||
self.RemoteCtlSend(t)
|
||||
continue
|
||||
|
||||
def PollCwKey(self): # Called periodically at HW Poll usec period (typ. 50-200 Hz) by the sound thread
|
||||
cw_queue_len = len(self.cw_event_queue)
|
||||
if self.cw_next_event_ts or cw_queue_len > 0:
|
||||
# We have at least one CW event. If it's time to do so, set the next CW key down/up, look for next CW event.
|
||||
ts = time.time()
|
||||
if DEBUG_CW_JITTER > 1: print(f'{ts:10.4f}')
|
||||
if not self.cw_next_event_ts:
|
||||
# Nothing "on deck", but there is something on the cw event queue, so pop it off queue and put it "on deck".
|
||||
self.cw_next_event_ts, self.cw_next_keydown = self.cw_event_queue.popleft()
|
||||
if DEBUG_CW_JITTER: print(f'{ts:10.4f} queue len: {cw_queue_len}, popping: {self.cw_next_keydown} {self.cw_next_event_ts:10.4f}')
|
||||
if ts >= self.cw_next_event_ts:
|
||||
if DEBUG_CW_JITTER: print(f'{ts:10.4f} set_remote_cwkey: {self.cw_next_keydown}')
|
||||
QS.set_remote_cwkey(self.cw_next_keydown)
|
||||
self.cw_key_down = self.cw_next_keydown
|
||||
cw_queue_len = len(self.cw_event_queue)
|
||||
if cw_queue_len > 0:
|
||||
self.cw_next_event_ts, self.cw_next_keydown = self.cw_event_queue.popleft()
|
||||
if DEBUG_CW_JITTER: print(f'{ts:10.4f} queue len: {cw_queue_len}, popping: {self.cw_next_keydown} {self.cw_next_event_ts:10.4f}')
|
||||
else:
|
||||
self.cw_next_event_ts = None
|
||||
self.cw_next_keydown = None
|
||||
|
||||
def StopTransmit(self):
|
||||
# TODO: Add code for modes other than CW
|
||||
while len(self.cw_event_queue):
|
||||
self.cw_event_queue.popleft()
|
||||
self.cw_next_event_ts = None
|
||||
self.cw_next_keydown = None
|
||||
self.cw_key_down = 0
|
||||
QS.set_remote_cwkey(0)
|
||||
|
||||
def ProcessRadioBtn(self, command, cmd_text):
|
||||
# Large and Small format screens send different button events for radio buttons.
|
||||
# Band buttons:
|
||||
if command in self.conf.BandList:
|
||||
self.app.bandBtnGroup.SetLabel(command, True)
|
||||
return True # We processed this command
|
||||
if command in ("Audio", "Time"):
|
||||
self.app.bandBtnGroup.SetLabel(command, True)
|
||||
return True # We processed this command
|
||||
Mode = self.app.modeButns.SetLabel
|
||||
Screen = self.app.screenBtnGroup.SetLabel
|
||||
# Mode buttons: process both formats:
|
||||
if cmd_text in ("CW U/L;0", "CWL;0", "CWL;1"):
|
||||
Mode("CWL", True)
|
||||
elif cmd_text in ("CW U/L;1", "CWU;0", "CWU;1"):
|
||||
Mode("CWU", True)
|
||||
elif cmd_text in ("SSB U/L;0", "LSB;0", "LSB;1"):
|
||||
Mode("LSB", True)
|
||||
elif cmd_text in ("SSB U/L;1", "USB;0", "USB;1"):
|
||||
Mode("USB", True)
|
||||
elif cmd_text in ("AM;0", "AM;1"):
|
||||
Mode("AM", True)
|
||||
elif cmd_text in ("FM;0", "FM;1"):
|
||||
Mode("FM", True)
|
||||
elif cmd_text in ("DGT;0", "DGT-U;0", "DGT-U;1"):
|
||||
Mode("DGT-U", True)
|
||||
elif cmd_text in ("DGT;1", "DGT-L;0", "DGT-L;1"):
|
||||
Mode("DGT-L", True)
|
||||
elif cmd_text in ("DGT;2", "DGT-FM;0", "DGT-FM;1"):
|
||||
Mode("DGT-FM", True)
|
||||
elif cmd_text in ("DGT;3", "DGT-IQ;0", "DGT-IQ;1"):
|
||||
Mode("DGT-IQ", True)
|
||||
elif cmd_text in ("FDV;0", "FDV-U;0", "FDV-U;1"):
|
||||
Mode("FDV-U", True)
|
||||
elif cmd_text in ("FDV;1", "FDV-L;0", "FDV-L;1"):
|
||||
Mode("FDV-L", True)
|
||||
# Screen buttons: process both formats:
|
||||
# Due to ambiguous received commands, the setting is always "Graph" or "WFall" without "P1 or "P2".
|
||||
# The P1 and P2 are handled at the control head so this shouldn't matter.
|
||||
elif cmd_text in ("Graph;0", "Graph;0", "Graph;1"):
|
||||
Screen("Graph", True)
|
||||
elif cmd_text in ("Graph;1", "GraphP1;0", "GraphP1;1"):
|
||||
Screen("Graph", True)
|
||||
elif cmd_text in ("Graph;2", "GraphP2;0", "GraphP2;1"):
|
||||
Screen("Graph", True)
|
||||
elif cmd_text in ("WFall;0", "WFall;0", "WFall;1"):
|
||||
Screen("WFall", True)
|
||||
elif cmd_text in ("WFall;1", "WFallP1;0", "WFallP1;1"):
|
||||
Screen("WFall", True)
|
||||
elif cmd_text in ("WFall;2", "WFallP2;0", "WFallP2;1"):
|
||||
Screen("WFall", True)
|
||||
else:
|
||||
return False
|
||||
return True # We processed this command
|
||||
@@ -0,0 +1,25 @@
|
||||
# This provides access to a remote radio. See ac2yd/remote_common.py and .pdf files for documentation.
|
||||
|
||||
from hermes.quisk_hardware import Hardware as Radio
|
||||
from ac2yd.remote_common import Remot
|
||||
|
||||
class Hardware(Remot, Radio):
|
||||
def __init__(self, app, conf):
|
||||
Radio.__init__(self, app, conf)
|
||||
Remot.__init__(self, app, conf)
|
||||
def open(self):
|
||||
Remot.open(self)
|
||||
return "Server: " + Radio.open(self)
|
||||
def close(self):
|
||||
Remot.close(self)
|
||||
Radio.close(self)
|
||||
def HeartBeat(self):
|
||||
Remot.HeartBeat(self)
|
||||
Radio.HeartBeat(self)
|
||||
if self.app.remote_control_slave:
|
||||
widg = self.app.bottom_widgets
|
||||
t = "HL2_TEMP;%s;%s;%s;%s\n" % (
|
||||
widg.text_temperature.GetLabel(), widg.text_pa_current.GetLabel(),
|
||||
widg.text_fwd_power.GetLabel(), widg.text_swr.GetLabel())
|
||||
self.RemoteCtlSend(t)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# This provides access to a remote radio. See ac2yd/remote_common.py and .pdf files for documentation.
|
||||
|
||||
from hiqsdr.quisk_hardware import Hardware as Radio
|
||||
from ac2yd.remote_common import Remot
|
||||
|
||||
class Hardware(Remot, Radio):
|
||||
def __init__(self, app, conf):
|
||||
Radio.__init__(self, app, conf)
|
||||
Remot.__init__(self, app, conf)
|
||||
def open(self):
|
||||
Remot.open(self)
|
||||
return "Server: " + Radio.open(self)
|
||||
def close(self):
|
||||
Remot.close(self)
|
||||
Radio.close(self)
|
||||
def HeartBeat(self):
|
||||
Remot.HeartBeat(self)
|
||||
Radio.HeartBeat(self)
|
||||
@@ -0,0 +1,19 @@
|
||||
# This provides access to a remote radio. See ac2yd/remote_common.py and .pdf files for documentation.
|
||||
|
||||
from softrock.hardware_usb import Hardware as Radio
|
||||
from ac2yd.remote_common import Remot
|
||||
|
||||
class Hardware(Remot, Radio):
|
||||
def __init__(self, app, conf):
|
||||
Radio.__init__(self, app, conf)
|
||||
Remot.__init__(self, app, conf)
|
||||
def open(self):
|
||||
Remot.open(self)
|
||||
return "Server: " + Radio.open(self)
|
||||
def close(self):
|
||||
Remot.close(self)
|
||||
Radio.close(self)
|
||||
def HeartBeat(self):
|
||||
Remot.HeartBeat(self)
|
||||
Radio.HeartBeat(self)
|
||||
|
||||
Reference in New Issue
Block a user