Refactored to better split everything up

This commit is contained in:
2025-03-01 01:31:17 -05:00
parent f1de077b72
commit 59ee866ac9
14 changed files with 312 additions and 192 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@ __pycache__*
bot-poc.py bot-poc.py
configs* configs*
.env .env
*.log

View File

@@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive
# Install system dependencies # Install system dependencies
RUN apt-get update && \ 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 \ apt-get install -y --no-install-recommends git \
curl \ curl \
python3 \ python3 \
@@ -60,11 +60,11 @@ VOLUME ["/configs"]
# Set the working directory in the container # Set the working directory in the container
WORKDIR /app 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 the rest of the directory contents into the container at /app
COPY ./app /app COPY ./app /app
# Copy the pre-built opus libraries
COPY ./opus /app/opus
# Run the node script # Run the node script
ENTRYPOINT ["uvicorn", "bot:app", "--host", "0.0.0.0", "--port", "8001", "--reload"] ENTRYPOINT ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001", "--reload"]

View File

@@ -1,15 +1,15 @@
import audioop import audioop
import logging
import math import math
import time import time
import pyaudio import pyaudio
import discord import discord
import numpy import numpy
from internal.logger import create_logger
voice_connection = None voice_connection = None
LOGGER = logging.getLogger("Discord_Radio_Bot.NoiseGateV2") LOGGER = create_logger(__name__)
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@@ -30,15 +30,15 @@ class AudioStream:
if _input: if _input:
self.paInstance_kwargs['input_device_index'] = _input_device_index self.paInstance_kwargs['input_device_index'] = _input_device_index
else: else:
LOGGER.warning("[AudioStream.__init__]:\tInput was not enabled." LOGGER.warning(f"[AudioStream.__init__]:\tInput was not enabled."
" Reinitialize with '_input=True'") f" Reinitialize with '_input=True'")
if _output_device_index: if _output_device_index:
if _output: if _output:
self.paInstance_kwargs['output_device_index'] = _output_device_index self.paInstance_kwargs['output_device_index'] = _output_device_index
else: else:
LOGGER.warning("[AudioStream.__init__]:\tOutput was not enabled." LOGGER.warning(f"[AudioStream.__init__]:\tOutput was not enabled."
" Reinitialize with '_output=True'") f" Reinitialize with '_output=True'")
if _init_on_startup: if _init_on_startup:
# Init PyAudio instance # Init PyAudio instance
@@ -59,15 +59,15 @@ class AudioStream:
if self.paInstance_kwargs['input']: if self.paInstance_kwargs['input']:
self.paInstance_kwargs['input_device_index'] = _new_input_device_index self.paInstance_kwargs['input_device_index'] = _new_input_device_index
else: else:
LOGGER.warning("[AudioStream.init_stream]:\tInput was not enabled when initialized." LOGGER.warning(f"[AudioStream.init_stream]:\tInput was not enabled when initialized."
" Reinitialize with '_input=True'") f" Reinitialize with '_input=True'")
if _new_output_device_index: if _new_output_device_index:
if self.paInstance_kwargs['output']: if self.paInstance_kwargs['output']:
self.paInstance_kwargs['output_device_index'] = _new_output_device_index self.paInstance_kwargs['output_device_index'] = _new_output_device_index
else: else:
LOGGER.warning("[AudioStream.init_stream]:\tOutput was not enabled when initialized." LOGGER.warning(f"[AudioStream.init_stream]:\tOutput was not enabled when initialized."
" Reinitialize with '_output=True'") f" Reinitialize with '_output=True'")
self.close_if_open() self.close_if_open()
@@ -80,7 +80,7 @@ class AudioStream:
if self.stream.is_active(): if self.stream.is_active():
self.stream.stop_stream() self.stream.stop_stream()
self.stream.close() 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): def list_devices(self, _display_input_devices: bool = True, _display_output_devices: bool = True):
LOGGER.info('Getting a list of the devices connected') LOGGER.info('Getting a list of the devices connected')
@@ -126,7 +126,7 @@ class NoiseGate(AudioStream):
def run(self) -> None: def run(self) -> None:
global voice_connection global voice_connection
# Start the audio stream # Start the audio stream
LOGGER.debug("Starting stream") LOGGER.debug(f"Starting stream")
self.stream.start_stream() self.stream.start_stream()
# Start the stream to discord # Start the stream to discord
self.core() self.core()
@@ -139,15 +139,15 @@ class NoiseGate(AudioStream):
time.sleep(.2) time.sleep(.2)
if not voice_connection.is_playing(): 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) voice_connection.play(self.NGStream, after=self.core)
async def close(self): async def close(self):
LOGGER.debug("Closing") LOGGER.debug(f"Closing")
await voice_connection.disconnect() await voice_connection.disconnect()
if self.stream.is_active: if self.stream.is_active:
self.stream.stop_stream() self.stream.stop_stream()
LOGGER.debug("Stopping stream") LOGGER.debug(f"Stopping stream")
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@@ -155,7 +155,7 @@ class NoiseGateStream(discord.AudioSource):
def __init__(self, _stream): def __init__(self, _stream):
super(NoiseGateStream, self).__init__() super(NoiseGateStream, self).__init__()
self.stream = _stream # The actual audio stream object 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.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 self.process_set_count = 0 # Counts how many processes have been made

View File

