# This is a sample hardware file for UDP control.  Use this file for my 2010 transceiver
# described in QEX and for the improved version HiQSDR.  To turn on the extended
# features in HiQSDR, update your FPGA firmware to version 1.1 or later and use use_rx_udp = 2.

from __future__ import print_function

import struct, socket, math, traceback
import _quisk as QS

from quisk_hardware_model import Hardware as BaseHardware

DEBUG = 0

class Hardware(BaseHardware):
  def __init__(self, app, conf):
    BaseHardware.__init__(self, app, conf)
    self.use_sidetone = 1
    self.got_udp_status = ''		# status from UDP receiver
	# want_udp_status is a 14-byte string with numbers in little-endian order:
	#	[0:2]		'St'
	#	[2:6]		Rx tune phase
	#	[6:10]		Tx tune phase
	#	[10]		Tx output level 0 to 255
	#	[11]		Tx control bits:
	#		0x01	Enable CW transmit
	#		0x02	Enable all other transmit
	#		0x04	Use the HiQSDR extended IO pins not present in the 2010 QEX ver 1.0
	#		0x08	The key is down (software key)
	#	[12]	Rx control bits
	#			Second stage decimation less one, 1-39, six bits
	#	[13]	zero or firmware version number
	# The above is used for firmware  version 1.0.
	# Version 1.1 adds eight more bytes for the HiQSDR conntrol ports:
	#	[14]	X1 connector:  Preselect pins 69, 68, 65, 64; Preamp pin 63, Tx LED pin 57
	#	[15]	Attenuator pins 84, 83, 82, 81, 80
	#	[16]	More bits: AntSwitch pin 41 is 0x01
	#	[17:22] The remaining five bytes are sent as zero.
	# Version 1.2 uses the same format as 1.1, but adds the "Qs" command (see below).
	# Version 1.3 adds features needed by the new quisk_vna.py program:
	#	[17]	This one byte must be zero
	#	[18:20]	This is vna_count, the number of VNA data points; or zero for normal operation
	#	[20:22]	These two bytes mmust be zero

# The "Qs" command is a two-byte UDP packet sent to the control port.  It returns the hardware status
# as the above string, except that the string starts with "Qs" instead of "St".  Do not send the "Qs" command
# from Quisk, as it interferes with the "St" command.  The "Qs" command is meant to be used from an
# external program, such as HamLib or a logging program.

