#!/usr/bin/python3
#
# RPi python program to control a DDS-VFO for a TS120S
# - via keyboard and mouse
# - via an IR remote control
# - via TCPIP using hamlib protocol (e.g. from WSJT-X app)
# - via bluetooth RFCOMM using subset of ft818 protocol (e.g. from FT8CN app)
#
# Implementation for a dds9850 or dds9833 module
# Copyright (C) Herbert Hanewinkel
#
# Version 3.0
# handling of multiple connections
# Integrated Bluetooth agent for pairing added:
# sspmode=0 with fixed PIN worked only to Win7 but failed to Android 9 and 15
# therefore sspmode=1  with 'DisplayYesNo' is implemented.
# Button KEY_RADIO on remote sets discoverable, on a paring request from
# client a code is send to display and must be confirmed by KEY_OK.
# The agent requires an extra thread for listening to DBus events.
# The thread could run permanent, but it is implemented to started only with
# 'discoverable on' and terminated on succesfull pairing or by timeout
# To set a bluetooth device discoverable requires root or access to sdp socket
# /run/sdp for normal users: chmod o+rw /run/sdp
#
# Version 2.0
# Support for Bluetooth client added using commands
# from yaesu ft818 cat control
#
# RPi isolated by 3x ADUM1201 boards from DDS module/TRX
# If the input of an ADUM1201 ic is unpowered the output is high,
# therefore the logic of the TRX line had to be inverted:
# high = TRX unpowered/off, low = TRX powered/on
#
# Version 1.1
# Support for mouse events added
# mouse wheel changes frequency up/down
# mouse left/right button change stepsize

# HamlibHandler code adapted from: 
  # hamlibserver.py
  # This software is Copyright (C) 2012 by James C. Ahlstrom, and is
  # licensed for use under the GNU General Public License (GPL).

import RPi.GPIO as GPIO
import sys
import evdev
import signal, os
import socket
import string
import subprocess
import threading
import dbus
import dbus.service
import dbus.mainloop.glib
from gi.repository import GLib

from evdev import ecodes
from select import select
from os.path import exists
from time import sleep

import bluetooth                  # requires pybluez

########################################################################
# DDS to RPi GPIO connections (plus ground):
FSYNC   = 9                       # GPIO-OUT SPI sync
SCLK    = 10                      # GPIO-OUT SPI clock
SDAT    = 25                      # GPIO-OUT SPI data
PINS    = [FSYNC, SCLK, SDAT]
DDSCHIP = 9850                    # type of dds chip, 9833 or 9850

# TRX to RPi GPIO connections (plus ground)
PTT      = 26                     # GPIO-OUT high for transmit
TRX_ON   = 13                     # GPIO-IN  low on TRX powered, high unpowered
TX_ON    = 5                      # GPIO-IN  high on transmit
TX_CW    = 6                      # GPIO-IN  high on CW transmit

# TRX specific values
# The TS120S vfo has a 500kHz range from 5.5 to 6. Mhz
# The TS120S has a mechanical three position mode switch
# and a nine position band switch, no connector for electronic
# readout of the switch positions.
FREQ_MIN   = 5500000              # min vfo freq
FREQ_MAX   = 6000000              # max vfo freq
MODE       = ["CW", "USB", "LSB"] # supported modes
NFWIDTH    = 2400
#supported bands
BAND       = [3500000, 7000000, 14000000, 21000000, 28000000, 28500000,
              29000000, 29500000]

# constant values
FREQ_FT8   = 5574000              # memory presets
FREQ_PSK   = 5580000
FREQ_4     = 5600000
FREQ_5     = 5800000
CW_TXSHIFT = 700                  # cw tx offset in hz
STEPS      = [5000, 10, 100, 1000, 10000]
LOCKFILE   = '/run/lock/dds'      # lock file
FREQFILE   = '/home/pi/lastfreq'  # file for saving frequency and state

# constants for hamlib connection
PORT       = 4575                 # default tcp port of hamlib protocol

# to make bluetooth client happy
FT_DIG     = 0xA
FT_PKT     = 0xC
BT_DEVICE  = 'hci0'
BT_ADAPTER = 'org.bluez.Adapter1'
BT_UUID    = "9c6c0bb2-3a90-4f96-9241-665f045a22e0"
BT_NAME    = "TS120S-dds"

# instances of classes
dds          = None
bt_socket    = None
bt_agent     = None

rns          = 1             # 1 = mainloop running, 0 = quit from mainloop

########################################################################
# state of dds vfo

class DDS:
    def __init__(self):
        # DDS-VFO data structure
        self.dignum      = 0           # number created from digits
        self.digidx      = 0           # digit counter
        self.save        = 0           # used by 2-key seq
        self.memfreq     = [FREQ_MIN,FREQ_FT8,FREQ_PSK,FREQ_4,FREQ_5]
        self.act         = 0           # index of active vfo
        self.band        = 2           # index of selected band
        self.step        = 2           # index of tuning steps
        self.txi         = 0           # on split op, index of tx vfo
        self.ptt         = 0           # internal ptt on/off
        self.txon        = 0           # state of trx: tx on/off
        self.cwshift     = 0           # cw shift active
        self.splitmode   = 0           # split vfo for rx/tx
        self.vfofreq     = [FREQ_FT8, FREQ_MIN]
        self.freq        = [BAND[self.band], BAND[self.band]]
        self.vfo         = ["VFOA", "VFOB"]
        self.last        = self.vfofreq[0]
        self.mode        = 1           # index of mode
  
        # fixed value for hamlb requests
        self.bandwidth   = NFWIDTH     # NF bandwidth of TRX

        # ft8xx fake values for bluetooth client
        self.ftmode      = 0
        self.lock        = 0
        self.clar        = 0

    def Init(self):
       return self

########################################################################
#   Low-level RPi GPIO routines
#   These routines access GPIO directly

def SetPin(pin, value):
    #sets the GPIO pin to desired value (1=on, 0=off)
    GPIO.output(pin, value)

