From 2c57923c1148643ccc94aa3761036f66a982d442 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Sat, 22 Jan 2022 20:39:37 +0100 Subject: [PATCH] new daemon non blocking and multi client support also attempt of fixing #129 --- gui/daemon.js | 4 +- tnc/audio.py | 69 ++++++ tnc/daemon.py | 516 +++++++++++++++++--------------------------- tnc/data_handler.py | 2 + tnc/sock.py | 151 ++++++++++--- tnc/static.py | 7 +- 6 files changed, 407 insertions(+), 342 deletions(-) create mode 100644 tnc/audio.py mode change 100755 => 100644 tnc/daemon.py diff --git a/gui/daemon.js b/gui/daemon.js index 6aff9a13..6fbf1088 100644 --- a/gui/daemon.js +++ b/gui/daemon.js @@ -85,7 +85,7 @@ daemon.on('data', function(data) { msg += data.toString('utf8'); /*append data to buffer so we can stick long data together */ /* check if we reached an EOF, if true, clear buffer and parse JSON data */ - if (data.endsWith('}')) { + if (data.endsWith('}\n')) { /*console.log(msg)*/ try { /*console.log(msg)*/ @@ -163,7 +163,7 @@ exports.startTNC = function(mycall, mygrid, rx_audio, tx_audio, radiocontrol, de }] }) - //console.log(json_command) + console.log(json_command) writeDaemonCommand(json_command) } diff --git a/tnc/audio.py b/tnc/audio.py new file mode 100644 index 00000000..374c14fe --- /dev/null +++ b/tnc/audio.py @@ -0,0 +1,69 @@ + +import json + +#################################################### +# https://stackoverflow.com/questions/7088672/pyaudio-working-but-spits-out-error-messages-each-time +# https://github.com/DJ2LS/FreeDATA/issues/22 +# we need to have a look at this if we want to run this on Windows and MacOS ! +# Currently it seems, this is a Linux-only problem + +from ctypes import * +from contextlib import contextmanager +import pyaudio + +ERROR_HANDLER_FUNC = CFUNCTYPE(None, c_char_p, c_int, c_char_p, c_int, c_char_p) + +def py_error_handler(filename, line, function, err, fmt): + pass + +c_error_handler = ERROR_HANDLER_FUNC(py_error_handler) + +@contextmanager +def noalsaerr(): + asound = cdll.LoadLibrary('libasound.so') + asound.snd_lib_error_set_handler(c_error_handler) + yield + asound.snd_lib_error_set_handler(None) + +# with noalsaerr(): +# p = pyaudio.PyAudio() +###################################################### + +def get_input_devices(): + # UPDATE LIST OF AUDIO DEVICES + try: + # we need to "try" this, because sometimes libasound.so isn't in the default place + # try to supress error messages + with noalsaerr(): # https://github.com/DJ2LS/FreeDATA/issues/22 + p = pyaudio.PyAudio() + # else do it the default way + except Exception as e: + p = pyaudio.PyAudio() + + input_devices = [] + output_devices = [] + + for i in range(0, p.get_device_count()): + # we need to do a try exception, beacuse for windows theres now audio device range + try: + maxInputChannels = p.get_device_info_by_host_api_device_index(0, i).get('maxInputChannels') + maxOutputChannels = p.get_device_info_by_host_api_device_index(0, i).get('maxOutputChannels') + name = p.get_device_info_by_host_api_device_index(0, i).get('name') + except: + maxInputChannels = 0 + maxOutputChannels = 0 + name = '' + + if maxInputChannels > 0: + input_devices.append({"ID": i, "NAME": str(name)}) + if maxOutputChannels > 0: + output_devices.append({"ID": i, "NAME": str(name)}) + + p.terminate() + + return [input_devices, output_devices] + + + +def get_output_devices(): + pass \ No newline at end of file diff --git a/tnc/daemon.py b/tnc/daemon.py old mode 100755 new mode 100644 index 6920e572..0f53c106 --- a/tnc/daemon.py +++ b/tnc/daemon.py @@ -25,326 +25,201 @@ import helpers import os import queue import audio - - -DAEMON_QUEUE = queue.Queue() +import sock +class DAEMON(): + def __init__(self): + self.daemon_queue = sock.DAEMON_QUEUE + update_audio_devices = threading.Thread(target=self.update_audio_devices, name="UPDATE_AUDIO_DEVICES") + update_audio_devices.start() -log_handler.setup_logging("daemon") -structlog.get_logger("structlog").info("[DMN] Starting FreeDATA daemon", author="DJ2LS", year="2022", version="0.1") + update_serial_devices = threading.Thread(target=self.update_serial_devices, name="UPDATE_SERIAL_DEVICES") + update_serial_devices.start() -# get python version, which is needed later for determining installation path -python_version = str(sys.version_info[0]) + "." + str(sys.version_info[1]) -structlog.get_logger("structlog").info("[DMN] Python", version=python_version) - - - -# load crc engine -crc_algorithm = crcengine.new('crc16-ccitt-false') # load crc8 library - - -def start_daemon(): - - try: - structlog.get_logger("structlog").info("[DMN] Starting TCP/IP socket", port=PORT) - # https://stackoverflow.com/a/16641793 - socketserver.TCPServer.allow_reuse_address = True - daemon = socketserver.TCPServer(('0.0.0.0', PORT), CMDTCPRequestHandler) - daemon.serve_forever() - - finally: - structlog.get_logger("structlog").warning("[DMN] Closing socket", port=PORT) - daemon.server_close() - - -class CMDTCPRequestHandler(socketserver.BaseRequestHandler): - - def handle(self, hamlib_version = 0): - structlog.get_logger("structlog").debug("[DMN] Client connected", ip=self.client_address[0]) - - # loop through socket buffer until timeout is reached. then close buffer - socketTimeout = time.time() + 6 - while socketTimeout > time.time(): - - time.sleep(0.01) - encoding = 'utf-8' - #data = str(self.request.recv(1024), 'utf-8') - - data = bytes() - - # we need to loop through buffer until end of chunk is reached or timeout occured - while socketTimeout > time.time(): - data += self.request.recv(64) - # or chunk.endswith(b'\n'): - if data.startswith(b'{"type"') and data.endswith(b'}\n'): - break - data = data[:-1] # remove b'\n' - data = str(data, encoding) - - if len(data) > 0: - # reset socket timeout - socketTimeout = time.time() + static.SOCKET_TIMEOUT - # only read first line of string. multiple lines will cause an json error - # this occurs possibly, if we are getting data too fast - # data = data.splitlines()[0] - data = data.splitlines()[0] - - - # we need to do some error handling in case of socket timeout or decoding issue - try: - - # convert data to json object - received_json = json.loads(data) - - # GET COMMANDS - # "command" : "..." - - # SET COMMANDS - # "command" : "..." - # "parameter" : " ..." - - # DATA COMMANDS - # "command" : "..." - # "type" : "..." - # "dxcallsign" : "..." - # "data" : "..." - - # print(received_json) - # print(received_json["type"]) - # print(received_json["command"]) - # try: + worker = threading.Thread(target=self.worker, name="WORKER") + worker.start() + + + def update_audio_devices(self): + while 1: + static.AUDIO_INPUT_DEVICES, static.AUDIO_OUTPUT_DEVICES = audio.get_input_devices() + time.sleep(1) + + def update_serial_devices(self): + while 1: + serial_devices = [] + ports = serial.tools.list_ports.comports() + for port, desc, hwid in ports: - if received_json["type"] == 'SET' and received_json["command"] == 'MYCALLSIGN': - callsign = received_json["parameter"] - print(received_json) - if bytes(callsign, 'utf-8') == b'': - self.request.sendall(b'INVALID CALLSIGN') - structlog.get_logger("structlog").warning("[DMN] SET MYCALL FAILED", call=static.MYCALLSIGN, crc=static.MYCALLSIGN_CRC8) - else: - static.MYCALLSIGN = bytes(callsign, 'utf-8') - static.MYCALLSIGN_CRC8 = helpers.get_crc_8(static.MYCALLSIGN) - - structlog.get_logger("structlog").info("[DMN] SET MYCALL", call=static.MYCALLSIGN, crc=static.MYCALLSIGN_CRC8) + # calculate hex of hwid if we have unique names + crc_hwid = crc_algorithm(bytes(hwid, encoding='utf-8')) + crc_hwid = crc_hwid.to_bytes(2, byteorder='big') + crc_hwid = crc_hwid.hex() + description = desc + ' [' + crc_hwid + ']' + serial_devices.append({"PORT": str(port), "DESCRIPTION": str(description) }) + + static.SERIAL_DEVICES = serial_devices + time.sleep(1) + + + def worker(self): + while 1: + + data = self.daemon_queue.get() + + # data[1] mycall + # data[2] mygrid + # data[3] rx_audio + # data[4] tx_audio + # data[5] devicename + # data[6] deviceport + # data[7] serialspeed + # data[8] pttprotocol + # data[9] pttport + # data[10] data_bits + # data[11] stop_bits + # data[12] handshake + # data[13] radiocontrol + # data[14] rigctld_ip + # data[15] rigctld_port + if data[0] == 'STARTTNC': + structlog.get_logger("structlog").warning("[DMN] Starting TNC", rig=data[5], port=data[6]) + + # list of parameters, necessary for running subprocess command as a list + options = [] + options.append('--mycall') + options.append(data[1]) - if received_json["type"] == 'SET' and received_json["command"] == 'MYGRID': - mygrid = received_json["parameter"] + options.append('--mygrid') + options.append(data[2]) + + options.append('--rx') + options.append(data[3]) + + options.append('--tx') + options.append(data[4]) + + options.append('--devicename') + options.append(data[5]) + + options.append('--deviceport') + options.append(data[6]) + + options.append('--serialspeed') + options.append(data[7]) + + options.append('--pttprotocol') + options.append(data[8]) + + options.append('--pttport') + options.append(data[9]) + + options.append('--data_bits') + options.append(data[10]) + + options.append('--stop_bits') + options.append(data[11]) + + options.append('--handshake') + options.append(data[12]) + + options.append('--radiocontrol') + options.append(data[13]) + + options.append('--rigctld_ip') + options.append(data[14]) + + options.append('--rigctld_port') + options.append(data[15]) - if bytes(mygrid, 'utf-8') == b'': - self.request.sendall(b'INVALID GRID') - else: - static.MYGRID = bytes(mygrid, 'utf-8') - structlog.get_logger("structlog").info("[DMN] SET MYGRID", grid=static.MYGRID) - + # try running tnc from binary, else run from source + # this helps running the tnc in a developer environment + try: + command = [] + if sys.platform == 'linux' or sys.platform == 'darwin': + command.append('./tnc') + elif sys.platform == 'win32' or sys.platform == 'win64': + command.append('tnc.exe') + + command += options + p = subprocess.Popen(command) + structlog.get_logger("structlog").info("[DMN] TNC started", path="binary") + except: + command = [] + if sys.platform == 'linux' or sys.platform == 'darwin': + command.append('python3') + elif sys.platform == 'win32' or sys.platform == 'win64': + command.append('python') + + command.append('main.py') + command += options + p = subprocess.Popen(command) + structlog.get_logger("structlog").info("[DMN] TNC started", path="source") - if received_json["type"] == 'SET' and received_json["command"] == 'STARTTNC' and not static.TNCSTARTED: - mycall = str(received_json["parameter"][0]["mycall"]) - mygrid = str(received_json["parameter"][0]["mygrid"]) - rx_audio = str(received_json["parameter"][0]["rx_audio"]) - tx_audio = str(received_json["parameter"][0]["tx_audio"]) - devicename = str(received_json["parameter"][0]["devicename"]) - deviceport = str(received_json["parameter"][0]["deviceport"]) - serialspeed = str(received_json["parameter"][0]["serialspeed"]) - pttprotocol = str(received_json["parameter"][0]["pttprotocol"]) - pttport = str(received_json["parameter"][0]["pttport"]) - data_bits = str(received_json["parameter"][0]["data_bits"]) - stop_bits = str(received_json["parameter"][0]["stop_bits"]) - handshake = str(received_json["parameter"][0]["handshake"]) - radiocontrol = str(received_json["parameter"][0]["radiocontrol"]) - rigctld_ip = str(received_json["parameter"][0]["rigctld_ip"]) - rigctld_port = str(received_json["parameter"][0]["rigctld_port"]) + static.TNCPROCESS = p # .pid + static.TNCSTARTED = True + + # data[1] devicename + # data[2] deviceport + # data[3] serialspeed + # data[4] pttprotocol + # data[5] pttport + # data[6] data_bits + # data[7] stop_bits + # data[8] handshake + # data[9] radiocontrol + # data[10] rigctld_ip + # data[11] rigctld_port + if data[0] == 'TEST_HAMLIB': + + devicename = data[1] + deviceport = data[2] + serialspeed = data[3] + pttprotocol = data[4] + pttport = data[5] + data_bits = data[6] + stop_bits = data[7] + handshake = data[8] + radiocontrol = data[9] + rigctld_ip = data[10] + rigctld_port = data[11] + + + # check how we want to control the radio + if radiocontrol == 'direct': + import rig + elif radiocontrol == 'rigctl': + import rigctl as rig + elif radiocontrol == 'rigctld': + import rigctld as rig + else: + raise NotImplementedError + + hamlib = rig.radio() + hamlib.open_rig(devicename=devicename, deviceport=deviceport, hamlib_ptt_type=pttprotocol, serialspeed=serialspeed, pttport=pttport, data_bits=data_bits, stop_bits=stop_bits, handshake=handshake, rigctld_ip=rigctld_ip, rigctld_port = rigctld_port) + + hamlib_version = rig.hamlib_version + + hamlib.set_ptt(True) + pttstate = hamlib.get_ptt() + + if pttstate: + structlog.get_logger("structlog").info("[DMN] Hamlib PTT", status = 'SUCCESS') + response = {'COMMAND': 'TEST_HAMLIB', 'RESULT': 'SUCCESS'} + elif not pttstate: + structlog.get_logger("structlog").warning("[DMN] Hamlib PTT", status = 'NO SUCCESS') + response = {'COMMAND': 'TEST_HAMLIB', 'RESULT': 'NOSUCCESS'} + else: + structlog.get_logger("structlog").error("[DMN] Hamlib PTT", status = 'FAILED') + response = {'COMMAND': 'TEST_HAMLIB', 'RESULT': 'FAILED'} - structlog.get_logger("structlog").warning("[DMN] Starting TNC", rig=devicename, port=deviceport) - - # list of parameters, necessary for running subprocess command as a list - options = [] - options.append('--mycall') - options.append(mycall) - options.append('--mygrid') - options.append(mygrid) - options.append('--rx') - options.append(rx_audio) - options.append('--tx') - options.append(tx_audio) - options.append('--deviceport') - options.append(deviceport) - options.append('--devicename') - options.append(devicename) - options.append('--serialspeed') - options.append(serialspeed) - options.append('--pttprotocol') - options.append(pttprotocol) - options.append('--pttport') - options.append(pttport) - options.append('--data_bits') - options.append(data_bits) - options.append('--stop_bits') - options.append(stop_bits) - options.append('--handshake') - options.append(handshake) - options.append('--radiocontrol') - options.append(radiocontrol) - options.append('--rigctld_ip') - options.append(rigctld_ip) - options.append('--rigctld_port') - options.append(rigctld_port) - - - - # try running tnc from binary, else run from source - # this helps running the tnc in a developer environment - try: - command = [] - if sys.platform == 'linux' or sys.platform == 'darwin': - command.append('./tnc') - elif sys.platform == 'win32' or sys.platform == 'win64': - command.append('tnc.exe') - - command += options - p = subprocess.Popen(command) - structlog.get_logger("structlog").info("[DMN] TNC started", path="binary") - except: - command = [] - if sys.platform == 'linux' or sys.platform == 'darwin': - command.append('python3') - elif sys.platform == 'win32' or sys.platform == 'win64': - command.append('python') - - command.append('main.py') - command += options - p = subprocess.Popen(command) - structlog.get_logger("structlog").info("[DMN] TNC started", path="source") - - static.TNCPROCESS = p # .pid - static.TNCSTARTED = True - - if received_json["type"] == 'SET' and received_json["command"] == 'STOPTNC': - static.TNCPROCESS.kill() - structlog.get_logger("structlog").warning("[DMN] Stopping TNC") - #os.kill(static.TNCPROCESS, signal.SIGKILL) - static.TNCSTARTED = False - - if received_json["type"] == 'GET' and received_json["command"] == 'DAEMON_STATE': + hamlib.set_ptt(False) + hamlib.close_rig() - data = { - 'COMMAND': 'DAEMON_STATE', - 'DAEMON_STATE': [], - 'PYTHON_VERSION': str(python_version), - 'HAMLIB_VERSION': str(hamlib_version), - 'INPUT_DEVICES': [], - 'OUTPUT_DEVICES': [], - 'SERIAL_DEVICES': [], - "CPU": str(psutil.cpu_percent()), "RAM": str(psutil.virtual_memory().percent), "VERSION": "0.1-prototype"} - - if static.TNCSTARTED: - data["DAEMON_STATE"].append({"STATUS": "running"}) - else: - data["DAEMON_STATE"].append({"STATUS": "stopped"}) - - # UPDATE LIST OF AUDIO DEVICES - try: - # we need to "try" this, because sometimes libasound.so isn't in the default place - # try to supress error messages - with audio.noalsaerr(): # https://github.com/DJ2LS/FreeDATA/issues/22 - p = audio.pyaudio.PyAudio() - # else do it the default way - except Exception as e: - p = audio.pyaudio.PyAudio() - - for i in range(0, p.get_device_count()): - # we need to do a try exception, beacuse for windows theres now audio device range - try: - maxInputChannels = p.get_device_info_by_host_api_device_index(0, i).get('maxInputChannels') - maxOutputChannels = p.get_device_info_by_host_api_device_index(0, i).get('maxOutputChannels') - name = p.get_device_info_by_host_api_device_index(0, i).get('name') - except: - maxInputChannels = 0 - maxOutputChannels = 0 - name = '' - - if maxInputChannels > 0: - data["INPUT_DEVICES"].append( - {"ID": i, "NAME": str(name)}) - if maxOutputChannels > 0: - data["OUTPUT_DEVICES"].append( - {"ID": i, "NAME": str(name)}) - p.terminate() - - # UPDATE LIST OF SERIAL DEVICES - ports = serial.tools.list_ports.comports() - for port, desc, hwid in ports: - - # calculate hex of hwid if we have unique names - crc_hwid = crc_algorithm(bytes(hwid, encoding='utf-8')) - crc_hwid = crc_hwid.to_bytes(2, byteorder='big') - crc_hwid = crc_hwid.hex() - description = desc + ' [' + crc_hwid + ']' - - data["SERIAL_DEVICES"].append( - {"PORT": str(port), "DESCRIPTION": str(description) }) - - - jsondata = json.dumps(data) - self.request.sendall(bytes(jsondata, encoding)) - - - if received_json["type"] == 'GET' and received_json["command"] == 'TEST_HAMLIB': - - try: - print(received_json["parameter"]) - - devicename = str(received_json["parameter"][0]["devicename"]) - deviceport = str(received_json["parameter"][0]["deviceport"]) - serialspeed = str(received_json["parameter"][0]["serialspeed"]) - pttprotocol = str(received_json["parameter"][0]["pttprotocol"]) - pttport = str(received_json["parameter"][0]["pttport"]) - data_bits = str(received_json["parameter"][0]["data_bits"]) - stop_bits = str(received_json["parameter"][0]["stop_bits"]) - handshake = str(received_json["parameter"][0]["handshake"]) - radiocontrol = str(received_json["parameter"][0]["radiocontrol"]) - rigctld_ip = str(received_json["parameter"][0]["rigctld_ip"]) - rigctld_port = str(received_json["parameter"][0]["rigctld_port"]) - - - # check how we want to control the radio - if radiocontrol == 'direct': - import rig - elif radiocontrol == 'rigctl': - import rigctl as rig - elif radiocontrol == 'rigctld': - import rigctld as rig - else: - raise NotImplementedError - - hamlib = rig.radio() - hamlib.open_rig(devicename=devicename, deviceport=deviceport, hamlib_ptt_type=pttprotocol, serialspeed=serialspeed, pttport=pttport, data_bits=data_bits, stop_bits=stop_bits, handshake=handshake, rigctld_ip=rigctld_ip, rigctld_port = rigctld_port) - - hamlib_version = rig.hamlib_version - - hamlib.set_ptt(True) - pttstate = hamlib.get_ptt() - if pttstate: - structlog.get_logger("structlog").info("[DMN] Hamlib PTT", status = 'SUCCESS') - data = {'COMMAND': 'TEST_HAMLIB', 'RESULT': 'SUCCESS'} - elif not pttstate: - structlog.get_logger("structlog").warning("[DMN] Hamlib PTT", status = 'NO SUCCESS') - data = {'COMMAND': 'TEST_HAMLIB', 'RESULT': 'NOSUCCESS'} - else: - structlog.get_logger("structlog").error("[DMN] Hamlib PTT", status = 'FAILED') - data = {'COMMAND': 'TEST_HAMLIB', 'RESULT': 'FAILED'} - - hamlib.set_ptt(False) - hamlib.close_rig() - - jsondata = json.dumps(data) - self.request.sendall(bytes(jsondata, encoding)) - - except Exception as e: - structlog.get_logger("structlog").error("[DMN] Hamlib: Can't open rig", e = sys.exc_info()[0], error=e) - - except Exception as e: - structlog.get_logger("structlog").error("[DMN] Network error", error=e) - structlog.get_logger("structlog").warning("[DMN] Closing client socket", ip=self.client_address[0], port=self.client_address[1]) + jsondata = json.dumps(response) + sock.SOCKET_QUEUE.put(jsondata) + if __name__ == '__main__': @@ -354,9 +229,22 @@ if __name__ == '__main__': PARSER.add_argument('--port', dest="socket_port",default=3001, help="Socket port", type=int) ARGS = PARSER.parse_args() - PORT = ARGS.socket_port + static.DAEMONPORT = ARGS.socket_port - # --------------------------------------------START CMD SERVER - DAEMON_THREAD = threading.Thread(target=start_daemon, name="daemon") - DAEMON_THREAD.start() + try: + structlog.get_logger("structlog").info("[DMN] Starting TCP/IP socket", port=static.DAEMONPORT) + # https://stackoverflow.com/a/16641793 + socketserver.TCPServer.allow_reuse_address = True + cmdserver = sock.ThreadedTCPServer((static.HOST, static.DAEMONPORT), sock.ThreadedTCPRequestHandler) + server_thread = threading.Thread(target=cmdserver.serve_forever) + server_thread.daemon = True + server_thread.start() + + except Exception as e: + structlog.get_logger("structlog").error("[DMN] Starting TCP/IP socket failed", port=static.DAEMONPORT, e=e) + + + daemon = DAEMON() + while True: + time.sleep(1) \ No newline at end of file diff --git a/tnc/data_handler.py b/tnc/data_handler.py index be351d76..97082ffe 100644 --- a/tnc/data_handler.py +++ b/tnc/data_handler.py @@ -48,6 +48,8 @@ class DATA(): self.mode_list = [14,14,14,12,10] # mode list of available modes, each mode will be used 2times per speed level + self.speed_level = len(self.mode_list) - 1 # speed level for selecting mode + self.rx_frame_bof_received = False self.rx_frame_eof_received = False diff --git a/tnc/sock.py b/tnc/sock.py index 47fc8dbf..6f84796c 100644 --- a/tnc/sock.py +++ b/tnc/sock.py @@ -31,32 +31,56 @@ import sys import os import logging, structlog, log_handler import queue +import psutil +import audio SOCKET_QUEUE = queue.Queue() +DAEMON_QUEUE = queue.Queue() + +CONNECTED_CLIENTS = set() + class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): pass - - + + + class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler): - + + def send_to_client(self): while self.connection_alive: # send tnc state as network stream - data = send_tnc_state() + # check server port against daemon port and send corresponding data + if self.server.server_address[1] == static.PORT and not static.TNCSTARTED: + data = send_tnc_state() + SOCKET_QUEUE.put(data) + else: + data = send_daemon_state() + SOCKET_QUEUE.put(data) + time.sleep(0.5) + + + while not SOCKET_QUEUE.empty(): + data = SOCKET_QUEUE.get() + sock_data = bytes(data, 'utf-8') + sock_data += b'\n' # append line limiter + + # send data to all clients + for client in CONNECTED_CLIENTS: + client.send(sock_data) + # we want to transmit scatter data only once to reduce network traffic static.SCATTER = [] # we want to display INFO messages only once - static.INFO = [] - - sock_data = bytes(data, 'utf-8') - sock_data += b'\n' # append line limiter - self.request.sendall(sock_data) + static.INFO = [] + #self.request.sendall(sock_data) time.sleep(0.15) def receive_from_client(self): data = bytes() while self.connection_alive: + # BrokenPipeError: [Errno 32] Broken pipe chunk = self.request.recv(2) data += chunk @@ -66,18 +90,20 @@ class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler): if data.startswith(b'{"type"') and data.endswith(b'}\n'): data = data[:-1] # remove b'\n' - process_tnc_commands(data) - data = bytes() - - if data.endswith(b'1\n'): - print(data) - data = bytes() + if self.server.server_address[1] == static.PORT: + process_tnc_commands(data) + else: + process_daemon_commands(data) + data = bytes() + def handle(self): + CONNECTED_CLIENTS.add(self.request) structlog.get_logger("structlog").debug("[TNC] Client connected", ip=self.client_address[0], port=self.client_address[1]) self.connection_alive = True + self.sendThread = threading.Thread(target=self.send_to_client, args=[]).start() self.receiveThread = threading.Thread(target=self.receive_from_client, args=[]).start() @@ -85,8 +111,12 @@ class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler): while self.connection_alive: time.sleep(1) - structlog.get_logger("structlog").warning("[TNC] Closing client socket", ip=self.client_address[0], port=self.client_address[1]) + + def finish(self): + structlog.get_logger("structlog").warning("[TNC] Closing client socket", ip=self.client_address[0], port=self.client_address[1]) + CONNECTED_CLIENTS.remove(self.request) + print(CONNECTED_CLIENTS) def process_tnc_commands(data): @@ -204,8 +234,9 @@ def process_tnc_commands(data): output["DATA-ARRAY"].append({"DXCALLSIGN": str(static.RX_BUFFER[i][0], 'utf-8'), "DXGRID": str(static.RX_BUFFER[i][1], 'utf-8'), "TIMESTAMP": static.RX_BUFFER[i][2], "RXDATA": [rawdata]}) jsondata = json.dumps(output) - self.request.sendall(bytes(jsondata, encoding)) - + #self.request.sendall(bytes(jsondata, encoding)) + SOCKET_QUEUE.put(jsondata) + if received_json["type"] == 'GET' and received_json["command"] == 'RX_MSG_BUFFER': output = { "COMMAND": "RX_MSG_BUFFER", @@ -218,13 +249,15 @@ def process_tnc_commands(data): output["DATA-ARRAY"].append({"DXCALLSIGN": str(static.RX_MSG_BUFFER[i][0], 'utf-8'), "DXGRID": str(static.RX_MSG_BUFFER[i][1], 'utf-8'), "TIMESTAMP": static.RX_MSG_BUFFER[i][2], "RXDATA": [rawdata]}) jsondata = json.dumps(output) - self.request.sendall(bytes(jsondata, encoding)) + #self.request.sendall(bytes(jsondata, encoding)) + SOCKET_QUEUE.put(jsondata) if received_json["type"] == 'SET' and received_json["command"] == 'DEL_RX_BUFFER': static.RX_BUFFER = [] if received_json["type"] == 'SET' and received_json["command"] == 'DEL_RX_MSG_BUFFER': static.RX_MSG_BUFFER = [] + # exception, if JSON cant be decoded except Exception as e: structlog.get_logger("structlog").error("[TNC] Network error", e=e) @@ -265,11 +298,12 @@ def send_tnc_state(): output["STATIONS"].append({"DXCALLSIGN": str(static.HEARD_STATIONS[i][0], 'utf-8'), "DXGRID": str(static.HEARD_STATIONS[i][1], 'utf-8'),"TIMESTAMP": static.HEARD_STATIONS[i][2], "DATATYPE": static.HEARD_STATIONS[i][3], "SNR": static.HEARD_STATIONS[i][4], "OFFSET": static.HEARD_STATIONS[i][5], "FREQUENCY": static.HEARD_STATIONS[i][6]}) jsondata = json.dumps(output) - static.NETWORK_BUFFER = jsondata return jsondata -def process_daemon_commands(): +def process_daemon_commands(data): + # convert data to json object + received_json = json.loads(data) if received_json["type"] == 'SET' and received_json["command"] == 'MYCALLSIGN': callsign = received_json["parameter"] @@ -292,8 +326,8 @@ def process_daemon_commands(): static.MYGRID = bytes(mygrid, 'utf-8') structlog.get_logger("structlog").info("[DMN] SET MYGRID", grid=static.MYGRID) - if received_json["type"] == 'SET' and received_json["command"] == 'STARTTNC' and not static.TNCSTARTED: + mycall = str(received_json["parameter"][0]["mycall"]) mygrid = str(received_json["parameter"][0]["mygrid"]) rx_audio = str(received_json["parameter"][0]["rx_audio"]) @@ -309,6 +343,52 @@ def process_daemon_commands(): radiocontrol = str(received_json["parameter"][0]["radiocontrol"]) rigctld_ip = str(received_json["parameter"][0]["rigctld_ip"]) rigctld_port = str(received_json["parameter"][0]["rigctld_port"]) + DAEMON_QUEUE.put(['STARTTNC', \ + mycall, \ + mygrid, \ + rx_audio, \ + tx_audio, \ + devicename, \ + deviceport, \ + serialspeed, \ + pttprotocol, \ + pttport, \ + data_bits, \ + stop_bits, \ + handshake, \ + radiocontrol, \ + rigctld_ip, \ + rigctld_port \ + ]) + + + if received_json["type"] == 'GET' and received_json["command"] == 'TEST_HAMLIB': + + + devicename = str(received_json["parameter"][0]["devicename"]) + deviceport = str(received_json["parameter"][0]["deviceport"]) + serialspeed = str(received_json["parameter"][0]["serialspeed"]) + pttprotocol = str(received_json["parameter"][0]["pttprotocol"]) + pttport = str(received_json["parameter"][0]["pttport"]) + data_bits = str(received_json["parameter"][0]["data_bits"]) + stop_bits = str(received_json["parameter"][0]["stop_bits"]) + handshake = str(received_json["parameter"][0]["handshake"]) + radiocontrol = str(received_json["parameter"][0]["radiocontrol"]) + rigctld_ip = str(received_json["parameter"][0]["rigctld_ip"]) + rigctld_port = str(received_json["parameter"][0]["rigctld_port"]) + DAEMON_QUEUE.put(['TEST_HAMLIB', \ + devicename, \ + deviceport, \ + serialspeed, \ + pttprotocol, \ + pttport, \ + data_bits, \ + stop_bits, \ + handshake, \ + radiocontrol, \ + rigctld_ip, \ + rigctld_port \ + ]) if received_json["type"] == 'SET' and received_json["command"] == 'STOPTNC': static.TNCPROCESS.kill() @@ -316,5 +396,28 @@ def process_daemon_commands(): static.TNCSTARTED = False -def sent_daemon_state(): - pass \ No newline at end of file +def send_daemon_state(): + + python_version = str(sys.version_info[0]) + "." + str(sys.version_info[1]) + + + output = { + 'COMMAND': 'DAEMON_STATE', + 'DAEMON_STATE': [], + 'PYTHON_VERSION': str(python_version), + 'HAMLIB_VERSION': static.HAMLIB_VERSION, + 'INPUT_DEVICES': static.AUDIO_INPUT_DEVICES, + 'OUTPUT_DEVICES': static.AUDIO_OUTPUT_DEVICES, + 'SERIAL_DEVICES': static.SERIAL_DEVICES, + 'CPU': str(psutil.cpu_percent()), + 'RAM': str(psutil.virtual_memory().percent), + 'VERSION': '0.1' + } + + if static.TNCSTARTED: + output["DAEMON_STATE"].append({"STATUS": "running"}) + else: + output["DAEMON_STATE"].append({"STATUS": "stopped"}) + + jsondata = json.dumps(output) + return jsondata \ No newline at end of file diff --git a/tnc/static.py b/tnc/static.py index ea6f0753..a666e5b1 100644 --- a/tnc/static.py +++ b/tnc/static.py @@ -7,7 +7,6 @@ Created on Wed Dec 23 11:13:57 2020 Here we are saving application wide variables and stats, which have to be accessed everywhere. Not nice, tipps are appreciated :-) """ -NETWORK_BUFFER = b'' # DAEMON DAEMONPORT = 3001 TNCSTARTED = False @@ -31,12 +30,14 @@ HOST = "0.0.0.0" PORT = 3000 SOCKET_TIMEOUT = 1 # seconds # --------------------------------- - +SERIAL_DEVICES = [] +# --------------------------------- PTT_STATE = False TRANSMITTING = False +HAMLIB_VERSION = '0' HAMLIB_PTT_TYPE = 'RTS' HAMLIB_DEVICE_NAME = 'RIG_MODEL_DUMMY_NOVFO' HAMLIB_DEVICE_PORT = '/dev/ttyUSB0' @@ -61,6 +62,8 @@ SCATTER = [] # --------------------------------- # Audio Defaults +AUDIO_INPUT_DEVICES = [] +AUDIO_OUTPUT_DEVICES = [] AUDIO_INPUT_DEVICE = -2 AUDIO_OUTPUT_DEVICE = -2 BUFFER_OVERFLOW_COUNTER = [0,0,0]