@@ -1,37 +1,24 @@
import asyncio import asyncio
import logging import platform
import discord import os
from discord import VoiceClient, VoiceChannel, opus, Activity, ActivityType, Intents
from discord.ext import commands from discord.ext import commands
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional, Dict from typing import Optional, Dict
from NoiseGatev2 import NoiseGate from internal.NoiseGatev2 import NoiseGate
import op25_controller from internal.logger import create_logger
import pulse
# Initialize logging LOGGER = create_logger(__name__)
logging.basicConfig(level=logging.INFO)
LOGGER = logging.getLogger(__name__)
# Define FastAPI app # Configure discord intents
app = FastAPI() intents = Intents.default()
intents = discord.Intents.default()
intents.voice_states = True intents.voice_states = True
intents.guilds = True intents.guilds = True
class BotConfig(BaseModel):
token: str
class VoiceChannelRequest(BaseModel):
guild_id: int
channel_id: int
class DiscordBotManager: class DiscordBotManager:
def __init__(self): def __init__(self):
self.bot: Optional[commands.Bot] = None self.bot: Optional[commands.Bot] = None
self.bot_task: Optional[asyncio.Task] = 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.token: Optional[str] = None
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.lock = asyncio.Lock() self.lock = asyncio.Lock()
@@ -57,13 +44,16 @@ class DiscordBotManager:
guild_id = before.channel.guild.id guild_id = before.channel.guild.id
LOGGER.info(f"Bot was disconnected from channel in guild {guild_id}. Attempting to reconnect...") LOGGER.info(f"Bot was disconnected from channel in guild {guild_id}. Attempting to reconnect...")
try: try:
leave_voice_channel(guild_id) await leave_voice_channel(guild_id)
except Exception as e: except Exception as e:
LOGGER.warning(f"Error leaving voice channel: '{e}'") LOGGER.warning(f"Error leaving voice channel: '{e}'")
# Attempt to reconnect to the channel after a brief pause # Attempt to reconnect to the channel after a brief pause
await asyncio.sleep(2) await asyncio.sleep(2)
await self.join_voice_channel(guild_id, before.channel.id) 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)) self.bot_task = self.loop.create_task(self.bot.start(token))
async def stop_bot(self): async def stop_bot(self):
@@ -85,8 +75,11 @@ class DiscordBotManager:
if not guild: if not guild:
raise ValueError("Guild not found.") raise ValueError("Guild not found.")
if not opus.is_loaded():
raise RuntimeError("Opus is not loaded.")
channel = guild.get_channel(channel_id) 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.") raise ValueError("Channel is not a voice channel.")
if guild_id in self.voice_clients: if guild_id in self.voice_clients:
@@ -94,13 +87,15 @@ class DiscordBotManager:
try: try:
voice_client = await channel.connect(timeout=60.0, reconnect=True) voice_client = await channel.connect(timeout=60.0, reconnect=True)
LOGGER.debug(f"Voice Connected.")
streamHandler = NoiseGate( streamHandler = NoiseGate(
_input_device_index=device_id, _input_device_index=device_id,
_voice_connection=voice_client, _voice_connection=voice_client,
_noise_gate_threshold=ng_threshold) _noise_gate_threshold=ng_threshold)
streamHandler.run() streamHandler.run()
LOGGER.debug(f"Stream is running.")
self.voice_clients[guild_id] = voice_client 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: except Exception as e:
LOGGER.error(f"Failed to connect to voice channel: {e}") LOGGER.error(f"Failed to connect to voice channel: {e}")
@@ -116,54 +111,29 @@ class DiscordBotManager:
del self.voice_clients[guild_id] del self.voice_clients[guild_id]
LOGGER.info(f"Left guild {guild_id} voice channel.") 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 async def set_presence(self, presence: str):
bot_manager = DiscordBotManager() """ Set the presense (activity) of the bot """
try:
# API Endpoints await self.bot.change_presence(activity=Activity(type=ActivityType.listening, name=presence))
@app.post("/start_bot") except Exception as pe:
async def start_bot(config: BotConfig): LOGGER.error(f"Unable to set presence: '{pe}'")
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")

55
app/internal/logger.py Normal file
View File

@@ -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')

20
app/main.py Normal file
View File

@@ -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")

101
app/models.py Normal file
View File

@@ -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

62
app/routers/bot.py Normal file
View File

@@ -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

View File

@@ -1,14 +1,15 @@
from fastapi import HTTPException, APIRouter from fastapi import HTTPException, APIRouter
from pydantic import BaseModel from pydantic import BaseModel
from enum import Enum
import subprocess import subprocess
import os import os
import signal import signal
import json import json
import csv import csv
from typing import List, Optional from models import *
from internal.logger import create_logger
router = APIRouter() router = APIRouter()
LOGGER = create_logger(__name__)
op25_process = None op25_process = None
OP25_PATH = "/op25/op25/gr-op25_repeater/apps/" OP25_PATH = "/op25/op25/gr-op25_repeater/apps/"
@@ -20,7 +21,7 @@ async def start_op25():
if op25_process is None: if op25_process is None:
try: try:
op25_process = subprocess.Popen(os.path.join(OP25_PATH, OP25_SCRIPT), shell=True, preexec_fn=os.setsid, cwd=OP25_PATH) 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"} return {"status": "OP25 started"}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@@ -44,96 +45,6 @@ async def stop_op25():
async def get_status(): async def get_status():
return {"status": "running" if op25_process else "stopped"} 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") @router.post("/generate-config")
async def generate_config(generator: ConfigGenerator): async def generate_config(generator: ConfigGenerator):
try: try:
@@ -231,7 +142,7 @@ def del_none_in_dict(d):
This alters the input so you may wish to ``copy`` the dict first. This alters the input so you may wish to ``copy`` the dict first.
""" """
for key, value in list(d.items()): for key, value in list(d.items()):
print(f"Key: '{key}'\nValue: '{value}'") LOGGER.info(f"Key: '{key}'\nValue: '{value}'")
if value is None: if value is None:
del d[key] del d[key]
elif isinstance(value, dict): elif isinstance(value, dict):