def InitIO():
    GPIO.setmode(GPIO.BCM)
    GPIO.setwarnings(False)
    for pin in PINS:
        GPIO.setup(pin,GPIO.OUT)
        SetPin(pin, 0)

    GPIO.setup(PTT, GPIO.OUT)        # PTT
    SetPin(PTT, 0)
    GPIO.setup(TRX_ON, GPIO.IN)     # TRX power on/off
    GPIO.setup(TX_ON, GPIO.IN)      # TX on/off
    GPIO.setup(TX_CW, GPIO.IN)      # TX CW on/off

########################################################################
#   DDS routines:

def BeginDDS():
    #Start with FSYNC & SCLK lines high
    SetPin(FSYNC,1)
    SetPin(SCLK,1)

def FinalDDS():
    #Set SCLK, SDAT, FSYNC lines low to avoid output current
    SetPin(SCLK,0)
    SetPin(SDAT,0)
    SetPin(FSYNC,0)

def PulseClock():       #pulses the DDS serial clock line LOW
    SetPin(SCLK,0)      #bit clocked on high-low transition
    SetPin(SCLK,1)      #no delay since python is slow

# -------------------------------------------------------------
#Send data word = 16 serial bits to DDS9833

#FSYNC is low for duration of data transfer
#SCLK is pulsed low to clock in each bit

def Shift16 (data):
    SetPin(FSYNC,0);                  #enable data input to DDS
    for b in range(16):               #loop for 16 data bits
        value = data & 0x8000         #look at left-most bit
        SetPin(SDAT,value)            #puts its value on data line
        data <<= 1                    #shift data bits to left
        data &= 0xFFFF                #limit data to 16 bits
        PulseClock()                  #clock in the data bit
    SetPin(FSYNC,1)                   #brink FSYNC high after word sent

def OutputReg(i):                     # 9833 has two output registers
    BeginDDS()
    if (i == 0): Shift16(0x2000)      #sends DDS register0 to output
    else:        Shift16(0x2800)      #sends DDS register1 to output
    FinalDDS()

def Reset9833():
    BeginDDS()
    Shift16(0x2100)                   #this is DDS reset command
    Shift16(0x2000)                   #sends DDS register0 to output
    FinalDDS()

def SetFreq9833(i, hz):
    #input = frequency in Hz;  result: sends command to DDS
    #this routine converts requested Hz into a DDS register value
    #the conversion factor depends on the master oscillator input
    #If DDS uses 25 MHz oszi, factor = 2^28/25,000,000

    factor = 10.73741824        #assumes 25 Mhz oscillator
    reg = int(hz * factor)      #convert Hz to DDS register value

    #divide 28-bit register value into 14-bit halves
    regLo = reg & 0x3FFF
    regHi = reg >> 14

    if (i == 0):                # prefix each half with reg0 command
        regLo |= 0x4000
        regHi |= 0x4000
    else:                       # prefix each half with reg1 command
        regLo |= 0x8000
        regHi |= 0x8000

    BeginDDS()
    Shift16(regLo)              #send both halves to the DDS
    Shift16(regHi)
    FinalDDS()
##    print("Freq: ", i, hz)

# --------------------------------------------------------------
# Send data word = 40 bits to DDS9850

def ShiftControl(data):
    for b in range(8):          #loop for 8 bits
        data &= 0xff            # mask out other bits
        value = data & 0x1      #look at left-most bit
        SetPin(SDAT,value)      #puts its value on data line
        data >>= 1              #shift data bits to right
        PulseClock()            #clock in the data bit

def Shift40(data, control):
    BeginDDS()
    SetPin(FSYNC,0)             #enable data input to DDS
    for b in range(32):         #loop for 32 data bits
        value = data & 0x1      #look at left-most bit
        SetPin(SDAT,value)      #puts its value on data line
        data >>= 1              #shift data bits to right
        PulseClock()            #clock in the data bit
    ShiftControl(control)       #append control byte
    SetPin(FSYNC,1)             #brink FSYNC high after word sent
    FinalDDS()

def Reset9850():
    BeginDDS()
    SetPin(FSYNC,0)             #enable data input to DDS
    ShiftControl(0)             #this is DDS reset command
    SetPin(FSYNC,1)             #brink FSYNC high after word sent
    FinalDDS()

def SetFreq9850(hz):
    #input = frequency in Hz;  result: sends command to DDS
    #this routine converts requested Hz into a DDS register value
    #the conversion factor depends on the master oscillator input
    #factor = 2^32/125,000,000

    factor = 34.35973837        #assumes 125 Mhz oscillator
    reg = int(hz * factor)      #convert Hz to DDS register value
    Shift40(reg, 0)
##    print("Freq: ", hz)

# --------------------------------------------------------------
# common function for 9833 and 9850

def ResetDDS():
    if (DDSCHIP == 9833): Reset9833()
    else:                 Reset9850() 

# dds9833 chip has two output registers

def SetDDS(i, hz):
    if (DDSCHIP == 9833):
        if (len(dds.vfo) == 2):
            SetFreq9833(i, hz)
            OutputReg(i)
        else:
            SetFreq9833(0, hz)
            OutputReg(0)
    else:
        SetFreq9850(hz)

# switch dds9833 output register only

def ChgDDS(i, hz):
    if (DDSCHIP == 9833 and len(dds.vfo) == 2): OutputReg(i)
    else:                                       SetDDS(0, hz)

# --------------------------------------------------------------
# create a lock file to inhibit running multiple instances
# write current vfo+freq+state to this file
# file contents is read and displayed by RPi status display script

def ToFile(s):
    with open(LOCKFILE, "w") as fd:
        fd.write(s)
        fd.close()

def LockDDS(trx):
    i = trx.act
    trx.freq[i] = trx.vfofreq[i] - FREQ_MIN + BAND[trx.band]
    if (trx.txon):
        s = "TX: "
        if (trx.splitmode == 1): i = trx.txi
    else:
        s = "RX: "
    s += trx.vfo[i][-1]
    if (trx.splitmode == 1): s += "s "
    else:                    s += " "
    try:
        # to get a string format of 00.000.00
        s += (f"{trx.freq[i]:,}")[0:-1]
    except:
        pass

    s += " " + MODE[trx.mode][0:1]
    s += f"{(trx.step):}"
##    print(s)
    ToFile(s)

