FreeDATA/modem/daemon.py
2023-10-27 22:58:11 +02:00

630 lines
20 KiB
Python
Executable file

#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
daemon.py
Author: DJ2LS, January 2022
daemon for providing basic information for the modem like audio or serial devices
"""
# pylint: disable=invalid-name, line-too-long, c-extension-no-member
# pylint: disable=import-outside-toplevel
import argparse
import atexit
import multiprocessing
import os
import signal
import socketserver
import subprocess
import sys
import threading
import time
import audio
import crcengine
import log_handler
import serial.tools.list_ports
import sock
from global_instances import ARQ, AudioParam, Beacon, Channel, Daemon, HamlibParam, ModemParam, Station, Statistics, TCIParam, Modem
import structlog
import ujson as json
import config
# signal handler for closing application
def signal_handler(sig, frame):
"""
Signal handler for closing the network socket on app exit
Args:
sig:
frame:
Returns: system exit
"""
print("Closing daemon...")
sock.CLOSE_SIGNAL = True
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
class DAEMON:
"""
Daemon class
"""
log = structlog.get_logger("DAEMON")
def __init__(self):
# load crc engine
self.crc_algorithm = crcengine.new("crc16-ccitt-false") # load crc8 library
self.daemon_queue = sock.DAEMON_QUEUE
update_audio_devices = threading.Thread(
target=self.update_audio_devices, name="UPDATE_AUDIO_DEVICES", daemon=True
)
update_audio_devices.start()
update_serial_devices = threading.Thread(
target=self.update_serial_devices, name="UPDATE_SERIAL_DEVICES", daemon=True
)
update_serial_devices.start()
worker = threading.Thread(target=self.worker, name="WORKER", daemon=True)
worker.start()
rigctld_watchdog_thread = threading.Thread(target=self.rigctld_watchdog, name="WORKER", daemon=True)
rigctld_watchdog_thread.start()
def rigctld_watchdog(self):
"""
Check for rigctld status
Returns:
"""
while True:
threading.Event().wait(0.01)
# only continue, if we have a process object initialized
if hasattr(Daemon.rigctldprocess, "returncode"):
if Daemon.rigctldprocess.returncode in [None, "None"] or not Daemon.rigctldstarted:
Daemon.rigctldstarted = True
# outs, errs = Daemon.rigctldprocess.communicate(timeout=10)
# print(f"outs: {outs}")
# print(f"errs: {errs}")
else:
self.log.warning("[DMN] [RIGCTLD] [Watchdog] returncode detected",process=Daemon.rigctldprocess)
Daemon.rigctldstarted = False
# triggering another kill
Daemon.rigctldprocess.kill()
# erase process object
Daemon.rigctldprocess = None
else:
Daemon.rigctldstarted = False
def update_audio_devices(self):
"""
Update audio devices and set to static
"""
while True:
try:
if not Daemon.modemstarted:
(
AudioParam.audio_input_devices,
AudioParam.audio_output_devices,
) = audio.get_audio_devices()
except Exception as err1:
self.log.error(
"[DMN] update_audio_devices: Exception gathering audio devices:",
e=err1,
)
threading.Event().wait(1)
def update_serial_devices(self):
"""
Update serial devices and set to static
"""
while True:
try:
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 = self.crc_algorithm(bytes(hwid, encoding="utf-8"))
crc_hwid = crc_hwid.to_bytes(2, byteorder="big")
crc_hwid = crc_hwid.hex()
description = f"{desc} [{crc_hwid}]"
serial_devices.append(
{"port": str(port), "description": str(description)}
)
Daemon.serial_devices = serial_devices
threading.Event().wait(1)
except Exception as err1:
self.log.error(
"[DMN] update_serial_devices: Exception gathering serial devices:",
e=err1,
)
def worker(self):
"""
Worker to handle the received commands
"""
while True:
try:
data = self.daemon_queue.get()
# increase length of list for storing additional
# parameters starting at entry 64
data = data[:64] + [None] * (64 - len(data))
# data[1] mycall
# data[2] mygrid
# data[3] rx_audio
# data[4] tx_audio
# data[5] radiocontrol
# data[6] rigctld_ip
# data[7] rigctld_port
# data[8] send_scatter
# data[9] send_fft
# data[10] low_bandwidth_mode
# data[11] tuning_range_fmin
# data[12] tuning_range_fmax
# data[13] enable FSK
# data[14] tx-audio-level
# data[15] respond_to_cq
# data[16] rx_buffer_size
# data[17] explorer
# data[18] ssid_list
# data[19] auto_tune
# data[20] stats
# data[21] tx_delay
if data[0] == "STARTModem":
self.start_modem(data)
if data[0] == "TEST_HAMLIB":
# data[9] radiocontrol
# data[10] rigctld_ip
# data[11] rigctld_port
self.test_hamlib_ptt(data)
if data[0] == "START_RIGCTLD":
"""
data[0] START_RIGCTLD,
data[1] hamlib_deviceid,
data[2] hamlib_deviceport,
data[3] hamlib_stop_bits,
data[4] hamlib_data_bits,
data[5] hamlib_handshake,
data[6] hamlib_serialspeed,
data[7] hamlib_dtrstate,
data[8] hamlib_pttprotocol,
data[9] hamlib_ptt_port,
data[10] hamlib_dcd,
data[11] hamlbib_serialspeed_ptt,
data[12] hamlib_rigctld_port,
data[13] hamlib_rigctld_ip,
data[14] hamlib_rigctld_path,
data[15] hamlib_rigctld_server_port,
data[16] hamlib_rigctld_custom_args
"""
self.start_rigctld(data)
except Exception as err1:
self.log.error("[DMN] worker: Exception: ", e=err1)
def test_hamlib_ptt(self, data):
radiocontrol = data[1]
# check how we want to control the radio
if radiocontrol == "direct":
print("direct hamlib support deprecated - not usable anymore")
sys.exit(1)
elif radiocontrol == "rigctl":
print("rigctl support deprecated - not usable anymore")
sys.exit(1)
elif radiocontrol == "rigctld":
import rigctld as rig
rigctld_ip = data[2]
rigctld_port = data[3]
elif radiocontrol == "tci":
import tci as rig
rigctld_ip = data[22]
rigctld_port = data[23]
else:
import rigdummy as rig
rigctld_ip = '127.0.0.1'
rigctld_port = '0'
hamlib = rig.radio()
hamlib.open_rig(
rigctld_ip=rigctld_ip,
rigctld_port=rigctld_port,
)
# hamlib_version = rig.hamlib_version
hamlib.set_ptt(True)
#Allow a little time for network based rig to register PTT is active
time.sleep(.250)
if hamlib.get_ptt():
self.log.info("[DMN] Hamlib PTT", status="SUCCESS")
response = {"command": "test_hamlib", "result": "SUCCESS"}
else:
self.log.warning("[DMN] Hamlib PTT", status="NO SUCCESS")
response = {"command": "test_hamlib", "result": "NOSUCCESS"}
hamlib.set_ptt(False)
hamlib.close_rig()
jsondata = json.dumps(response)
sock.SOCKET_QUEUE.put(jsondata)
def start_rigctld(self, data):
# Seems to be working on Win
"""
data[0] START_RIGCTLD,
data[1] hamlib_deviceid,
data[2] hamlib_deviceport,
data[3] hamlib_stop_bits,
data[4] hamlib_data_bits,
data[5] hamlib_handshake,
data[6] hamlib_serialspeed,
data[7] hamlib_dtrstate,
data[8] hamlib_pttprotocol,
data[9] hamlib_ptt_port,
data[10] hamlib_dcd,
data[11] hamlbib_serialspeed_ptt,
data[12] hamlib_rigctld_port,
data[13] hamlib_rigctld_ip,
data[14] hamlib_rigctld_path,
data[15] hamlib_rigctld_server_port,
data[16] hamlib_rigctld_custom_args
"""
try:
command = []
isWin = False
if sys.platform in ["darwin"]:
if data[14] not in [""]:
# hamlib_rigctld_path
application_path = data[14]
else:
application_path = "rigctld"
command.append(f'{application_path}')
elif sys.platform in ["linux", "darwin"]:
if data[14] not in [""]:
# hamlib_rigctld_path
application_path = data[14]
else:
application_path = "rigctld"
command.append(f'{application_path}')
elif sys.platform in ["win32", "win64"]:
isWin=True
if data[13].lower() == "localhost":
data[13]="127.0.0.1"
if data[14] not in [""]:
# hamlib_rigctld_path
application_path = data[14]
else:
application_path = "rigctld.exe"
command.append(f'{application_path}')
options = []
# hamlib_deviceid
if data[1] not in [None, "None", "ignore"]:
options.append(("--model=" + data[1] ))
# hamlib_deviceport
if data[2] not in [None, "None", "ignore"]:
options.append(("--rig-file="+ data[2]))
# hamlib_stop_bits
if data[3] not in [None, "None", "ignore"]:
options.append(("--set-conf=stop_bits=" + data[3]))
# hamlib_data_bits
if data[4] not in [None, "None", "ignore"]:
options.append(("--set-conf=data_bits=" + data[4]))
# hamlib_handshake
if data[5] not in [None, "None", "ignore"]:
options.append(("--set-conf=serial_handshake=" + data[5]))
# hamlib_serialspeed
if data[6] not in [None, "None", "ignore"]:
options.append(("--serial-speed=" + data[6]))
# hamlib_dtrstate
if data[7] not in [None, "None", "ignore"]:
options.append(("--set-conf=dtr_state=" + data[7]))
# hamlib_pttprotocol
if data[8] not in [None, "None", "ignore"]:
options.append(("--ptt-type=" + data[8]))
# hamlib_ptt_port
if data[9] not in [None, "None", "ignore"]:
options.append(("--ptt-file=" + data[9]))
# hamlib_dcd
if data[10] not in [None, "None", "ignore"]:
options.append(("--dcd-type=" + data[10]))
# hamlbib_serialspeed_ptt
if data[11] not in [None, "None", "ignore"]:
# options.extend(("-m", data[11]))
pass
# hamlib_rigctld_port
# Using this ensures rigctld starts on port configured in GUI
if data[12] not in [None, "None", "ignore"]:
options.append(("--port="+ data[12]))
# hamlib_rigctld_ip
if data[13] not in [None, "None", "ignore"]:
options.append(("--listen-addr="+ data[13]))
# data[14] == hamlib_rigctld_path
# maybe at wrong place in list...
#Not needed for setting command line arguments
# hamlib_rigctld_server_port
# Ignore configured value and use value configured in GUI
#if data[15] not in [None, "None", "ignore"]:
# options.extend(("-m", data[15]))
# pass
# hamlib_rigctld_custom_args
if data[16] not in [None, "None", "ignore"]:
for o in data[16].split(" "):
options.append(o)
# append debugging paramter
# disabled as this could be set via gui
#options.append(("-vvv"))
command += options
self.log.info("[DMN] starting rigctld: ", param=command)
if not isWin:
# NOTE --> It seems Popen is non blocking, while run is blocking
#proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
proc = subprocess.Popen(command)
#proc = subprocess.run(command, shell=False, check=True, text=True, capture_output=True)
else:
#On windows, open rigctld in new window for easier troubleshooting
proc = subprocess.Popen(command, creationflags=subprocess.CREATE_NEW_CONSOLE,close_fds=True)
Daemon.rigctldstarted = True
Daemon.rigctldprocess = proc
atexit.register(proc.kill)
except Exception as err:
self.log.warning("[DMN] err starting rigctld: ", e=err)
def start_modem(self, data):
self.log.warning("[DMN] Starting Modem", rig=data[5], port=data[6])
# list of parameters, necessary for running subprocess command as a list
options = ["--port", str(DAEMON.port - 1)]
# create an additional list entry for parameters not covered by gui
data[50] = int(DAEMON.port - 1)
options.append("--mycall")
options.extend((data[1], "--mygrid"))
options.extend((data[2], "--rx"))
options.extend((data[3], "--tx"))
options.append(data[4])
# if radiocontrol != disabled
# this should hopefully avoid a ton of problems if we are just running in
# disabled mode
if data[5] != "disabled":
options.append("--radiocontrol")
options.append(data[5])
if data[5] == "rigctld":
options.append("--rigctld_ip")
options.extend((data[6], "--rigctld_port"))
options.append(data[7])
if data[5] == "tci":
options.append("--tci-ip")
options.extend((data[22], "--tci-port"))
options.append(data[23])
if data[8] == "True":
options.append("--scatter")
if data[9] == "True":
options.append("--fft")
if data[10] == "True":
options.append("--500hz")
options.append("--tuning_range_fmin")
options.extend((data[11], "--tuning_range_fmax"))
options.extend((data[12], "--tx-audio-level"))
options.append(data[14])
if data[15] == "True":
options.append("--qrv")
options.append("--rx-buffer-size")
options.append(data[16])
if data[17] == "True":
options.append("--explorer")
options.append("--ssid")
options.extend(str(i) for i in data[18])
if data[19] == "True":
options.append("--tune")
if data[20] == "True":
options.append("--stats")
if data[13] == "True":
options.append("--fsk")
options.append("--tx-delay")
options.append(data[21])
#Mesh
print(data[24])
if data[24] == "True":
options.append("--mesh")
print(options)
# safe data to config file
config.write_entire_config(data)
# Try running modem from binary, else run from source
# This helps running the modem in a developer environment
try:
command = []
if (getattr(sys, 'frozen', False) or hasattr(sys, "_MEIPASS")) and sys.platform in ["darwin"]:
# If the application is run as a bundle, the PyInstaller bootloader
# extends the sys module by a flag frozen=True and sets the app
# path into variable _MEIPASS'.
application_path = sys._MEIPASS
command.append(f'{application_path}/freedata-modem')
elif sys.platform in ["linux", "darwin"]:
command.append("./freedata-modem")
elif sys.platform in ["win32", "win64"]:
command.append("freedata-modem.exe")
command += options
proc = subprocess.Popen(command)
atexit.register(proc.kill)
Daemon.modemprocess = proc
Daemon.modemstarted = True
self.log.info("[DMN] Modem started", path="binary")
except FileNotFoundError as err1:
try:
self.log.info("[DMN] worker: ", e=err1)
command = []
if sys.platform in ["linux", "darwin"]:
command.append("python3")
elif sys.platform in ["win32", "win64"]:
command.append("python")
command.append("main.py")
command += options
proc = subprocess.Popen(command)
atexit.register(proc.kill)
self.log.info("[DMN] Modem started", path="source")
Daemon.modemprocess = proc
Daemon.modemstarted = True
except Exception as e:
self.log.error("[DMN] Modem not started", error=e)
Daemon.modemstarted = False
if __name__ == "__main__":
mainlog = structlog.get_logger(__file__)
# we need to run this on Windows for multiprocessing support
multiprocessing.freeze_support()
# --------------------------------------------GET PARAMETER INPUTS
PARSER = argparse.ArgumentParser(description="FreeDATA Daemon")
PARSER.add_argument(
"--port",
dest="socket_port",
default=3001,
help="Socket port in the range of 1024-65535",
type=int,
)
ARGS = PARSER.parse_args()
DAEMON.port = ARGS.socket_port
try:
if sys.platform == "linux":
logging_path = os.getenv("HOME") + "/.config/" + "FreeDATA/" + "daemon"
if sys.platform == "darwin":
logging_path = (
os.getenv("HOME")
+ "/Library/"
+ "Application Support/"
+ "FreeDATA/"
+ "daemon"
)
if sys.platform in ["win32", "win64"]:
logging_path = os.getenv("APPDATA") + "/" + "FreeDATA/" + "daemon"
if not os.path.exists(logging_path):
os.makedirs(logging_path)
log_handler.setup_logging(logging_path)
except Exception as err:
mainlog.error("[DMN] logger init error", exception=err)
# init config
config = config.CONFIG("config.ini")
try:
mainlog.info("[DMN] Starting TCP/IP socket", port=DAEMON.port)
# https://stackoverflow.com/a/16641793
socketserver.TCPServer.allow_reuse_address = True
cmdserver = sock.ThreadedTCPServer(
(Modem.host, DAEMON.port), sock.ThreadedTCPRequestHandler
)
server_thread = threading.Thread(target=cmdserver.serve_forever)
server_thread.daemon = True
server_thread.start()
except Exception as err:
mainlog.error(
"[DMN] Starting TCP/IP socket failed", port=DAEMON.port, e=err
)
sys.exit(1)
daemon = DAEMON()
mainlog.info(
"[DMN] Starting FreeDATA Daemon",
author="DJ2LS",
year="2023",
version=Modem.version,
)
while True:
threading.Event().wait(1)