# When vna_count != 0, we are in VNA mode.  The start frequency is rx_phase, and for each point tx_phase is added
# to advance the frequency.  A zero sample is added to mark the blocks.  The samples are I and Q averaged at DC.

    self.rx_phase = 0
    self.tx_phase = 0
    self.tx_level = 0
    self.tx_control = 0
    self.rx_control = 0
    self.vna_count = 0	# VNA scan count; MUST be zero for non-VNA operation
    self.index = 0
    self.mode = None
    self.band = None
    self.rf_gain = 0
    self.HiQSDR_Connector_X1 = 0
    self.HiQSDR_Attenuator = 0
    self.HiQSDR_Bits = 0
    if conf.use_rx_udp == 2:	# Set to 2 for the HiQSDR
      self.rf_gain_labels = ('RF 0 dB', 'RF +10', 'RF -10', 'RF -20', 'RF -30')
      self.antenna_labels = ('Ant 1', 'Ant 2')
    self.firmware_version = None	# firmware version is initially unknown
    self.rx_udp_socket = None
    self.vfo_frequency = 0		# current vfo frequency
    self.tx_frequency = 0
    self.decimations = []		# supported decimation rates
    for dec in (40, 20, 10, 8, 5, 4, 2):
      self.decimations.append(dec * 64)
    if self.conf.fft_size_multiplier == 0:
      self.conf.fft_size_multiplier = 6		# Set size needed by VarDecim
  def open(self):
    # Create the proper broadcast address for rx_udp_ip.
    nm = self.conf.rx_udp_ip_netmask.split('.')
    ip = self.conf.rx_udp_ip.split('.')
    nm = map(int, nm)
    ip = map(int, ip)
    bc = ''
    for i in range(4):
      x = (ip[i] | ~ nm[i]) & 0xFF
      bc = bc + str(x) + '.'
    self.broadcast_addr = bc[:-1]
    # This socket is used for the Simple Network Discovery Protocol by AE4JY
    self.socket_sndp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    self.socket_sndp.setblocking(0)
    self.socket_sndp.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    self.sndp_request = chr(56) + chr(0) + chr(0x5A) + chr(0xA5) + chr(0) * 52
    self.sndp_active = self.conf.sndp_active
    # conf.rx_udp_port is used for returning ADC samples
    # conf.rx_udp_port + 1 is used for control
    self.rx_udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    self.rx_udp_socket.setblocking(0)
    self.rx_udp_socket.connect((self.conf.rx_udp_ip, self.conf.rx_udp_port + 1))
    return QS.open_rx_udp(self.conf.rx_udp_ip, self.conf.rx_udp_port)
  def close(self):
    if self.rx_udp_socket:
      self.rx_udp_socket.close()
      self.rx_udp_socket = None
  def ReturnFrequency(self):	# Return the current tuning and VFO frequency
    return None, None		# frequencies have not changed
  def ReturnVfoFloat(self):	# Return the accurate VFO as a float
    return float(self.rx_phase) * self.conf.rx_udp_clock / 2.0**32
  def ChangeFrequency(self, tx_freq, vfo_freq, source='', band='', event=None):
    if vfo_freq != self.vfo_frequency:
      self.vfo_frequency = vfo_freq
      self.rx_phase = int(float(vfo_freq) / self.conf.rx_udp_clock * 2.0**32 + 0.5) & 0xFFFFFFFF
    if tx_freq and tx_freq > 0:
      self.tx_frequency = tx_freq
      tx = tx_freq
      self.tx_phase = int(float(tx) / self.conf.rx_udp_clock * 2.0**32 + 0.5) & 0xFFFFFFFF
    self.NewUdpStatus()
    return tx_freq, vfo_freq
  def ChangeMode(self, mode):
    # mode is a string: "USB", "AM", etc.
    self.mode = mode
    self.tx_control &= ~0x03	# Erase last two bits
    if self.vna_count:
      pass
    elif mode in ("CWL", "CWU"):
      self.tx_control |= 0x01
    elif mode in ("USB", "LSB", "AM", "FM"):
      self.tx_control |= 0x02
    elif mode[0:4] == 'DGT-':
      self.tx_control |= 0x02
    elif mode[0:3] == 'IMD':
      self.tx_control |= 0x02
    self.SetTxLevel()
  def ChangeBand(self, band):
    # band is a string: "60", "40", "WWV", etc.
    self.band = band
    self.HiQSDR_Connector_X1 &= ~0x0F	# Mask in the last four bits
    self.HiQSDR_Connector_X1 |= self.conf.HiQSDR_BandDict.get(band, 0) & 0x0F
    self.SetTxLevel()
  def SetTxLevel(self):
    # As tx_level varies from 50 to 200, the output level changes from 263 to 752 mV
    # So 0 to 255 is 100 to 931, or 1.0 to 9.31; v = 1.0 + 0.0326 * level
    if not self.vna_count:
      try:
        self.tx_level = self.conf.tx_level[self.band]
      except KeyError:
        self.tx_level = self.conf.tx_level[None]		# The default
      if self.mode[0:4] == 'DGT-':
        reduc = self.application.digital_tx_level
      else:
        reduc = self.application.tx_level
      if reduc < 100:				# reduce power by a percentage
        level = 1.0 + self.tx_level * 0.0326
        level *= math.sqrt(reduc / 100.0)      # Convert from a power to an amplitude
        self.tx_level = int((level - 1.0) / 0.0326 + 0.5)
        if self.tx_level < 0:
          self.tx_level = 0
    self.NewUdpStatus()
  def OnButtonRfGain(self, event):
    # The HiQSDR attenuator is five bits: 2, 4, 8, 10, 20 dB
    btn = event.GetEventObject()
    n = btn.index
    self.HiQSDR_Connector_X1 &= ~0x10	# Mask in the preamp bit
    if n == 0:		# 0dB
      self.HiQSDR_Attenuator = 0
      self.rf_gain = 0
    elif n == 1:	# +10
      self.HiQSDR_Attenuator = 0
      self.HiQSDR_Connector_X1 |= 0x10
      self.rf_gain = 10
    elif n == 2:	# -10
      self.HiQSDR_Attenuator = 0x08
      self.rf_gain = -10
    elif n == 3:	# -20
      self.HiQSDR_Attenuator = 0x10
      self.rf_gain = -20
    elif n == 4:	# -30
      self.HiQSDR_Attenuator = 0x18
      self.rf_gain = -30
    else:
      self.HiQSDR_Attenuator = 0
      self.rf_gain = 0
      print ('Unknown RfGain')
    self.NewUdpStatus()
  def OnButtonPTT(self, event):
    # This feature requires firmware version 1.1 or higher
    if self.firmware_version:
      btn = event.GetEventObject()
      if btn.GetValue():		# Turn the software key bit on or off
        self.tx_control |= 0x08
      else:
        self.tx_control &= ~0x08
      self.NewUdpStatus(True)	# Prompt update for PTT
  def OnButtonAntenna(self, event):
    # This feature requires extended IO
    btn = event.GetEventObject()
    if btn.index:
      self.HiQSDR_Bits |= 0x01
    else:
      self.HiQSDR_Bits &= ~0x01
    self.NewUdpStatus()
  def HeartBeat(self):
    if self.sndp_active:	# AE4JY Simple Network Discovery Protocol - attempt to set the FPGA IP address
      try:
        self.socket_sndp.sendto(self.sndp_request, (self.broadcast_addr, 48321))
        data = self.socket_sndp.recv(1024)
        # print(repr(data))
      except:
        # traceback.print_exc()
        pass
      else:
        if len(data) == 56 and data[5:14] == 'HiQSDR-v1':
          ip = self.conf.rx_udp_ip.split('.')
          t = (data[0:4] + chr(2) + data[5:37] + chr(int(ip[3])) + chr(int(ip[2])) + chr(int(ip[1])) + chr(int(ip[0]))
               + chr(0) * 12 + chr(self.conf.rx_udp_port & 0xFF) + chr(self.conf.rx_udp_port >> 8) + chr(0))
          # print(repr(t))
          self.socket_sndp.sendto(t, (self.broadcast_addr, 48321))
    try:	# receive the old status if any
      data = self.rx_udp_socket.recv(1024)
      if DEBUG:
        self.PrintStatus(' got ', data)
    except:
      pass
    else:
      if data[0:2] == 'St':
        self.got_udp_status = data
    if self.firmware_version is None:		# get the firmware version
      if self.want_udp_status[0:13] != self.got_udp_status[0:13]:
        try:
          self.rx_udp_socket.send(self.want_udp_status)
          if DEBUG:
            self.PrintStatus('Start', self.want_udp_status)
        except:
          pass
      else:		# We got a correct response.
        self.firmware_version = ord(self.got_udp_status[13])	# Firmware version is returned here
        if DEBUG:
          print ('Got version',  self.firmware_version)
        if self.firmware_version > 0 and self.conf.use_rx_udp == 2:
          self.tx_control |= 0x04	# Use extra control bytes
        self.sndp_active = False
        self.NewUdpStatus()
    else:
      if self.want_udp_status != self.got_udp_status:
        if DEBUG:
          self.PrintStatus('Have ', self.got_udp_status)
          self.PrintStatus(' send', self.want_udp_status)
        try:
          self.rx_udp_socket.send(self.want_udp_status)
        except:
          pass
      elif DEBUG:
        self.rx_udp_socket.send('Qs')
  def PrintStatus(self, msg, string):
    print (msg, ' ', end=' ')
    print (string[0:2], end=' ')
    for c in string[2:]:
      print ("%2X" % ord(c), end=' ')
    print ()
  def GetFirmwareVersion(self):
    return self.firmware_version
  def OnSpot(self, level):
    pass
  def OnBtnFDX(self, is_fdx):   # Status of FDX button, 0 or 1
    if is_fdx:
      self.HiQSDR_Connector_X1 |= 0x20     # Mask in the FDX bit
    else:
      self.HiQSDR_Connector_X1 &= ~0x20
    self.NewUdpStatus()
  def VarDecimGetChoices(self):		# return text labels for the control
    clock = self.conf.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.conf.rx_udp_clock // rate + 0.5))
        self.index = self.decimations.index(dec)
      except:
        try:
          self.index = self.decimations.index(self.conf.rx_udp_decimation)
        except:
          self.index = 0
    else:
      self.index = index
    dec = self.decimations[self.index]
    self.rx_control = dec // 64 - 1		# Second stage decimation less one
    self.NewUdpStatus()
    return int(float(self.conf.rx_udp_clock) / dec + 0.5)
  def VarDecimRange(self):
    return (48000, 960000)
  def NewUdpStatus(self, do_tx=False):
    s = "St"
    s = s + struct.pack("<L", self.rx_phase)
    s = s + struct.pack("<L", self.tx_phase)
    s = s + chr(self.tx_level) + chr(self.tx_control)
    s = s + chr(self.rx_control)
    if self.firmware_version:	# Add the version
      s = s + chr(self.firmware_version)	# The firmware version will be returned
      if self.tx_control & 0x04:	# Use extra HiQSDR control bytes
        s = s + chr(self.HiQSDR_Connector_X1)
        s = s + chr(self.HiQSDR_Attenuator)
        s = s + chr(self.HiQSDR_Bits)
        s = s + chr(0)
      else:
        s = s + chr(0) * 4
      s = s + struct.pack("<H", self.vna_count)
      s = s + chr(0) * 2
    else:		# firmware version 0 or None
      s = s + chr(0)	# assume version 0
    self.want_udp_status = s
    if do_tx:
      try:
        self.rx_udp_socket.send(s)
      except:
        pass
  def SetVNA(self, key_down=None, vna_start=None, vna_stop=None, vna_count=None, do_tx=False):
    if key_down is None:
      pass
    elif key_down:
      self.tx_control |= 0x08
    else:
      self.tx_control &= ~0x08
    if vna_count is not None:
      self.vna_count = vna_count	# Number of scan points
    if vna_start is not None:	# Set the start and stop frequencies.  The tx_phase is the frequency delta.
      self.rx_phase = int(float(vna_start) / self.conf.rx_udp_clock * 2.0**32 + 0.5) & 0xFFFFFFFF
      self.tx_phase = int(float(vna_stop - vna_start) / self.vna_count / self.conf.rx_udp_clock * 2.0**32 + 0.5) & 0xFFFFFFFF
    self.tx_control &= ~0x03	# Erase last two bits
    self.rx_control = 40 - 1
    self.tx_level = 255
    self.NewUdpStatus(do_tx)
    start = int(float(self.rx_phase) * self.conf.rx_udp_clock / 2.0**32 + 0.5)
    stop = int(start + float(self.tx_phase) * self.vna_count * self.conf.rx_udp_clock / 2.0**32 + 0.5)
    return start, stop		# return the start and stop frequencies after integer rounding