# --------------------------------------------------------------
# load/save current vfo+freq+state to FREQFILE

def LoadState(trx):
    """load VFO state from file"""
    try:
        with open(freqfile, "r") as fd:
            s = fd.read()
            fd.close()
        x = s.split(" ", 6)
        trx.vfofreq[0] = int(x[0])  # VFOA freq
        trx.vfofreq[1] = int(x[1])  # VFOB freq
        if (trx.vfofreq[0] < FREQ_MIN or trx.vfofreq[0] > FREQ_MAX):
            trx.vfofreq[0] = FREQ_MIN
        if (trx.vfofreq[1] < FREQ_MIN or trx.vfofreq[1] > FREQ_MAX):
            trx.vfofreq[1] = FREQ_MIN

        trx.act        = int(x[2])  # active VFO
        if (trx.act >= len(trx.vfo)): trx.act = 0

        trx.txi        = int(x[3])  # split VFO
        if (trx.txi >= len(trx.vfo)): trx.txi = trx.act + 1
        if (trx.txi >= len(trx.vfo)): trx.txi = 0

        trx.splitmode  = int(x[4])  # split mode
        trx.band       = int(x[5])  # band
        if (trx.band > len(BAND)): trx.band = 0
        trx.mode       = int(x[6])  # mode
        if (trx.mode > len(MODE)): trx.mode = 1
    except:
        pass

def SaveState(trx):
    """save VFO state to file"""
    s = f"{trx.vfofreq[0]} " + f"{trx.vfofreq[1]} " + f"{trx.act} " + f"{trx.txi} " + f"{trx.splitmode} " + f"{trx.band} " + f"{trx.mode}"
    try:
        with open(FREQFILE, "w") as fd:
            fd.write(s)
            fd.close()
    except:
        pass

# --------------------------------------------------------------
# VFO functions

def ToggleVFO(trx):
    trx.last = trx.vfofreq[trx.act]
    trx.act += 1
    if (trx.act >= len(trx.vfo)): trx.act = 0
    if (trx.splitmode == 1):
        trx.txi = trx.act + 1
        if (trx.txi >= len(trx.vfo)): trx.txi = 0
    ChgDDS(trx.act, trx.vfofreq[trx.act])
    LockDDS(trx)

def OnPTT(trx, s):
    t = trx.ptt
    trx.ptt = s
    if (trx.splitmode == 1):
        if (trx.ptt): i = trx.txi
        else:        i = trx.act
        ChgDDS(i, trx.vfofreq[i])
    LockDDS(trx)
    GPIO.output(PTT, trx.ptt)
    if ((t and s) or (t == 0 and s == 0)): t = 0xf0
    else:                                 t = 0
    return t

def OnSplit(trx, s):
    t = trx.splitmode
    trx.splitmode = s
    if (trx.splitmode):
        trx.txi = trx.act + 1
        if (trx.txi >= len(trx.vfo)): trx.txi = 0
    else:
        trx.txi = trx.act
    LockDDS(trx)
    if ((t and s) or (t == 0 and s == 0)): t = 0xf0
    else:                                  t = 0
    return t

def ChgFreq(trx, value):
    """change frequency up or down"""
    i = trx.act
    trx.vfofreq[i] += value
    if (trx.vfofreq[i] > FREQ_MAX): trx.vfofreq[i] = FREQ_MAX
    if (trx.vfofreq[i] < FREQ_MIN): trx.vfofreq[i] = FREQ_MIN

def MemFreq(trx, n):
    """save or load frequency to/from memory"""
    if (trx.save == 1):
        trx.memfreq[n] = trx.vfofreq[trx.act]
    else:
        trx.last = trx.vfofreq[trx.act]
        trx.vfofreq[trx.act] = trx.memfreq[n]

def UpdateBand(trx, x, i):
     """change band index based on frequency"""
     x = x - trx.vfofreq[i] + FREQ_MIN
     try:
         trx.band = BAND.index(x)
     except:
         pass

########################################################################
# handler for GPIO signals

def trxOnOff(pin):
    """GPIO event handler for trx on/off event"""
    sleep(0.05)
    trx = not GPIO.input(TRX_ON)
    if (trx):
        ResetDDS()
        LoadState(dds)
        SetDDS(dds.act, dds.vfofreq[dds.act])
        LockDDS(dds)
    else:
        SaveState(dds)
##    print("TRX:", trx, dds.vfofreq[dds.act])

def txOnOff(pin):
    """GPIO event handler for tx on/off event"""
    sleep(0.05)
    if (GPIO.input(TRX_ON)): return           # ignore if trx switched off 
    dds.txon = GPIO.input(TX_ON)
##    print("TX:", dds.txon, dds.ptt)
    if (dds.ptt == 0 and dds.splitmode == 1): # excute on ext ptt change only
        if (dds.txon): i = dds.txi
        else:         i = dds.act
        ChgDDS(i, dds.vfofreq[i])
    LockDDS(dds)

def cwOnOff(pin):
    """GPIO event handler for tx cw on/off event"""
    sleep(0.05)
    if (GPIO.input(TRX_ON)): return           # ignore if trx switched off 
    cw = GPIO.input(TX_CW)
##    print("TXcw:", cw)
    if (dds.splitmode == 0):
        if (cw and dds.cwshift == 0):
            i = dds.act
            dds.vfofreq[i] += CW_TXSHIFT
            dds.cwshift = 1
            SetDDS(i, dds.vfofreq[i])
            LockDDS(dds)
        elif (cw == 0 and dds.cwshift):
            i = dds.act
            dds.vfofreq[i] -= CW_TXSHIFT
            dds.cwshift = 0
            SetDDS(i, dds.vfofreq[i])
            LockDDS(dds)

# --------------------------------------------------------------
# handler for signals

def sighandler(signum, frame):
    global rns

    SaveState(dds)
    try:
        bt_agent.stop()
        GPIO.remove_event_detect(TX_ON)
        GPIO.cleanup()
        os.remove(LOCKFILE)
    except:
        pass

    rns = 0
    raise SystemExit

# --------------------------------------------------------------
# handling of mouse wheel events

