From 59ee866ac9e317e8d23f01d78511df7b608d7f6b Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sat, 1 Mar 2025 01:31:17 -0500 Subject: [PATCH] Refactored to better split everything up --- .gitignore | 3 +- Dockerfile | 10 +- app/{ => internal}/NoiseGatev2.py | 32 ++--- app/{bot.py => internal/bot_manager.py} | 122 +++++++----------- app/{ => internal}/get_devices.py | 0 app/internal/logger.py | 55 ++++++++ .../internal/opus}/libopus_aarcch64.so | Bin {opus => app/internal/opus}/libopus_amd64.dll | Bin {opus => app/internal/opus}/libopus_armv7l.so | Bin app/main.py | 20 +++ app/models.py | 101 +++++++++++++++ app/routers/bot.py | 62 +++++++++ app/{ => routers}/op25_controller.py | 99 +------------- app/{ => routers}/pulse.py | 0 14 files changed, 312 insertions(+), 192 deletions(-) rename app/{ => internal}/NoiseGatev2.py (87%) rename app/{bot.py => internal/bot_manager.py} (59%) rename app/{ => internal}/get_devices.py (100%) create mode 100644 app/internal/logger.py rename {opus => app/internal/opus}/libopus_aarcch64.so (100%) rename {opus => app/internal/opus}/libopus_amd64.dll (100%) rename {opus => app/internal/opus}/libopus_armv7l.so (100%) create mode 100644 app/main.py create mode 100644 app/models.py create mode 100644 app/routers/bot.py rename app/{ => routers}/op25_controller.py (64%) rename app/{ => routers}/pulse.py (100%) diff --git a/.gitignore b/.gitignore index dd4113b..c947cf5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__* bot-poc.py configs* -.env \ No newline at end of file +.env +*.log \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7f10fa8..61e9ac3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive # Install system dependencies RUN apt-get update && \ - apt-get install -y --no-install-recommends libc-bin apt-transport-https && \ + apt-get install -y --no-install-recommends libc-bin apt-transport-https tzdata && \ apt-get install -y --no-install-recommends git \ curl \ python3 \ @@ -60,11 +60,11 @@ VOLUME ["/configs"] # Set the working directory in the container WORKDIR /app +# Copy opus first to break up the build time +COPY ./app/opus /app/opus + # Copy the rest of the directory contents into the container at /app COPY ./app /app -# Copy the pre-built opus libraries -COPY ./opus /app/opus - # Run the node script -ENTRYPOINT ["uvicorn", "bot:app", "--host", "0.0.0.0", "--port", "8001", "--reload"] \ No newline at end of file +ENTRYPOINT ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001", "--reload"] \ No newline at end of file diff --git a/app/NoiseGatev2.py b/app/internal/NoiseGatev2.py similarity index 87% rename from app/NoiseGatev2.py rename to app/internal/NoiseGatev2.py index 454d343..7cf80d2 100644 --- a/app/NoiseGatev2.py +++ b/app/internal/NoiseGatev2.py @@ -1,15 +1,15 @@ import audioop -import logging import math import time import pyaudio import discord import numpy +from internal.logger import create_logger voice_connection = None -LOGGER = logging.getLogger("Discord_Radio_Bot.NoiseGateV2") +LOGGER = create_logger(__name__) # noinspection PyUnresolvedReferences @@ -30,15 +30,15 @@ class AudioStream: if _input: self.paInstance_kwargs['input_device_index'] = _input_device_index else: - LOGGER.warning("[AudioStream.__init__]:\tInput was not enabled." - " Reinitialize with '_input=True'") + LOGGER.warning(f"[AudioStream.__init__]:\tInput was not enabled." + f" Reinitialize with '_input=True'") if _output_device_index: if _output: self.paInstance_kwargs['output_device_index'] = _output_device_index else: - LOGGER.warning("[AudioStream.__init__]:\tOutput was not enabled." - " Reinitialize with '_output=True'") + LOGGER.warning(f"[AudioStream.__init__]:\tOutput was not enabled." + f" Reinitialize with '_output=True'") if _init_on_startup: # Init PyAudio instance @@ -59,15 +59,15 @@ class AudioStream: if self.paInstance_kwargs['input']: self.paInstance_kwargs['input_device_index'] = _new_input_device_index else: - LOGGER.warning("[AudioStream.init_stream]:\tInput was not enabled when initialized." - " Reinitialize with '_input=True'") + LOGGER.warning(f"[AudioStream.init_stream]:\tInput was not enabled when initialized." + f" Reinitialize with '_input=True'") if _new_output_device_index: if self.paInstance_kwargs['output']: self.paInstance_kwargs['output_device_index'] = _new_output_device_index else: - LOGGER.warning("[AudioStream.init_stream]:\tOutput was not enabled when initialized." - " Reinitialize with '_output=True'") + LOGGER.warning(f"[AudioStream.init_stream]:\tOutput was not enabled when initialized." + f" Reinitialize with '_output=True'") self.close_if_open() @@ -80,7 +80,7 @@ class AudioStream: if self.stream.is_active(): self.stream.stop_stream() self.stream.close() - LOGGER.debug("[ReopenStream.close_if_open]:\t Stream was open; It was closed.") + LOGGER.debug(f"[ReopenStream.close_if_open]:\t Stream was open; It was closed.") def list_devices(self, _display_input_devices: bool = True, _display_output_devices: bool = True): LOGGER.info('Getting a list of the devices connected') @@ -126,7 +126,7 @@ class NoiseGate(AudioStream): def run(self) -> None: global voice_connection # Start the audio stream - LOGGER.debug("Starting stream") + LOGGER.debug(f"Starting stream") self.stream.start_stream() # Start the stream to discord self.core() @@ -139,15 +139,15 @@ class NoiseGate(AudioStream): time.sleep(.2) if not voice_connection.is_playing(): - LOGGER.debug("Playing stream to discord") + LOGGER.debug(f"Playing stream to discord") voice_connection.play(self.NGStream, after=self.core) async def close(self): - LOGGER.debug("Closing") + LOGGER.debug(f"Closing") await voice_connection.disconnect() if self.stream.is_active: self.stream.stop_stream() - LOGGER.debug("Stopping stream") + LOGGER.debug(f"Stopping stream") # noinspection PyUnresolvedReferences @@ -155,7 +155,7 @@ class NoiseGateStream(discord.AudioSource): def __init__(self, _stream): super(NoiseGateStream, self).__init__() self.stream = _stream # The actual audio stream object - self.NG_fadeout = 240 / 20 # Fadeout value used to hold the noisegate after de-triggering + self.NG_fadeout = 240/20 # Fadeout value used to hold the noisegate after de-triggering self.NG_fadeout_count = 0 # A count set when the noisegate is triggered and was de-triggered self.process_set_count = 0 # Counts how many processes have been made diff --git a/app/bot.py b/app/internal/bot_manager.py similarity index 59% rename from app/bot.py rename to app/internal/bot_manager.py index cf379db..e0484da 100644 --- a/app/bot.py +++ b/app/internal/bot_manager.py @@ -1,37 +1,24 @@ import asyncio -import logging -import discord +import platform +import os +from discord import VoiceClient, VoiceChannel, opus, Activity, ActivityType, Intents from discord.ext import commands -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel from typing import Optional, Dict -from NoiseGatev2 import NoiseGate -import op25_controller -import pulse +from internal.NoiseGatev2 import NoiseGate +from internal.logger import create_logger -# Initialize logging -logging.basicConfig(level=logging.INFO) -LOGGER = logging.getLogger(__name__) +LOGGER = create_logger(__name__) -# Define FastAPI app -app = FastAPI() - -intents = discord.Intents.default() +# Configure discord intents +intents = Intents.default() intents.voice_states = True intents.guilds = True -class BotConfig(BaseModel): - token: str - -class VoiceChannelRequest(BaseModel): - guild_id: int - channel_id: int - class DiscordBotManager: def __init__(self): self.bot: Optional[commands.Bot] = None self.bot_task: Optional[asyncio.Task] = None - self.voice_clients: Dict[int, discord.VoiceClient] = {} + self.voice_clients: Dict[int, VoiceClient] = {} self.token: Optional[str] = None self.loop = asyncio.get_event_loop() self.lock = asyncio.Lock() @@ -57,13 +44,16 @@ class DiscordBotManager: guild_id = before.channel.guild.id LOGGER.info(f"Bot was disconnected from channel in guild {guild_id}. Attempting to reconnect...") try: - leave_voice_channel(guild_id) + await leave_voice_channel(guild_id) except Exception as e: LOGGER.warning(f"Error leaving voice channel: '{e}'") # Attempt to reconnect to the channel after a brief pause await asyncio.sleep(2) await self.join_voice_channel(guild_id, before.channel.id) + # Load Opus for the current CPU + await self.load_opus() + self.bot_task = self.loop.create_task(self.bot.start(token)) async def stop_bot(self): @@ -85,22 +75,27 @@ class DiscordBotManager: if not guild: raise ValueError("Guild not found.") + if not opus.is_loaded(): + raise RuntimeError("Opus is not loaded.") + channel = guild.get_channel(channel_id) - if not isinstance(channel, discord.VoiceChannel): + if not isinstance(channel, VoiceChannel): raise ValueError("Channel is not a voice channel.") if guild_id in self.voice_clients: raise RuntimeError("Already connected to this guild's voice channel.") - + try: voice_client = await channel.connect(timeout=60.0, reconnect=True) + LOGGER.debug(f"Voice Connected.") streamHandler = NoiseGate( _input_device_index=device_id, _voice_connection=voice_client, _noise_gate_threshold=ng_threshold) streamHandler.run() + LOGGER.debug(f"Stream is running.") self.voice_clients[guild_id] = voice_client - LOGGER.info(f"Joined guild {guild_id} voice channel {channel_id}.") + LOGGER.info(f"Joined guild {guild_id} voice channel {channel_id} and stream is running.") except Exception as e: LOGGER.error(f"Failed to connect to voice channel: {e}") @@ -116,54 +111,29 @@ class DiscordBotManager: del self.voice_clients[guild_id] LOGGER.info(f"Left guild {guild_id} voice channel.") + async def load_opus(self): + """ Load the proper OPUS library for the device being used """ + processor = platform.machine() + script_dir = os.path.dirname(os.path.abspath(__file__)) + LOGGER.debug("Processor: ", processor) + if os.name == 'nt': + if processor == "AMD64": + opus.load_opus(os.path.join(script_dir, './opus/libopus_amd64.dll')) + LOGGER.info(f"Loaded OPUS library for AMD64") + return "AMD64" + else: + if processor == "aarch64": + opus.load_opus(os.path.join(script_dir, './opus/libopus_aarcch64.so')) + LOGGER.info(f"Loaded OPUS library for aarch64") + return "aarch64" + elif processor == "armv7l": + opus.load_opus(os.path.join(script_dir, './opus/libopus_armv7l.so')) + LOGGER.info(f"Loaded OPUS library for armv7l") + return "armv7l" -# Initialize Discord Bot Manager -bot_manager = DiscordBotManager() - -# API Endpoints -@app.post("/start_bot") -async def start_bot(config: BotConfig): - try: - await bot_manager.start_bot(config.token) - return {"status": "Bot started successfully."} - except Exception as e: - LOGGER.error(f"Error starting bot: {e}") - raise HTTPException(status_code=400, detail=str(e)) - -@app.post("/stop_bot") -async def stop_bot(): - try: - await bot_manager.stop_bot() - return {"status": "Bot stopped successfully."} - except Exception as e: - LOGGER.error(f"Error stopping bot: {e}") - raise HTTPException(status_code=400, detail=str(e)) - -@app.post("/join_voice") -async def join_voice_channel(request: VoiceChannelRequest): - try: - await bot_manager.join_voice_channel(request.guild_id, request.channel_id) - return {"status": f"Joined guild {request.guild_id} voice channel {request.channel_id}."} - except Exception as e: - LOGGER.error(f"Error joining voice channel: {e}") - raise HTTPException(status_code=400, detail=str(e)) - -@app.post("/leave_voice") -async def leave_voice_channel(request: VoiceChannelRequest): - try: - await bot_manager.leave_voice_channel(request.guild_id) - return {"status": f"Left guild {request.guild_id} voice channel."} - except Exception as e: - LOGGER.error(f"Error leaving voice channel: {e}") - raise HTTPException(status_code=400, detail=str(e)) - -@app.get("/status") -async def get_status(): - status = { - "bot_running": bot_manager.bot is not None and not bot_manager.bot.is_closed(), - "connected_guilds": list(bot_manager.voice_clients.keys()) - } - return status - -app.include_router(op25_controller.router, prefix="/op25") -app.include_router(pulse.router, prefix="/pulse") + async def set_presence(self, presence: str): + """ Set the presense (activity) of the bot """ + try: + await self.bot.change_presence(activity=Activity(type=ActivityType.listening, name=presence)) + except Exception as pe: + LOGGER.error(f"Unable to set presence: '{pe}'") \ No newline at end of file diff --git a/app/get_devices.py b/app/internal/get_devices.py similarity index 100% rename from app/get_devices.py rename to app/internal/get_devices.py diff --git a/app/internal/logger.py b/app/internal/logger.py new file mode 100644 index 0000000..67ffc98 --- /dev/null +++ b/app/internal/logger.py @@ -0,0 +1,55 @@ +import logging +from logging.handlers import RotatingFileHandler + +def create_logger(name, level=logging.DEBUG, max_bytes=10485760, backup_count=2): + """ + Creates a logger with a console and rotating file handlers for both debug and info log levels. + + Args: + name (str): The name for the logger. + level (int): The logging level for the logger. Defaults to logging.DEBUG. + max_bytes (int): Maximum size of the log file in bytes before it gets rotated. Defaults to 10 MB. + backup_count (int): Number of backup files to keep. Defaults to 2. + + Returns: + logging.Logger: Configured logger. + """ + # Set the log file paths + debug_log_file = "./client.debug.log" + info_log_file = "./client.log" + + # Create a logger + logger = logging.getLogger(name) + logger.setLevel(level) + + # Check if the logger already has handlers to avoid duplicate logs + if not logger.hasHandlers(): + # Create console handler + console_handler = logging.StreamHandler() + console_handler.setLevel(level) + + # Create rotating file handler for debug level + debug_file_handler = RotatingFileHandler(debug_log_file, maxBytes=max_bytes, backupCount=backup_count) + debug_file_handler.setLevel(logging.DEBUG) + + # Create rotating file handler for info level + info_file_handler = RotatingFileHandler(info_log_file, maxBytes=max_bytes, backupCount=backup_count) + info_file_handler.setLevel(logging.INFO) + + # Create formatter and add it to the handlers + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + console_handler.setFormatter(formatter) + debug_file_handler.setFormatter(formatter) + info_file_handler.setFormatter(formatter) + + # Add the handlers to the logger + logger.addHandler(console_handler) + logger.addHandler(debug_file_handler) + logger.addHandler(info_file_handler) + + return logger + +# Example usage: +# logger = create_logger('my_logger') +# logger.debug('This is a debug message') +# logger.info('This is an info message') \ No newline at end of file diff --git a/opus/libopus_aarcch64.so b/app/internal/opus/libopus_aarcch64.so similarity index 100% rename from opus/libopus_aarcch64.so rename to app/internal/opus/libopus_aarcch64.so diff --git a/opus/libopus_amd64.dll b/app/internal/opus/libopus_amd64.dll similarity index 100% rename from opus/libopus_amd64.dll rename to app/internal/opus/libopus_amd64.dll diff --git a/opus/libopus_armv7l.so b/app/internal/opus/libopus_armv7l.so similarity index 100% rename from opus/libopus_armv7l.so rename to app/internal/opus/libopus_armv7l.so diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..3d350a9 --- /dev/null +++ b/app/main.py @@ -0,0 +1,20 @@ +import asyncio +import discord +from discord.ext import commands +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from typing import Optional, Dict +import routers.op25_controller as op25_controller +import routers.pulse as pulse +import routers.bot as bot +from internal.logger import create_logger + +# Initialize logging +LOGGER = create_logger(__name__) + +# Define FastAPI app +app = FastAPI() + +app.include_router(op25_controller.router, prefix="/op25") +app.include_router(pulse.router, prefix="/pulse") +app.include_router(bot.router, prefix="/bot") diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..606aa2f --- /dev/null +++ b/app/models.py @@ -0,0 +1,101 @@ +from pydantic import BaseModel +from typing import List, Optional +from enum import Enum + + +class BotConfig(BaseModel): + token: str + +class VoiceChannelRequest(BaseModel): + guild_id: int + channel_id: int + +class DecodeMode(str, Enum): + P25 = "P25" + DMR = "DMR" + ANALOG = "NBFM" + +class TalkgroupTag(BaseModel): + talkgroup: str + tagDec: int + +class ConfigGenerator(BaseModel): + type: DecodeMode + systemName: str + channels: List[str] + tags: Optional[List[TalkgroupTag]] + whitelist: Optional[List[int]] + +class DemodType(str, Enum): + CQPSK = "cqpsk" + FSK4 = "fsk4" + +class FilterType(str, Enum): + RC = "rc" + WIDEPULSE = "widepulse" + +class ChannelConfig(BaseModel): + name: str + trunking_sysname: Optional[str] + enable_analog: str + demod_type: DemodType + filter_type: FilterType + device: Optional[str] = "sdr" + cqpsk_tracking: Optional[bool] = None + frequency: Optional[float] = None + nbfmSquelch: Optional[float] = None + destination: Optional[str] = "udp://127.0.0.1:23456" + tracking_threshold: Optional[int] = 120 + tracking_feedback: Optional[float] = 0.75 + excess_bw: Optional[float] = 0.2 + if_rate: Optional[int] = 24000 + plot: Optional[str] = "" + symbol_rate: Optional[int] = 4800 + blacklist: Optional[str] = "" + whitelist: Optional[str] = "" + +class DeviceConfig(BaseModel): + args: Optional[str] = "rtl" + gains: Optional[str] = "lna:39" + gain_mode: Optional[bool] = False + name: Optional[str] = "sdr" + offset: Optional[int] = 0 + ppm: Optional[float] = 0.0 + rate: Optional[int] = 1920000 + usable_bw_pct: Optional[float] = 0.85 + tunable: Optional[bool] = True + +class TrunkingChannelConfig(BaseModel): + sysname: str + control_channel_list: str + tagsFile: Optional[str] = None + whitelist: Optional[str] = None + nac: Optional[str] = "" + wacn: Optional[str] = "" + tdma_cc: Optional[bool] = False + crypt_behavior: Optional[int] = 2 + +class TrunkingConfig(BaseModel): + module: str + chans: List[TrunkingChannelConfig] + +class AudioInstanceConfig(BaseModel): + instance_name: Optional[str] = "audio0" + device_name: Optional[str] = "pulse" + udp_port: Optional[int] = 23456 + audio_gain: Optional[float] = 2.5 + number_channels: Optional[int] = 1 + +class AudioConfig(BaseModel): + module: Optional[str] = "sockaudio.py" + instances: Optional[List[AudioInstanceConfig]] = [AudioInstanceConfig()] + +class TerminalConfig(BaseModel): + module: Optional[str] = "terminal.py" + terminal_type: Optional[str] = "http:0.0.0.0:8081" + terminal_timeout: Optional[float] = 5.0 + curses_plot_interval: Optional[float] = 0.2 + http_plot_interval: Optional[float] = 1.0 + http_plot_directory: Optional[str] = "../www/images" + tuning_step_large: Optional[int] = 1200 + tuning_step_small: Optional[int] = 100 diff --git a/app/routers/bot.py b/app/routers/bot.py new file mode 100644 index 0000000..9c9e4bf --- /dev/null +++ b/app/routers/bot.py @@ -0,0 +1,62 @@ +import asyncio +import discord +from discord.ext import commands +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from typing import Optional, Dict +from models import BotConfig, VoiceChannelRequest +from internal.bot_manager import DiscordBotManager +from internal.logger import create_logger + +LOGGER = create_logger(__name__) + +# Define FastAPI app +router = APIRouter() + +# Initialize Discord Bot Manager +bot_manager = DiscordBotManager() + +# API Endpoints +@router.post("/start_bot") +async def start_bot(config: BotConfig): + try: + await bot_manager.start_bot(config.token) + return {"status": "Bot started successfully."} + except Exception as e: + LOGGER.error(f"Error starting bot: {e}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/stop_bot") +async def stop_bot(): + try: + await bot_manager.stop_bot() + return {"status": "Bot stopped successfully."} + except Exception as e: + LOGGER.error(f"Error stopping bot: {e}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/join_voice") +async def join_voice_channel(request: VoiceChannelRequest): + try: + await bot_manager.join_voice_channel(request.guild_id, request.channel_id) + return {"status": f"Joined guild {request.guild_id} voice channel {request.channel_id}."} + except Exception as e: + LOGGER.error(f"Error joining voice channel: {e}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/leave_voice") +async def leave_voice_channel(request: VoiceChannelRequest): + try: + await bot_manager.leave_voice_channel(request.guild_id) + return {"status": f"Left guild {request.guild_id} voice channel."} + except Exception as e: + LOGGER.error(f"Error leaving voice channel: {e}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/status") +async def get_status(): + status = { + "bot_running": bot_manager.bot is not None and not bot_manager.bot.is_closed(), + "connected_guilds": list(bot_manager.voice_clients.keys()) + } + return status diff --git a/app/op25_controller.py b/app/routers/op25_controller.py similarity index 64% rename from app/op25_controller.py rename to app/routers/op25_controller.py index 188de66..10ea6f2 100644 --- a/app/op25_controller.py +++ b/app/routers/op25_controller.py @@ -1,14 +1,15 @@ from fastapi import HTTPException, APIRouter from pydantic import BaseModel -from enum import Enum import subprocess import os import signal import json import csv -from typing import List, Optional +from models import * +from internal.logger import create_logger router = APIRouter() +LOGGER = create_logger(__name__) op25_process = None OP25_PATH = "/op25/op25/gr-op25_repeater/apps/" @@ -20,7 +21,7 @@ async def start_op25(): if op25_process is None: try: op25_process = subprocess.Popen(os.path.join(OP25_PATH, OP25_SCRIPT), shell=True, preexec_fn=os.setsid, cwd=OP25_PATH) - print(op25_process) + LOGGER.debug(op25_process) return {"status": "OP25 started"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -44,96 +45,6 @@ async def stop_op25(): async def get_status(): return {"status": "running" if op25_process else "stopped"} -class DecodeMode(str, Enum): - P25 = "P25" - DMR = "DMR" - ANALOG = "NBFM" - -class TalkgroupTag(BaseModel): - talkgroup: str - tagDec: int - -class ConfigGenerator(BaseModel): - type: DecodeMode - systemName: str - channels: List[str] - tags: List[TalkgroupTag] - whitelist: List[int] - -class DemodType(str, Enum): - CQPSK = "cqpsk" - FSK4 = "fsk4" - -class FilterType(str, Enum): - RC = "rc" - WIDEPULSE = "widepulse" - -class ChannelConfig(BaseModel): - name: str - trunking_sysname: Optional[str] - enable_analog: str - demod_type: DemodType - filter_type: FilterType - device: Optional[str] = "sdr" - cqpsk_tracking: Optional[bool] = None - frequency: Optional[float] = None - nbfmSquelch: Optional[float] = None - destination: Optional[str] = "udp://127.0.0.1:23456" - tracking_threshold: Optional[int] = 120 - tracking_feedback: Optional[float] = 0.75 - excess_bw: Optional[float] = 0.2 - if_rate: Optional[int] = 24000 - plot: Optional[str] = "" - symbol_rate: Optional[int] = 4800 - blacklist: Optional[str] = "" - whitelist: Optional[str] = "" - -class DeviceConfig(BaseModel): - args: Optional[str] = "rtl" - gains: Optional[str] = "lna:39" - gain_mode: Optional[bool] = False - name: Optional[str] = "sdr" - offset: Optional[int] = 0 - ppm: Optional[float] = 0.0 - rate: Optional[int] = 1920000 - usable_bw_pct: Optional[float] = 0.85 - tunable: Optional[bool] = True - -class TrunkingChannelConfig(BaseModel): - sysname: str - control_channel_list: str - tagsFile: Optional[str] = None - whitelist: Optional[str] = None - nac: Optional[str] = "" - wacn: Optional[str] = "" - tdma_cc: Optional[bool] = False - crypt_behavior: Optional[int] = 2 - -class TrunkingConfig(BaseModel): - module: str - chans: List[TrunkingChannelConfig] - -class AudioInstanceConfig(BaseModel): - instance_name: Optional[str] = "audio0" - device_name: Optional[str] = "pulse" - udp_port: Optional[int] = 23456 - audio_gain: Optional[float] = 2.5 - number_channels: Optional[int] = 1 - -class AudioConfig(BaseModel): - module: Optional[str] = "sockaudio.py" - instances: Optional[List[AudioInstanceConfig]] = [AudioInstanceConfig()] - -class TerminalConfig(BaseModel): - module: Optional[str] = "terminal.py" - terminal_type: Optional[str] = "http:0.0.0.0:8081" - terminal_timeout: Optional[float] = 5.0 - curses_plot_interval: Optional[float] = 0.2 - http_plot_interval: Optional[float] = 1.0 - http_plot_directory: Optional[str] = "../www/images" - tuning_step_large: Optional[int] = 1200 - tuning_step_small: Optional[int] = 100 - @router.post("/generate-config") async def generate_config(generator: ConfigGenerator): try: @@ -231,7 +142,7 @@ def del_none_in_dict(d): This alters the input so you may wish to ``copy`` the dict first. """ for key, value in list(d.items()): - print(f"Key: '{key}'\nValue: '{value}'") + LOGGER.info(f"Key: '{key}'\nValue: '{value}'") if value is None: del d[key] elif isinstance(value, dict): diff --git a/app/pulse.py b/app/routers/pulse.py similarity index 100% rename from app/pulse.py rename to app/routers/pulse.py