diff --git a/BotResources.py b/BotResources.py index d7f8efd..82a751b 100644 --- a/BotResources.py +++ b/BotResources.py @@ -5,18 +5,37 @@ from datetime import date from os.path import exists from NoiseGatev2 import AudioStream, query_devices +# Handler configs PDB_ACCEPTABLE_HANDLERS = {'gqrx': { 'Modes': ['wfm', 'fm'] }, 'op25': { 'Modes': ['d', 'p25'] }} + +# Known bot IDs PDB_KNOWN_BOT_IDS = {756327271597473863: "Greada", 915064996994633729: "Jorn", 943742040255115304: "Brent"} +# Default value to set noisegate on new or unknown profiles DEFAULT_NOISEGATE_THRESHOLD = 50 +# Initialize the logger for this file LOGGER = logging.getLogger('Discord_Radio_Bot.Bot_Resources') +# Location of the gqrx binary +GQRX_BIN_LOCATION = "/usr/bin/" +GQRX_BIN = "/usr/bin/gqrx" + + +# Default radio settings +DEFAULT_RADIO_SETTINGS = { + 'profile_name': None, + 'freq': "104700000", + 'mode': "wfm", + 'squelch': 0, + 'noisegate_sensitivity': DEFAULT_NOISEGATE_THRESHOLD, +} + def check_if_config_exists(): if exists('./config.ini'): diff --git a/README.md b/README.md index 77a0b3f..d2c4708 100644 --- a/README.md +++ b/README.md @@ -23,4 +23,9 @@ Voicemeeter is **highly** recommended for this bot. See a detailed guide on how To change the audio source, simply delete the ```config.ini``` that was generated and restart the bot. It will re-do the setup and allow you to select a new device. -### [To-Do](https://git.vpn.cusano.net/Discord_Bot_Gang/Discord-Radio-Bot/src/branch/master/TODO.md) \ No newline at end of file +### [To-Do](https://git.vpn.cusano.net/Discord_Bot_Gang/Discord-Radio-Bot/src/branch/master/TODO.md) + + +**Notes for readme leter on** +- Users need to save profile after any change in discord +- \ No newline at end of file diff --git a/TODO.md b/TODO.md index 4b281c7..40383b2 100644 --- a/TODO.md +++ b/TODO.md @@ -3,6 +3,7 @@ ### Main Development #### Core - [ ] Add new handlers for GQRX: https://github.com/gqrx-sdr/gqrx/blob/master/resources/remote-control.txt +- [ ] Add logging for Elasticstack https://www.elastic.co/guide/en/ecs-logging/python/master/installation.html - [ ] Add a process handler to start/stop gqrx - [ ] Fix the bug where they *disconnect* after a period of time and must be manually moved out and back in to hear them - *May* have been fixed with the noise gate? diff --git a/bot.py b/bot.py index 1c77a9e..0822e6f 100644 --- a/bot.py +++ b/bot.py @@ -40,11 +40,11 @@ class Bot(commands.Bot): self.Devices_List = NoiseGatev2.query_devices().items() # Init radio parameters - self.profile_name = None - self.freq = "104700000" - self.mode = "wfm" - self.squelch = 0 - self.noisegate_sensitivity = BotResources.DEFAULT_NOISEGATE_THRESHOLD + self.profile_name = BotResources.DEFAULT_RADIO_SETTINGS['profile_name'] + self.freq = BotResources.DEFAULT_RADIO_SETTINGS['freq'] + self.mode = BotResources.DEFAULT_RADIO_SETTINGS['mode'] + self.squelch = BotResources.DEFAULT_RADIO_SETTINGS['squelch'] + self.noisegate_sensitivity = BotResources.DEFAULT_RADIO_SETTINGS['noisegate_sensitivity'] # Init SDR Variables self.system_os_type = None @@ -171,8 +171,12 @@ class Bot(commands.Bot): f"{self.streamHandler.THRESHOLD} to {_threshold}") self.streamHandler.THRESHOLD = _threshold self.noisegate_sensitivity = _threshold - if self.sdr_started: - await self.set_activity() + + # Reset the profile name since we have made a change + self.profile_name = None + + # If the SDR is started, restart it with the updates + await self.use_current_radio_config() # Add commands for GQRX and OP25 if self.Handler in BotResources.PDB_ACCEPTABLE_HANDLERS.keys(): @@ -223,13 +227,11 @@ class Bot(commands.Bot): await ctx.send(f"Ok {str(member).capitalize()}, I'm changing the mode to " f"{str(self.mode).upper()} and frequency to {self.freq}") - # Reset the profile name since we have made a change to the freq + # Reset the profile name since we have made a change self.profile_name = None # If the SDR is started, restart it with the updates - if self.sdr_started: - self.start_sdr() - await self.set_activity() + await self.use_current_radio_config() else: await ctx.send(f"{str(member).capitalize()}, {mode} is not valid." f" You may only enter {self.possible_modes}") @@ -252,9 +254,11 @@ class Bot(commands.Bot): self.squelch = squelch await ctx.send(f"Ok {str(member).capitalize()}, I'm changing the squelch to {self.squelch}") + # Reset the profile name since we have made a change + self.profile_name = None + # If the SDR is started, restart it with the updates - if self.sdr_started: - self.start_sdr() + await self.use_current_radio_config() # Hidden admin commands @self.command(name='saveprofile', hidden=True) @@ -336,6 +340,7 @@ class Bot(commands.Bot): self.logger.info("Starting GQRX handler") from gqrxHandler import GQRXHandler self.GQRXHandler = GQRXHandler() + self.GQRXHandler.start() self.possible_modes = BotResources.PDB_ACCEPTABLE_HANDLERS['gqrx']['Modes'] elif self.Handler == 'op25': @@ -433,7 +438,9 @@ class Bot(commands.Bot): if self.Handler == 'gqrx': # Set the settings in GQRX - self.GQRXHandler.set_all_settings(self.mode, self.squelch, self.freq) + self.GQRXHandler.set_gqrx_parameters(_frequency=self.freq, _squelch=self.squelch, + _fm_mode=self.mode, _output_device_name=self.DEVICE_NAME, + _start=True) elif self.Handler == 'op25': self.OP25Handler.set_op25_parameters(self.freq, _start=True, _output_device_name=self.DEVICE_NAME) @@ -445,11 +452,16 @@ class Bot(commands.Bot): def stop_sdr(self): if self.sdr_started: # Wait for the running processes to close + + # Close the GQRX handler + if self.Handler == 'gqrx': + self.GQRXHandler.set_gqrx_parameters(_stop=True) + # Close the OP25 handler if self.Handler == 'op25': self.OP25Handler.set_op25_parameters(_stop=True) # self.OP25Handler.join() - # Need a way to 'close' GQRX + self.sdr_started = False # Set the activity of the bot @@ -495,8 +507,19 @@ class Bot(commands.Bot): self.profile_name = profile_name + await self.use_current_radio_config() + + async def use_current_radio_config(self): if self.sdr_started: - self.start_sdr() + # Set the loaded profile settings into GQRX + if self.Handler == "gqrx": + self.GQRXHandler.set_gqrx_parameters(_frequency=self.freq, _squelch=self.squelch, + _fm_mode=self.mode) + # Restart OP25 to use the loaded profile + if self.Handler == "op25": + self.start_sdr() + + # Set the activity to reflect the loaded profile await self.set_activity() # Load a saved profile into the current settings @@ -519,10 +542,7 @@ class Bot(commands.Bot): self.noisegate_sensitivity = BotResources.DEFAULT_NOISEGATE_THRESHOLD await self.save_radio_config(self.profile_name) - if self.sdr_started: - self.start_sdr() - await self.set_activity() - + await self.use_current_radio_config() return True else: return False diff --git a/gqrxHandler.py b/gqrxHandler.py index 2727a13..c3afbd1 100644 --- a/gqrxHandler.py +++ b/gqrxHandler.py @@ -1,22 +1,195 @@ +import configparser +import shutil import logging +import threading +import subprocess +import time +from pathlib import Path from telnetlib import Telnet -from BotResources import check_negative +from BotResources import * from time import sleep -class GQRXHandler(): - def __init__(self, hostname: str = "localhost", port: int = 7356): + +def reset_crashed(_config_path): + config = configparser.SafeConfigParser() + config.read(_config_path) + if config.has_section('General'): + if config.getboolean('General', 'crashed'): + config['General']['crashed'] = 'false' + with open(_config_path, 'w') as config_file: + config.write(config_file) + return True + else: + return False + + +def enable_agc(_config_path): + config = configparser.SafeConfigParser() + config.read(_config_path) + if config.has_option('receiver', 'agc_off'): + config.remove_option('receiver', 'agc_off') + with open(_config_path, 'w') as config_file: + config.write(config_file) + return True + + +class GQRXHandler(threading.Thread): + def __init__(self): + super().__init__() + self.GQRX_Config_Path = Path(f"{Path.home()}/.config/gqrx/drb_defaults.conf") + self.GQRXDir: str = GQRX_BIN_LOCATION + self.GQRXEXE: str = shutil.which(GQRX_BIN) + self.GQRXProc = None + self.GQRX_Started = False + self.logger = logging.getLogger("Discord_Radio_Bot.GQRXHandler") - self.hostname = hostname - self.port = port + + self.Frequency = None + + self.Mode = DEFAULT_RADIO_SETTINGS['mode'] + self.Frequency = DEFAULT_RADIO_SETTINGS['freq'] + self.Squelch = DEFAULT_RADIO_SETTINGS['squelch'] + + self.Start_GQRX = False + self.Stop_GQRX = False + + self.Output_Device_Name = None + + self.hostname = "localhost" + self.port = 7356 self.tel_conn = None - self.create_telnet_connection() + def run(self) -> None: + while True: + if self.Start_GQRX: + self.open_gqrx() + + self.Start_GQRX = False + self.Stop_GQRX = False + + self.logger.debug("GQRX is open, waiting for it to close") + + while not self.Stop_GQRX: + sleep(1) + + self.logger.debug('Request to close GQRX') + + self.close_gqrx() + sleep(.5) + + def set_gqrx_parameters(self, _frequency: str = False, _start: bool = False, _stop: bool = False, + _output_device_name: str = None, _fm_mode: str = None, _squelch: float = None, + _hostname: str = None, _port: int = None, _start_dsp: bool = None): + if _frequency: + self.Frequency = _frequency + if self.GQRX_Started: + self.change_freq(_frequency) + + if _output_device_name: + self.Output_Device_Name = _output_device_name + + if _fm_mode: + self.Mode = _fm_mode + if self.GQRX_Started: + self.change_mode(_fm_mode) + + if _squelch: + self.Squelch = _squelch + if self.GQRX_Started: + self.change_squelch(_squelch) + + if _hostname: + self.hostname = _hostname + self.Start_GQRX = True + + if _port: + self.port = _port + self.Start_GQRX = True + + if _start_dsp: + self.start_dsp() + + if _start: + self.Start_GQRX = _start + + if _stop: + self.Stop_GQRX = _stop + + def open_gqrx(self): + if self.GQRX_Started: + self.close_gqrx() + + gqrx_kwargs = [f"gqrx", "-c", "drb_defaults.conf"] + + self.logger.info(f"Resetting 'crashed' option in the GQRX config") + + self.reset_or_create_config() + + self.logger.info(f"Starting GQRX") + + self.logger.debug(f"GQRX Keyword Args: {gqrx_kwargs}") + + self.GQRXProc = subprocess.Popen(gqrx_kwargs, executable=self.GQRXEXE, shell=False) + + while not self.tel_conn: + self.create_telnet_connection() + sleep(2) + self.logger.debug(f"Waiting for GQRX to start") + + self.GQRX_Started = True + + self.start_dsp() + + self.set_all_settings(_squelch=self.Squelch, _mode=self.Mode, _freq=self.Frequency) + self.logger.debug('Finished opening GQRX') + + def close_gqrx(self): + self.logger.info(f"Closing GQRX") + try: + self.GQRXProc.kill() + + seconds_waited = 0 + while self.GQRXProc.poll() is None: + # Terminate the process every 5 seconds + if seconds_waited % 5 == 0: + self.logger.info("Terminating GQRX") + self.GQRXProc.terminate() + sleep(1) + self.logger.debug(f"Waited {seconds_waited} seconds") + seconds_waited += 1 + self.logger.debug("GQRX Closed") + self.GQRX_Started = False + self.tel_conn = None + + except Exception as e: + self.logger.error(e) def create_telnet_connection(self): - self.logger.info("Creating connection") - self.tel_conn = Telnet(self.hostname, self.port) - self.tel_conn.open(self.hostname, self.port) + self.logger.debug("Creating connection") + try: + self.tel_conn = Telnet(self.hostname, self.port) + self.tel_conn.open(self.hostname, self.port) + self.logger.debug(f"GQRX is open") + return True + except Exception as err: + self.logger.warning(err) + self.tel_conn = None + return False + + def check_dsp(self): + self.logger.debug(f"Checking if DSP is running on GQRX") + self.tel_conn.write(bytes(f"u DSP", 'utf-8')) + if self.tel_conn.read_some() == b"1": + return True + else: + return False + + def start_dsp(self): + if not self.check_dsp(): + self.logger.debug(f"Starting DSP on GQRX") + self.tel_conn.write(bytes(f"U DSP 1", 'utf-8')) + self.tel_conn.read_until(b'RPRT 0') def change_freq(self, freq): self.logger.debug(f"Changing freq to {freq}") @@ -35,8 +208,32 @@ class GQRXHandler(): self.tel_conn.write(bytes(f"M {str(mode)}", 'utf-8')) self.tel_conn.read_until(b'RPRT 0') - def set_all_settings(self, mode, squelch, freq): + def set_all_settings(self, _mode, _squelch, _freq): self.change_squelch(0) - self.change_mode(mode) - self.change_freq(freq) - self.change_squelch(squelch) + self.change_mode(_mode) + self.change_freq(_freq) + self.change_squelch(_squelch) + + def creat_config(self): + from templates.gqrx_config_template import drb_defaults + config = drb_defaults + try: + with open(self.GQRX_Config_Path, 'w+') as config_file: + config_file.write(config) + except OSError as err: + self.logger.error(err) + + def reset_or_create_config(self): + if self.GQRX_Config_Path.is_file(): + try: + self.logger.debug(f"Enabling AGC in the GQRX config") + enable_agc(_config_path=self.GQRX_Config_Path) + + self.logger.debug(f"GQRX Config exists, resetting 'crashed' setting") + reset_crashed(_config_path=self.GQRX_Config_Path) + except configparser.DuplicateOptionError as err: + self.logger.warning(err) + self.creat_config() + else: + self.logger.debug(f"GQRX config does not exist, creating it from template") + self.creat_config() diff --git a/modules/ClearChannelMessages/cog.py b/modules/ClearChannelMessages/cog.py index e902035..1b17f49 100644 --- a/modules/ClearChannelMessages/cog.py +++ b/modules/ClearChannelMessages/cog.py @@ -1,30 +1,51 @@ +import logging from discord.ext import commands -import asyncio + +LOGGER = logging.getLogger("Discord_Radio_Bot.LinkCop") class ClearMessages(commands.Cog): def __init__(self, bot): self.Bot = bot - @commands.command() - async def clear(self, ctx, amount=0): - if amount == 0: - fail = await ctx.send("Please enter an amount to delete!") - await asyncio.sleep(6) - await fail.delete() + @commands.command(name='clear', + help="Use this command to clear a given number of messages from the channel it is called in.\n" + "There is a limit of 100 messages. Please be patient, it may take a while to process a large request." + "Example command:\n" + "\t@ clear\n" + "\t@ clear 10", + breif="Clear x messages in the channel it's called in") + async def clear(self, ctx, amount=2): + member = ctx.author.display_name + member_id = ctx.author.id + mtn_member = f"<@{member_id}>" - if amount < 3: - await ctx.channel.purge(limit=amount) - sucess = await ctx.send( - f"{amount} messages has been deleted ") # sending success msg - await asyncio.sleep(6) # wait 6 seconds - await sucess.delete() # deleting the success msg + if isinstance(amount, int): + if not amount > 0: + ctx.channel.send(f"{member}, the number needs to be positive...") + + else: + LOGGER.info(f"Clear {amount} messages requested by {member}") + + authors = {} + async for message in ctx.channel.history(limit=amount): + if message.author not in authors: + authors[message.author] = 1 + else: + authors[message.author] += 1 + await message.delete() + + msg = f"{mtn_member}, I deleted {sum(authors.values())} messages from {len(authors.keys())} users:\n" + LOGGER.debug(f"Deleted {sum(authors.values())} messages from {ctx.message.channel}") + + for author in authors.keys(): + msg += f"\t{str(author).split('#', 1)[0]}: {authors[author]}\n" + LOGGER.debug(f"Deleted {authors[author]} messages from {author}") + + await ctx.channel.send(msg) else: - if amount == 0: - fail = await ctx.send("Please enter an amount to delete!") - await asyncio.sleep(6) - await fail.delete() + ctx.channel.send(f"{member}, you should check out the 'help' section...") def setup(bot: commands.Bot): diff --git a/op25Handler.py b/op25Handler.py index 947f62a..be20bfc 100644 --- a/op25Handler.py +++ b/op25Handler.py @@ -3,6 +3,7 @@ import shutil import subprocess import threading import time +from BotResources import DEFAULT_RADIO_SETTINGS class OP25Handler(threading.Thread): @@ -12,7 +13,7 @@ class OP25Handler(threading.Thread): self.OP25EXE: str = shutil.which("/home/pi/op25/op25/gr-op25_repeater/apps/rx.py") self.OP25Proc = None - self.Frequency = None + self.Frequency = DEFAULT_RADIO_SETTINGS['freq'] self.HTTP_ENABLED = False @@ -70,7 +71,8 @@ class OP25Handler(threading.Thread): self.logger.debug(f"OP25 Keyword Args: {p25_kwargs}") - self.OP25Proc = subprocess.Popen(p25_kwargs, executable=self.OP25EXE, shell=False, cwd=self.OP25Dir) + self.OP25Proc = subprocess.Popen(p25_kwargs, executable=self.OP25EXE, shell=False, cwd=self.OP25Dir, + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) def close_op25(self): self.logger.info(f"Closing OP25") @@ -81,7 +83,7 @@ class OP25Handler(threading.Thread): while self.OP25Proc.poll() is None: # Terminate the process every 5 seconds if seconds_waited % 5 == 0: - self.logger.debug("Terminating OP25") + self.logger.info("Terminating OP25") self.OP25Proc.terminate() time.sleep(1) self.logger.debug(f"Waited {seconds_waited} seconds") diff --git a/templates/__init__.py b/templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/templates/gqrx_config_template.py b/templates/gqrx_config_template.py new file mode 100644 index 0000000..bf5af44 --- /dev/null +++ b/templates/gqrx_config_template.py @@ -0,0 +1,41 @@ +drb_defaults = """[General] +configversion=2 +crashed=false + +[audio] +gain=-20 +udp_host=localhost + +[dxcluster] +DXCAddress=localhost +DXCFilter= +DXCPort=7300 +DXCSpotTimeout=10 +DXCUsername=nocall + +[fft] +averaging=85 +fft_window=5 +waterfall_span=1 + +[gui] +geometry=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\0\0\x4t\0\0\0$\0\0\a\x7f\0\0\x3\x12\0\0\x4v\0\0\0\x42\0\0\a\x7f\0\0\x3\x12\0\0\0\0\0\0\0\0\a\x80\0\0\x4v\0\0\0\x42\0\0\a\x7f\0\0\x3\x12) +state=@ByteArray(\0\0\0\xff\0\0\0\0\xfd\0\0\0\x2\0\0\0\x1\0\0\x1G\0\0\x2t\xfc\x2\0\0\0\x2\xfc\0\0\0\x42\0\0\x1\x89\0\0\x1\x89\0\b\0!\xfa\0\0\0\x1\x2\0\0\0\x3\xfb\0\0\0\x18\0\x44\0o\0\x63\0k\0I\0n\0p\0u\0t\0\x43\0t\0l\x1\0\0\0\0\xff\xff\xff\xff\0\0\x1P\0\xff\xff\xff\xfb\0\0\0\x12\0\x44\0o\0\x63\0k\0R\0x\0O\0p\0t\x1\0\0\0\0\xff\xff\xff\xff\0\0\x1g\0\a\xff\xff\xfb\0\0\0\xe\0\x44\0o\0\x63\0k\0\x46\0\x66\0t\x1\0\0\0\0\xff\xff\xff\xff\0\0\0\xc8\0\a\xff\xff\xfc\0\0\x1\xd1\0\0\0\xe5\0\0\0\xc3\0\xff\xff\xff\xfa\0\0\0\0\x2\0\0\0\x2\xfb\0\0\0\x12\0\x44\0o\0\x63\0k\0\x41\0u\0\x64\0i\0o\x1\0\0\0\0\xff\xff\xff\xff\0\0\0\xc3\0\xff\xff\xff\xfb\0\0\0\xe\0\x44\0o\0\x63\0k\0R\0\x44\0S\0\0\0\0\0\xff\xff\xff\xff\0\0\0h\0\xff\xff\xff\0\0\0\x3\0\0\0\0\0\0\0\0\xfc\x1\0\0\0\x1\xfb\0\0\0\x1a\0\x44\0o\0\x63\0k\0\x42\0o\0o\0k\0m\0\x61\0r\0k\0s\0\0\0\0\0\xff\xff\xff\xff\0\0\x1\x42\0\xff\xff\xff\0\0\x1\xbd\0\0\x2t\0\0\0\x1\0\0\0\x2\0\0\0\b\0\0\0\x2\xfc\0\0\0\x1\0\0\0\x2\0\0\0\x1\0\0\0\x16\0m\0\x61\0i\0n\0T\0o\0o\0l\0\x42\0\x61\0r\x1\0\0\0\0\xff\xff\xff\xff\0\0\0\0\0\0\0\0) + +[input] +dc_cancel=true +device="rtl=0" +frequency=154785000 +gains=@Variant(\0\0\0\b\0\0\0\x1\0\0\0\x6\0L\0N\0\x41\0\0\0\x2\0\0\x1P) +sample_rate=1800000 + +[receiver] +demod=3 +filter_high_cut=2500 +filter_low_cut=-2500 +offset=-590400 +sql_level=-50 + +[remote_control] +allowed_hosts=localhost, 127.0.0.1, ::1 +enabled=true"""