def OnWheel(trx, value):
    ChgFreq(trx, value * STEPS[trx.step])
    SetDDS(trx.act, trx.vfofreq[trx.act])
    LockDDS(trx)

# --------------------------------------------------------------
# handling of IR/key events

def OnNumKey(trx, num):
    """handle digits from remote control, 3-digits for new khz value"""
    if (trx.digidx == 0):
        trx.dignum = num * 100 
        trx.digidx = 1
    elif (trx.digidx == 1):
        trx.dignum += num * 10 
        trx.digidx = 2
    else:
        trx.dignum += num 
        # map 500-999 to 0-499
        if (trx.dignum > 500): trx.dignum -= 500
        trx.last = trx.vfofreq[trx.act]
        trx.vfofreq[trx.act] = trx.dignum * 1000 + FREQ_MIN
        trx.digidx = -1

def OnKey(trx, key):
    """handler for input keys (lirc is used for IR input)"""
    try:
        if (bt_agent.confirm(key)):       # confirm or reject new BT client
            LockDDS(trx)
            return
    except:
        pass

    if (trx.save > 0): trx.save -= 1       # decrement save flag

    # A sequence of 3 numeric keys sets a new kHz frequency value
    # any other key resets the 3 digit counter

    if (key == ecodes.KEY_0): OnNumKey(trx, 0)
    elif (key == ecodes.KEY_1): OnNumKey(trx, 1)
    elif (key == ecodes.KEY_2): OnNumKey(trx, 2)
    elif (key == ecodes.KEY_3): OnNumKey(trx, 3)
    elif (key == ecodes.KEY_4): OnNumKey(trx, 4)
    elif (key == ecodes.KEY_5): OnNumKey(trx, 5)
    elif (key == ecodes.KEY_6): OnNumKey(trx, 6)
    elif (key == ecodes.KEY_7): OnNumKey(trx, 7)
    elif (key == ecodes.KEY_8): OnNumKey(trx, 8)
    elif (key == ecodes.KEY_9): OnNumKey(trx, 9)
    else:
        # reset digit counter on ALL non Num keys
        trx.digidx = 0

    # numeric input >0 in progress, < 0 complete
    if (trx.digidx > 0): return
    elif (trx.digidx < 0): trx.digidx = 0

    # Colored keys on remote control are used for freq memory
    # menu  + <X> key stores the frequency, <X> key loads frequency
    # pressing other keys cancel a store frequency operation
    # start new 2 key sequence, use MENU+MENU to cancel
    # only if last key was menu key trx.save == 1 else 0
    elif (key == ecodes.KEY_MENU):
        if (trx.save == 0): trx.save = 2
    elif (key == ecodes.KEY_TITLE): MemFreq(trx, 0)
    elif (key == ecodes.KEY_SUBTITLE): MemFreq(trx, 1)
    elif (key == ecodes.KEY_INFO): MemFreq(trx, 2)
    elif (key == ecodes.KEY_EPG): MemFreq(trx, 3)
    elif (key == ecodes.KEY_TEXT): MemFreq(trx, 4)

    # UP/DOWN key: frequency up or down depending on actual step size
    elif (key == ecodes.KEY_UP): ChgFreq(trx, STEPS[trx.step])
    elif (key == ecodes.KEY_DOWN): ChgFreq(trx, -STEPS[trx.step])

    # LEFT/RIGHT key, LEFT/RIGHT mouse button: select step size
    elif (key == ecodes.KEY_RIGHT or key == ecodes.BTN_LEFT):
        trx.step += 1
        if (trx.step >= len(STEPS)): trx.step = len(STEPS) -1
        LockDDS(trx)
        return
    elif (key == ecodes.KEY_LEFT or key == ecodes.BTN_RIGHT):
        trx.step -= 1
        if (trx.step < 1): trx.step = 1
        LockDDS(trx)
        return

    # CHANNELUP/CHANNELDOWN key:
    # frequncy up or down by STEPS[0] value)
    elif (key == ecodes.KEY_CHANNELUP): ChgFreq(trx, STEPS[0])
    elif (key == ecodes.KEY_CHANNELDOWN): ChgFreq(trx, -STEPS[0])

    # BACK key: back to previous frequncy
    elif (key == ecodes.KEY_BACK):
        tmp = trx.vfofreq[trx.act]
        trx.vfofreq[trx.act] = trx.last
        trx.last = tmp

    # FORWARD/REWIND key: select band
    elif (key == ecodes.KEY_FORWARD):
        trx.band += 1
        if (trx.band >= len(BAND)): trx.band = 0
        LockDDS(trx)
        return
    elif (key == ecodes.KEY_REWIND):
        trx.band -= 1
        if (trx.band < 0): trx.band = len(BAND) - 1
        LockDDS(trx)
        return

    # NEXT/LAST key: select mode
    elif (key == ecodes.KEY_NEXT):
        trx.mode += 1
        if (trx.mode >= len(MODE)): trx.mode = 0
        LockDDS(trx)
        return
    elif (key == ecodes.KEY_LAST):
        trx.mode -= 1
        if (trx.mode < 0): trx.mode = len(MODE) - 1
        LockDDS(trx)
        return

    # OK key, middle mouse button: toggle VFO
    elif (key == ecodes.KEY_OK or key == ecodes.BTN_MIDDLE):
        ToggleVFO(trx)
        return

    # RECORD key: toggle ptt
    elif (key == ecodes.KEY_RECORD):
        if (trx.ptt == 1): OnPTT(trx, 0)
        else:             OnPTT(trx, 1)
        return

    # PAUSE key: toggle split mode
    # MENU+PAUSE key sequence: sets tx vfo freq to rx vfo freq
    elif (key == ecodes.KEY_PAUSE):
        if (trx.splitmode == 1): OnSplit(trx, 0)
        else:                   OnSplit(trx, 1)
 
        if (trx.save == 1 and trx.splitmode == 1):
            trx.vfofreq[trx.txi] = trx.vfofreq[trx.act]
            LockDDS(trx)
        return

    # TUNER terminate this VFO program
    elif (key == ecodes.KEY_TUNER):
        # TUNER key is used by lirc to start this VFO program
        # delay exit, that the new instance fails
        sleep(2.0)
        sighandler(0,0)
        return

    # RADIO make discoverable and start agent for pairing
    elif (key == ecodes.KEY_RADIO):
        if (bt_agent.discoverable(1)):
             print('BT discoverable on')
             ToFile(' BT discoverable')
             bt_agent.start()
        return

    else:
        print("key unused", key)
        return

    if (trx.save): return      # no update required on 2 key sequence

    # update DDS with new VFO frequency
    SetDDS(trx.act, trx.vfofreq[trx.act])
    LockDDS(trx)

