You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
285 lines
9.5 KiB
285 lines
9.5 KiB
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-2.0-only
|
|
#/**
|
|
#* Digital Voice Modem - Host Configuration Generator
|
|
#* GPLv2 Open Source. Use is subject to license terms.
|
|
#* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
#*
|
|
#*/
|
|
#/*
|
|
#* Copyright (C) 2026 by Bryan Biedenkapp N2PLL
|
|
#*
|
|
#* This program is free software; you can redistribute it and/or modify
|
|
#* it under the terms of the GNU General Public License as published by
|
|
#* the Free Software Foundation; either version 2 of the License, or
|
|
#* (at your option) any later version.
|
|
#*
|
|
#* This program is distributed in the hope that it will be useful,
|
|
#* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
#* GNU General Public License for more details.
|
|
#*
|
|
#* You should have received a copy of the GNU General Public License
|
|
#* along with this program; if not, write to the Free Software
|
|
#* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
#*/
|
|
"""
|
|
dvmcfggen - DVMHost Configuration Generator
|
|
|
|
Pre-configured templates loaded from dvmhost config.example.yml
|
|
Ensures all configuration parameters are present
|
|
"""
|
|
|
|
from typing import Dict, Any, Optional
|
|
import secrets
|
|
import yaml
|
|
from pathlib import Path
|
|
|
|
|
|
# Possible locations for config.example.yml
|
|
CONFIG_EXAMPLE_PATHS = [
|
|
Path(__file__).parent.parent / 'configs' / 'config.example.yml',
|
|
Path('/opt/dvm/config.example.yml'),
|
|
Path('./config.example.yml'),
|
|
]
|
|
|
|
|
|
def find_config_example() -> Optional[Path]:
|
|
"""Find the config.example.yml file from dvmhost"""
|
|
for path in CONFIG_EXAMPLE_PATHS:
|
|
if path.exists():
|
|
return path
|
|
return None
|
|
|
|
|
|
def load_base_config() -> Dict[str, Any]:
|
|
"""
|
|
Load the base configuration from config.example.yml
|
|
Falls back to minimal config if not found
|
|
"""
|
|
example_path = find_config_example()
|
|
|
|
if example_path:
|
|
try:
|
|
with open(example_path, 'r') as f:
|
|
config = yaml.safe_load(f)
|
|
if config:
|
|
return config
|
|
except Exception as e:
|
|
print(f"Warning: Could not load {example_path}: {e}")
|
|
|
|
# Fallback to minimal config if example not found
|
|
print("Warning: config.example.yml not found, using minimal fallback config")
|
|
return get_minimal_fallback_config()
|
|
|
|
|
|
def get_minimal_fallback_config() -> Dict[str, Any]:
|
|
"""Minimal fallback configuration if config.example.yml is not available"""
|
|
return {
|
|
'daemon': True,
|
|
'log': {
|
|
'displayLevel': 1,
|
|
'fileLevel': 1,
|
|
'filePath': '.',
|
|
'activityFilePath': '.',
|
|
'fileRoot': 'DVM'
|
|
},
|
|
'network': {
|
|
'enable': True,
|
|
'id': 100000,
|
|
'address': '127.0.0.1',
|
|
'port': 62031,
|
|
'password': 'PASSWORD',
|
|
'rpcAddress': '127.0.0.1',
|
|
'rpcPort': 9890,
|
|
'rpcPassword': 'ULTRA-VERY-SECURE-DEFAULT',
|
|
},
|
|
'system': {
|
|
'identity': 'DVM',
|
|
'timeout': 180,
|
|
'duplex': True,
|
|
'modeHang': 10,
|
|
'rfTalkgroupHang': 10,
|
|
'activeTickDelay': 5,
|
|
'idleTickDelay': 5,
|
|
'info': {
|
|
'latitude': 0.0,
|
|
'longitude': 0.0,
|
|
'height': 1,
|
|
'power': 0,
|
|
'location': 'Anywhere'
|
|
},
|
|
'config': {
|
|
'authoritative': True,
|
|
'supervisor': False,
|
|
'channelId': 1,
|
|
'channelNo': 1,
|
|
'serviceClass': 1,
|
|
'siteId': 1,
|
|
'dmrNetId': 1,
|
|
'dmrColorCode': 1,
|
|
'p25NAC': 0x293,
|
|
'p25NetId': 0xBB800,
|
|
'nxdnRAN': 1
|
|
},
|
|
'cwId': {
|
|
'enable': True,
|
|
'time': 10,
|
|
'callsign': 'MYCALL'
|
|
},
|
|
'modem': {
|
|
'protocol': {
|
|
'type': 'uart',
|
|
'uart': {
|
|
'port': '/dev/ttyUSB0',
|
|
'speed': 115200
|
|
}
|
|
},
|
|
'rxLevel': 50,
|
|
'txLevel': 50,
|
|
'rxDCOffset': 0,
|
|
'txDCOffset': 0
|
|
}
|
|
},
|
|
'protocols': {
|
|
'dmr': {
|
|
'enable': True,
|
|
'control': {
|
|
'enable': True,
|
|
'dedicated': False
|
|
}
|
|
},
|
|
'p25': {
|
|
'enable': True,
|
|
'control': {
|
|
'enable': True,
|
|
'dedicated': False
|
|
}
|
|
},
|
|
'nxdn': {
|
|
'enable': False,
|
|
'control': {
|
|
'enable': False,
|
|
'dedicated': False
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
def generate_secure_key(length: int = 64) -> str:
|
|
"""Generate a secure random hex key"""
|
|
return secrets.token_hex(length // 2).upper()
|
|
|
|
|
|
def apply_template_customizations(config: Dict[str, Any], template_type: str) -> Dict[str, Any]:
|
|
"""
|
|
Apply template-specific customizations to the base config
|
|
|
|
Args:
|
|
config: Base configuration dict
|
|
template_type: Template name (repeater, etc.)
|
|
|
|
Returns:
|
|
Customized configuration dict
|
|
"""
|
|
# Make a deep copy to avoid modifying the original
|
|
import copy
|
|
config = copy.deepcopy(config)
|
|
|
|
if template_type == 'control-channel-p25':
|
|
# P25 dedicated control channel
|
|
config['system']['duplex'] = True
|
|
config['system']['identity'] = 'CC-P25'
|
|
config['system']['config']['authoritative'] = True
|
|
config['system']['config']['supervisor'] = True
|
|
config['protocols']['dmr']['enable'] = False
|
|
config['protocols']['p25']['enable'] = True
|
|
config['protocols']['nxdn']['enable'] = False
|
|
config['protocols']['p25']['control']['enable'] = True
|
|
config['protocols']['p25']['control']['dedicated'] = True
|
|
|
|
elif template_type == 'control-channel-dmr':
|
|
# DMR dedicated control channel
|
|
config['system']['duplex'] = True
|
|
config['system']['identity'] = 'CC-DMR'
|
|
config['system']['config']['authoritative'] = True
|
|
config['system']['config']['supervisor'] = True
|
|
config['protocols']['dmr']['enable'] = True
|
|
config['protocols']['p25']['enable'] = False
|
|
config['protocols']['nxdn']['enable'] = False
|
|
config['protocols']['dmr']['control']['enable'] = True
|
|
config['protocols']['dmr']['control']['dedicated'] = True
|
|
|
|
elif template_type == 'voice-channel':
|
|
# Voice channel for trunking
|
|
config['system']['duplex'] = True
|
|
config['system']['identity'] = 'VC'
|
|
config['system']['config']['authoritative'] = False
|
|
config['system']['config']['supervisor'] = False
|
|
config['protocols']['dmr']['enable'] = True
|
|
config['protocols']['p25']['enable'] = True
|
|
config['protocols']['nxdn']['enable'] = False
|
|
config['protocols']['dmr']['control']['enable'] = False
|
|
config['protocols']['dmr']['control']['dedicated'] = False
|
|
config['protocols']['p25']['control']['enable'] = False
|
|
config['protocols']['p25']['control']['dedicated'] = False
|
|
|
|
elif template_type == 'enhanced':
|
|
# Enhanced conventional repeater with grants
|
|
config['system']['duplex'] = True
|
|
config['system']['identity'] = 'CONV'
|
|
config['protocols']['dmr']['enable'] = True
|
|
config['protocols']['p25']['enable'] = True
|
|
config['protocols']['nxdn']['enable'] = False
|
|
config['protocols']['dmr']['control']['enable'] = True
|
|
config['protocols']['dmr']['control']['dedicated'] = False
|
|
config['protocols']['p25']['control']['enable'] = True
|
|
config['protocols']['p25']['control']['dedicated'] = False
|
|
|
|
elif template_type == 'conventional':
|
|
# Conventional repeater
|
|
config['system']['duplex'] = True
|
|
config['system']['identity'] = 'RPT'
|
|
config['protocols']['dmr']['enable'] = False
|
|
config['protocols']['p25']['enable'] = True
|
|
config['protocols']['nxdn']['enable'] = False
|
|
config['protocols']['dmr']['control']['enable'] = False
|
|
config['protocols']['dmr']['control']['dedicated'] = False
|
|
config['protocols']['p25']['control']['enable'] = True
|
|
config['protocols']['p25']['control']['dedicated'] = False
|
|
|
|
return config
|
|
|
|
|
|
def get_template(template_name: str) -> Dict[str, Any]:
|
|
"""
|
|
Get a configuration template by name
|
|
|
|
Args:
|
|
template_name: Name of the template
|
|
|
|
Returns:
|
|
Complete configuration dictionary with all parameters
|
|
"""
|
|
if template_name not in TEMPLATES:
|
|
raise ValueError(f"Unknown template: {template_name}")
|
|
|
|
# Load base config from config.example.yml
|
|
base_config = load_base_config()
|
|
|
|
# Apply template-specific customizations
|
|
customized_config = apply_template_customizations(base_config, template_name)
|
|
|
|
return customized_config
|
|
|
|
|
|
# Available templates
|
|
TEMPLATES = {
|
|
'conventional': 'Conventional repeater/hotspot',
|
|
'enhanced': 'Enhanced conventional repeater/hotspot with grants',
|
|
'control-channel-p25': 'P25 dedicated control channel for trunking',
|
|
'control-channel-dmr': 'DMR dedicated control channel for trunking',
|
|
'voice-channel': 'Voice channel for trunking system',
|
|
}
|