########################################################################
# handling of requests from hamlib clients

class HamlibHandler:
    SingleLetters = {       # convert single-letter commands to long commands
        'f':'freq',
        'm':'mode',
        't':'ptt',
        'v':'vfo',
        's':'split_vfo',
        'i':'split_freq',
        'x':'split_mode',
        'q':'quit'
    }

    def __init__(self, app, conn, addr):
        self.app = app                # Reference back to the "hardware"
        self.conn = conn
        self.addr = addr
        self.rcvd = b''
        h = self.Handlers = {}
        h[''] = self.ErrProtocol
        h['chk_vfo']         = self.ChkVfo
        h['dump_state']      = self.DumpState
        h['get_freq']        = self.GetFreq
        h['set_freq']        = self.SetFreq
        h['get_mode']        = self.GetMode
        h['set_mode']        = self.SetMode
        h['get_vfo']         = self.GetVfo
        h['set_vfo']         = self.SetVfo
        h['get_ptt']         = self.GetPtt
        h['set_ptt']         = self.SetPtt
        h['get_split_vfo']   = self.GetSplitVfo
        h['set_split_vfo']   = self.SetSplitVfo
        h['get_split_freq']  = self.GetSplitFreq
        h['set_split_freq']  = self.SetSplitFreq
        h['get_split_mode']  = self.GetSplitMode
        h['set_split_mode']  = self.SetSplitMode

    def Send(self, text):
        """Send text back to the client."""
        try:
            return self.conn.sendall(bytearray(text.encode()))
        except socket.error:
            return False
  
    def Reply(self, *args):        # args is name, value, name, value, ..., int
        """Create a string of name, value pairs, and an ending integer code."""
        if self.extended:          # Use extended format
            # Extended format echoes command and parameters
            t = "%s:" % self.cmd
            for param in self.params:
                t = "%s %s" % (t, param)
            t += self.extended
            for i in range(0, len(args) - 1, 2):
                t = "%s%s: %s%c" % (t, args[i], args[i+1], self.extended)
            t += "RPRT %d\n" % args[-1]
        elif len(args) > 1:        # Use simple format
            t = ''
            for i in range(1, len(args) - 1, 2):
                t = "%s%s\n" % (t, args[i])
        else:                      # No names; just the required integer code
            t = "RPRT %d\n" % args[0]
        self.Send(t)

    def ErrParam(self):               # Invalid parameter
        self.Reply(-1)

    def UnImplemented(self):          # Command not implemented
        self.Reply(-4)

    def ErrProtocol(self):            # Protocol error
        self.Reply(-8)

    def Process(self):
        try:                          # Read any data from the socket
            rcvd = self.conn.recv(1024)
        except socket.error:
            return False
        if (len(rcvd) == 0): return False
        self.rcvd += rcvd

        while b'\n' in self.rcvd:
            # complete command with newline is available
            # packet could contain more than one command
            # Split off the command, save any further characters
            cmd, self.rcvd = self.rcvd.split(b'\n', 1)
            cmd = cmd.decode(errors = 'ignore')
            cmd = cmd.strip()
            if (len(cmd) == 0):  continue

            # Parse the command and call the appropriate handler
            if cmd[0] == '+':            # rigctld Extended Response Protocol
                self.extended = '\n'
                cmd = cmd[1:].strip()
            elif cmd[0] in ';|,':        # rigctld Extended Response Protocol
                self.extended = cmd[0]
                cmd = cmd[1:].strip()
            else:
                self.extended = None
    
            if cmd[0:1] == '\\':    # long form command starting with backslash
                args = cmd[1:].split()
                self.cmd = args[0]
                self.params = args[1:]
                self.Handlers.get(self.cmd, self.UnImplemented)()
            else:                                 # single-letter command
                self.params = cmd[1:].strip()
                cmd = cmd[0:1]
                try:
                    t = self.SingleLetters[cmd.lower()]
                except KeyError:
                    self.UnImplemented()
                else:
                    # on quit return False to close the connection
                    if (cmd == 'q'): return  False
          
                    if cmd in string.ascii_uppercase:
                        self.cmd = 'set_' + t
                    else:
                        self.cmd = 'get_' + t
                    self.Handlers.get(self.cmd, self.UnImplemented)()
        return True

# ---------------------------------------
# These are the handlers for each request

    def ChkVfo(self):
        self.Reply('ChkVFO', 0, 0)

    def DumpState(self):
        # prot_version\nrig_model\n0\n
        s = "0\n2\n\0\n"
        #rxstartf rxendf rxmodes rxlow_power rxhigh_power rxvfo rxant\n
        s += "3500000.000000 4000000.000000 0x4 -1 -1 0x10000003 0x3\n"
        s += "7000000.000000 7500000.000000 0x4 -1 -1 0x10000003 0x3\n"
        s += "14000000.000000 14500000.000000 0x4 -1 -1 0x10000003 0x3\n"
        s += "21000000.000000 21500000.000000 0x4 -1 -1 0x10000003 0x3\n"
        s += "28000000.000000 30000000.000000 0x4 -1 -1 0x10000003 0x3\n"
        s += "0 0 0 0 0 0 0\n"
        #txstartf txendf txmodes txlow_power txhigh_power txvfo txant\n
        s += "3500000.000000 4000000.000000 0x4 -1 -1 0x10000003 0x3\n"
        s += "7000000.000000 7500000.000000 0x4 -1 -1 0x10000003 0x3\n"
        s += "14000000.000000 14500000.000000 0x4 -1 -1 0x10000003 0x3\n"
        s += "21000000.000000 21500000.000000 0x4 -1 -1 0x10000003 0x3\n"
        s += "28000000.000000 30000000.000000 0x4 -1 -1 0x10000003 0x3\n"
        s += "0 0 0 0 0 0 0\n"
        #modes tuningsteps
        s += "0 0\n"
        #modes bandwidth
        s += "0 0\n"
        #max_rit\nmax_xit\nmax_ifshift\nannounces\n
        #preamp1 ...\n
        #attenuator1 ...\n
        #has_get_func\nhas_set_func\nhas_get_level\nhas_set_level\n
        #has_get_parm\nhas_set_parm\n
        s += "0\n0\n0\n\0\n\0\n\0\n0\n0\n0\n\0\n\0\n\0\n"
        self.Send(s)

    def GetFreq(self):
        self.Reply('Frequency', self.app.freq[self.app.act], 0)

    def SetFreq(self):
        try:
            x = float(self.params)
            self.Reply(0)
        except:
            self.ErrParam()
        else:
            x = int(x + 0.5)
            i = self.app.act
            self.app.vfofreq[i] = (x % 500000) + FREQ_MIN
            SetDDS(i, self.app.vfofreq[i])
            UpdateBand(self.app, x, i)
            LockDDS(self.app)

    def GetMode(self):
        self.Reply('Mode', MODE[self.app.mode], 'Passband',
                    self.app.bandwidth, 0)

    def SetMode(self):
        try:
            mode, bw = self.params.split()
            bw = int(float(bw) + 0.5)
            self.Reply(0)
        except:
            self.ErrParam()
        else:
            if (mode == 'CW'): self.app.mode = 0
            elif (mode == 'USB'): self.app.mode = 1
            elif (mode == 'LSB'): self.app.mode = 2
            self.app.bandwidth = bw

    def GetVfo(self):
        self.Reply('VFO', self.app.vfo[self.app.act], 0)

    def SetVfo(self):
        try:
            x = self.params.upper()
            self.Reply(0)
        except:
            self.ErrParam()
        else:
            try:
                i = self.app.vfo.index(x)
                self.app.act = i
                SetDDS(i, self.app.vfofreq[i])
                LockDDS(self.app)
            except:
                pass

    def GetPtt(self):
        self.Reply('PTT', self.app.ptt, 0)    # returns ptt state

    def SetPtt(self):
        try:
            x = int(self.params)
            self.Reply(0)
        except:
            self.ErrParam()
        else:
            OnPTT(self.app, x)

    def GetSplitVfo(self):
         self.Reply('SPLIT', self.app.splitmode, 'TXVFO',
                     self.app.vfo[self.app.txi], 0)

    def SetSplitVfo(self):
      try:
          splitmode, txvfo = self.params.split()
          self.Reply(0)
      except:
          self.ErrParam()
      else:
          self.app.splitmode = splitmode
          try:
              i = self.app.vfo.index(txvfo)
              self.app.txi = i
          except:
              pass

    def GetSplitFreq(self):
        self.Reply('TX Frequency', self.app.freq[self.app.txi], 0)

    def SetSplitFreq(self):
      try:
          x = float(self.params)
          self.Reply(0)
      except:
          self.ErrParam()
      else:
          x = int(x + 0.5)
          i = self.app.txi
          self.app.vfofreq[i] = (x % 500000) + FREQ_MIN
          if (DDSCHIP == 9833 and len(self.app.vfo) == 2):
              SetFreq9833(i, self.app.vfofreq[i])
          # cannot split with an another band, stay on current band
          # UpdateBand(self.app, x, i)
          self.app.freq[i] = self.app.vfofreq[i]-FREQ_MIN+BAND[self.app.band]

    def GetSplitMode(self):
        self.Reply('TX Mode', MODE[self.app.mode], 'TX Passband',
                    self.app.bandwidth, 0)

    def SetSplitMode(self):
        try:
            mode, bw = self.params.split()
            bw = int(float(bw) + 0.5)
            self.Reply(0)
        except:
            self.ErrParam()
        else:
            if (mode == 'CW'): self.app.mode = 0
            elif (mode == 'USB'): self.app.mode = 1
            elif (mode == 'LSB'): self.app.mode = 2
            self.app.bandwidth = bw

##########################################################################
# Handler, emulating a few commands from yaesu ft8x7 cat control

class Ft8xxHandler:
    def __init__(self, app, conn, addr):
        self.app = app
        self.conn = conn
        self.addr = addr
        self.rcvd = b''

    def Init(self):
        return self

    def Send(self, b):
        try:
            return self.conn.sendall(b)
        except:
            return False

    def Process(self):
        try:
            rcvd = self.conn.recv(100)
        except:
            return False
        if (len(rcvd) == 0): return False
        self.rcvd += rcvd

        while (len(self.rcvd) >= 5):    # ft8xx cmd complete
            data = self.rcvd[0:5]
            if (len(self.rcvd) > 5): self.rcvd = self.rcvd[5:-1]
            else:                    self.rcvd = b''
    
            cmd = data[4]
            if (cmd == 0):         # lock on
                s = self.app.lock
                self.app.lock = 1
                b = s.to_bytes(1, byteorder='big')
                continue
                
            elif (cmd == 0x80):    # lock off
                s = self.app.lock
                self.app.lock = 0
                b = s.to_bytes(1, byteorder='big')
                continue
        
            elif (cmd == 1):       # set frequency
                f0 = data[0]
                f1 = data[1]
                f2 = data[2]
                f3 = data[3]
                f  = (f3 & 0xf) * 10       + (f3 >> 4) * 100
                f += (f2 & 0xf) * 1000     + (f2 >> 4) * 10000
                f += (f1 & 0xf) * 100000   + (f1 >> 4) * 1000000
                f += (f0 & 0xf) * 10000000 + (f0 >> 4) * 100000000
                i = self.app.act
                if (self.app.splitmode == 1 and self.app.txon == 1):
                    i = self.app.txi
                self.app.vfofreq[i] = (f % 500000) + FREQ_MIN
                SetDDS(i, self.app.vfofreq[i])
                UpdateBand(self.app, f, i)
                LockDDS(self.app)
                continue
        
            elif (cmd == 2):         # split on
                s = OnSplit(self.app, 1)
                b = s.to_bytes(1, byteorder='big')
                continue
        
            elif (cmd == 0x82):      # split off
                s = OnSplit(self.app, 0)
                b = s.to_bytes(1, byteorder='big')
                continue
        
            elif (cmd == 3):         # read freq & mode
                i = self.app.act
                if (self.app.splitmode == 1 and self.app.txon == 1):
                    i = self.app.txi
                f   = int(self.app.freq[i])
                f0  = int(f / 100000000) << 4
                f   = int(f % 100000000)
                f0 |= int(f / 10000000)
                f   = int(f % 10000000)
                f1  = int(f / 1000000) << 4
                f   = int(f % 1000000)
                f1 |= int(f / 100000)
                f   = int(f % 100000)
                f2  = int(f / 10000) << 4
                f   = int(f % 10000)
                f2 |= int(f / 1000)
                f   = int(f % 1000)
                f3  = int(f / 100) << 4
                f   = int(f % 100)
                f3 |= int(f / 10)
        
                if (self.app.mode == 0):   m = 2   # CW
                elif (self.app.mode == 1): m = 1   # USB
                elif (self.app.mode == 2): m = 0   # LSB
                if (self.app.mode == 1):
                    if (self.app.ftmode == FT_DIG): m = FT_DIG
                    if (self.app.ftmode == FT_PKT): m = FT_PKT
                b = bytes([f0,f1,f2,f3,m])
        
            elif (cmd == 5):       # clarifier on
                s = self.app.clar
                self.app.clar = 1
                b = s.to_bytes(1, byteorder='big')
                continue
                
            elif (cmd == 0x85):    # clarfier off
                s = self.app.clar
                self.app.clar = 0
                b = s.to_bytes(1, byteorder='big')
                continue
        
            elif (cmd == 7):       # set mode
                self.app.ftmode = data[0]
                continue
        
            elif (cmd == 8):       # ptt on
                s = OnPTT(self.app, 1)
                b = s.to_bytes(1, byteorder='big')
                continue
        
            elif (cmd == 0x88):    # ptt off
                s = OnPTT(self.app, 0)
                b = s.to_bytes(1, byteorder='big')
                continue
        
            elif (cmd == 0x81):    # toggle vfo
                ToggleVFO(self.app)
                continue
        
            elif (cmd == 0xe7):                        # rx status
                s = 0
                b = s.to_bytes(1, byteorder='big')
        
            elif (cmd == 0xf7):                        # tx status
                s = 160 
                if (self.app.splitmode == 1): s &= ~32      # split on = 0
                if (self.app.txon == 1):      s &= 127      # tx on = 0
                b = s.to_bytes(1, byteorder='big')
        
            elif (cmd == 0xbb):                        # read memory
                if (data[0] == 0):
                    if (data[1] == 0x54):
                       i = self.app.act
                       if (self.app.txon == 1 and self.app.splitmode == 1):
                           i = self.app.txi
                       if (i == 0): s = 0x80     # VFOA
                       else:        s = 0x81     # VFOB
        
                    elif (data[1] == 0x64):
                        s = 0x4000
                        if (self.app.mode == 1): s |= 0x80
                        if (self.app.mode == 2): s |= 0x60
                    else:
                        continue
                    b = s.to_bytes(2, byteorder='big')
            else:
                print("unknown command", cmd)
                continue
            if (self.Send(b) == False): return False
        return True

##########################################################################
# BT agent access via DBus
# callbacks are based on GLib loop, requires an extra thread
# Init    - initial setup for DBus communications
# confirm - test if KEY_OK was pressed
# start   - start thread with GLib loop for callback from Agent 
# stop    - send quit to Glib loop to stop thread

class Rejected(dbus.DBusException):
    _dbus_error_name = "org.bluez.Error.Rejected"

class Agent(dbus.service.Object):

    # bluetooth sspmode 1, DisplayYesNo
    @dbus.service.method('org.bluez.Agent1',
                         in_signature="ou", out_signature="")

    def RequestConfirmation(self, device, passkey):
        print("RequestConfirmation (%s, %06d)" % (device, passkey))
        s = "BT Key: " + f"{(passkey):}"
        ToFile(s)
        bt_agent.wait = 2
        bt_agent.flag.clear()
        bt_agent.flag.wait(30.0)
        if (bt_agent.wait == 1):
            # on sussess: discoverable off, stop agent thread
            if (bt_agent.discoverable(0)):
                 print('BT discoverable off')
                 bt_agent.stop()
            return
        raise Rejected("Passkey doesn't match")

class bluetooth_agent:
    def __init__(self):
        self.loop    = None
        self.flag    = None
        self.wait    = 0
        self.run     = 0
        self.dis     = 0 
        self.time    = 0

    def Init(self):
        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
        bus = dbus.SystemBus()
        obj = bus.get_object('org.bluez', "/org/bluez");
        path = "/dds/agent"

        manager = dbus.Interface(obj, "org.bluez.AgentManager1")
        agent = Agent(bus, path)
        manager.RegisterAgent(path, "DisplayYesNo")
##        print("BT agent registered")
        self.loop = GLib.MainLoop()
        self.flag = threading.Event()
        return self

    def confirm(self, key):
        if (self.wait == 2):
            if (key == ecodes.KEY_OK or key == ecodes.BTN_MIDDLE):
                self.wait =  1
            else:
                self.wait =  0
            self.flag.set() 
            return True
        return False

    def to(self):
        if (self.time < 75):
            LockDDS(dds)
            self.time = 75
            GLib.timeout_add_seconds(self.time, self.to)
            return False        # remove short timer

        if (self.discoverable(-1) == False): return True  # call failed
        if (self.dis): return True                        # still visible
        self.loop.quit()               # terminate loop            
        return False                   # remove timer
        
    def listen(self):
        if (self.run): return
        print("BT agent listening")
        GLib.timeout_add_seconds(self.time, self.to)
        self.run = 1
        try:
            self.loop.run()
        except:
            pass
        self.run = 0
        print("BT agent quit")

    def start(self):
        t = threading.Thread(target=self.listen)
        self.time = 5
        t.start()

    def stop(self):
        self.loop.quit()

    def advertise(self, sock):
        try:
            bluetooth.advertise_service(sock, BT_NAME, service_id=BT_UUID,
                        service_classes=[bluetooth.SERIAL_PORT_CLASS],
                        profiles=[bluetooth.SERIAL_PORT_PROFILE], )
            print('BT advertising service', BT_NAME)
        except:
            print('BT advertising service failed')

    def discoverable(self, v):
        try:
            bus = dbus.SystemBus()
            ada = bus.get_object('org.bluez', '/org/bluez/' + BT_DEVICE)
            ada_ps = dbus.Interface(ada, "org.freedesktop.DBus.Properties")
            if (v == -1):
                v = ada_ps.Get(BT_ADAPTER, "Discoverable")
            else:
                ada_ps.Set(BT_ADAPTER, "Discoverable", dbus.Boolean(v))
            self.dis = v
            return 1
        except:
            print('BT changing discoverable state failed')
            return 0

########################################################################
#
#   Main Program

for arg in sys.argv:
    if (arg == "9833"): DDSCHIP = 9833

    if (arg == "-r"):        # remove lockfile after crash
        try:
            os.remove(LOCKFILE)
        except:
            pass
        raise SystemExit

if (exists(LOCKFILE)):        # only one instance
    print('only 1 instance, use -r to remove lockfile after crash')
    raise SystemExit

print(BT_NAME + ' Version 3')
print('DDS-Module:', DDSCHIP)

# ---------------------------------------------------------------
# install interrupt signal handler

signal.signal(signal.SIGTERM, sighandler)
signal.signal(signal.SIGQUIT, sighandler)
signal.signal(signal.SIGHUP, sighandler)
signal.signal(signal.SIGINT, sighandler)

# ---------------------------------------------------------------
# GPIO setup

InitIO()                     # init GPIO
dds = DDS().Init()           # load configuration
ResetDDS()
LoadState(dds)
SetDDS(dds.act, dds.vfofreq[dds.act])
LockDDS(dds)                 # create lock file

GPIO.setup(11,GPIO.OUT)
SetPin(11, 0)

# connect event handler for TRX line on GPIO.input 
GPIO.add_event_detect(TRX_ON, GPIO.BOTH, callback=trxOnOff, bouncetime=100)

# connect event handler for TX line on GPIO.input 
GPIO.add_event_detect(TX_ON, GPIO.BOTH, callback=txOnOff, bouncetime=100)

# connect event handler for TX_CW line on GPIO.input 
GPIO.add_event_detect(TX_CW, GPIO.BOTH, callback=cwOnOff, bouncetime=100)

# ---------------------------------------------------------------
# setup for handling input devices (remote control, usb keyboard)

inputs  = {}
clients = []
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]

for device in devices:
##     print(device.path, device.name, device.phys, device.fd)
     if 'uinput' in device.name:
         inputs[device.fd] = device

# ---------------------------------------------------------------
# setup for ip socket (hamlib socket client)

try:
    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_socket.bind(("", PORT))
    tcp_socket.settimeout(0.0)
    tcp_socket.listen(0)
    inputs[tcp_socket] = tcp_socket
    print("listening on port %d" % PORT)
except socket.error:
    print("could not open tcp socket")

# ---------------------------------------------------------------
# setup for bluetooth socket 

try:
    bt_socket = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
    try:
        bt_port = bluetooth.get_available_port(bluetooth.RFCOMM)
    except:
        bt_port = bluetooth.PORT_ANY
    try:
        bt_socket.bind(("", bt_port))
    except:
        print("BT bind failed")
except:
    print("BT could not open socket")
try:
    bt_socket.settimeout(0.0)
    bt_socket.listen(1)
    bt_port = bt_socket.getsockname()[1]
    print("BT listening on port %d" % bt_port)
    inputs[bt_socket] = bt_socket
except:
    print("BT could not listen on socket")

# ---------------------------------------------------------------
# setup for bluetooth service and pairing agent
# advertise service is required for paired and new clients
# advertise service failed, if not discoverable

try:
    bt_agent = bluetooth_agent().Init()
    bt_agent.discoverable(1)
    bt_agent.advertise(bt_socket)
    bt_agent.discoverable(0)
except:
    print("BT could not setup agent and service")

# ---------------------------------------------------------------
# mainloop, handles events from bluetooth, ip sockets, keyboard/mouse/ir

try:
    while rns:
        fdin, fdout, fderr = select(inputs, [], [])
        for fd in fdin:
            if fd is tcp_socket:                   # new tcp connection
                try:
                    conn, addr = fd.accept()
                    print('IP connection from', addr)
                    conn.settimeout(0.0)
                    inputs[conn] = conn
                    handler = HamlibHandler(dds, conn, addr)
                    clients.append(handler)
                except socket.error:
                    print("IP socket error")

            elif fd is bt_socket:                  # new bluetooth connetion
                try:
                    bt_conn, bt_addr = bt_socket.accept()
                    print('BT connection from', bt_addr)
                    bt_conn.settimeout(0.0)
                    inputs[bt_conn] = bt_conn
                    bt_handler = Ft8xxHandler(dds, bt_conn, bt_addr)
                    clients.append(bt_handler)
                except OSError:
                    print("BT socket error")

            else:
                for client in clients:             # new data available
                    if (fd == client.conn):
                        try:
                            ret = client.Process()
                        except socket.error:
                            ret = False
                        if not ret:
                            clients.remove(client) # remove from client list
                            inputs.pop(fd)         # remove connection
                            fd.close()
                            print(' Connection closed', client.addr)
                            break

                for device in devices:             # check for input event
                    if (fd == device.fd):
                        for event in inputs[fd].read():
                            if event.type == ecodes.EV_REL:
##                                print(evdev.categorize(event))
                                if (event.code == ecodes.REL_WHEEL):
                                    OnWheel(dds, event.value)

                            elif event.type == ecodes.EV_KEY:
##                                 print(evdev.categorize(event))
                                 if (event.value != 0):
                                     OnKey(dds, event.code)

# ---------------------------------------------------------------
# keyboard interupt handle

except KeyboardInterrupt:
    tcp_socket.close()
    bt_socket.close() 
    sighandler(0,0)
