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.
442 lines
12 KiB
442 lines
12 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
|
|
|
|
Network and System ID validation for DVMHost protocols.
|
|
|
|
This module provides validation functions for DMR, P25, and NXDN network/system
|
|
identification parameters based on limits defined in DVMHost C++ code.
|
|
"""
|
|
|
|
from typing import Tuple, Dict, Any
|
|
from enum import Enum
|
|
|
|
|
|
class DMRSiteModel(Enum):
|
|
"""DMR site model types (from dmr::defines::SiteModel)"""
|
|
TINY = 0 # netId: 0-0x1FF (511), siteId: 0-0x07 (7)
|
|
SMALL = 1 # netId: 0-0x7F (127), siteId: 0-0x1F (31)
|
|
LARGE = 2 # netId: 0-0x1F (31), siteId: 0-0x7F (127)
|
|
HUGE = 3 # netId: 0-0x03 (3), siteId: 0-0x3FF (1023)
|
|
|
|
|
|
# DMR Limits (from dvmhost/src/common/dmr/DMRUtils.h)
|
|
DMR_COLOR_CODE_MIN = 0
|
|
DMR_COLOR_CODE_MAX = 15
|
|
|
|
DMR_SITE_MODEL_LIMITS = {
|
|
DMRSiteModel.TINY: {
|
|
'netId': (1, 0x1FF), # 1-511
|
|
'siteId': (1, 0x07), # 1-7
|
|
},
|
|
DMRSiteModel.SMALL: {
|
|
'netId': (1, 0x7F), # 1-127
|
|
'siteId': (1, 0x1F), # 1-31
|
|
},
|
|
DMRSiteModel.LARGE: {
|
|
'netId': (1, 0x1F), # 1-31
|
|
'siteId': (1, 0x7F), # 1-127
|
|
},
|
|
DMRSiteModel.HUGE: {
|
|
'netId': (1, 0x03), # 1-3
|
|
'siteId': (1, 0x3FF), # 1-1023
|
|
},
|
|
}
|
|
|
|
# P25 Limits (from dvmhost/src/common/p25/P25Utils.h)
|
|
P25_NAC_MIN = 0x000
|
|
P25_NAC_MAX = 0xF7F # 3967
|
|
|
|
P25_NET_ID_MIN = 1
|
|
P25_NET_ID_MAX = 0xFFFFE # 1048574
|
|
|
|
P25_SYS_ID_MIN = 1
|
|
P25_SYS_ID_MAX = 0xFFE # 4094
|
|
|
|
P25_RFSS_ID_MIN = 1
|
|
P25_RFSS_ID_MAX = 0xFE # 254
|
|
|
|
P25_SITE_ID_MIN = 1
|
|
P25_SITE_ID_MAX = 0xFE # 254
|
|
|
|
# NXDN Limits (from dvmhost/src/common/nxdn/SiteData.h and NXDNDefines.h)
|
|
NXDN_RAN_MIN = 0
|
|
NXDN_RAN_MAX = 63 # 6-bit field
|
|
|
|
NXDN_LOC_ID_MIN = 1
|
|
NXDN_LOC_ID_MAX = 0xFFFFFF # 16777215 (24-bit field)
|
|
|
|
NXDN_SITE_ID_MIN = 1
|
|
NXDN_SITE_ID_MAX = 0xFFFFFF # Same as LOC_ID
|
|
|
|
|
|
def validate_dmr_color_code(color_code: int) -> Tuple[bool, str]:
|
|
"""
|
|
Validate DMR color code.
|
|
|
|
Args:
|
|
color_code: Color code value (0-15)
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message)
|
|
"""
|
|
if not isinstance(color_code, int):
|
|
return False, "Color code must be an integer"
|
|
|
|
if color_code < DMR_COLOR_CODE_MIN or color_code > DMR_COLOR_CODE_MAX:
|
|
return False, f"Color code must be between {DMR_COLOR_CODE_MIN} and {DMR_COLOR_CODE_MAX}"
|
|
|
|
return True, ""
|
|
|
|
|
|
def validate_dmr_network_id(net_id: int, site_model: DMRSiteModel) -> Tuple[bool, str]:
|
|
"""
|
|
Validate DMR network ID based on site model.
|
|
|
|
Args:
|
|
net_id: Network ID value
|
|
site_model: DMR site model
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message)
|
|
"""
|
|
if not isinstance(net_id, int):
|
|
return False, "Network ID must be an integer"
|
|
|
|
limits = DMR_SITE_MODEL_LIMITS[site_model]
|
|
min_val, max_val = limits['netId']
|
|
|
|
if net_id < min_val or net_id > max_val:
|
|
return False, f"Network ID for {site_model.name} site model must be between {min_val} and {max_val} (0x{max_val:X})"
|
|
|
|
return True, ""
|
|
|
|
|
|
def validate_dmr_site_id(site_id: int, site_model: DMRSiteModel) -> Tuple[bool, str]:
|
|
"""
|
|
Validate DMR site ID based on site model.
|
|
|
|
Args:
|
|
site_id: Site ID value
|
|
site_model: DMR site model
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message)
|
|
"""
|
|
if not isinstance(site_id, int):
|
|
return False, "Site ID must be an integer"
|
|
|
|
limits = DMR_SITE_MODEL_LIMITS[site_model]
|
|
min_val, max_val = limits['siteId']
|
|
|
|
if site_id < min_val or site_id > max_val:
|
|
return False, f"Site ID for {site_model.name} site model must be between {min_val} and {max_val} (0x{max_val:X})"
|
|
|
|
return True, ""
|
|
|
|
|
|
def validate_p25_nac(nac: int) -> Tuple[bool, str]:
|
|
"""
|
|
Validate P25 Network Access Code.
|
|
|
|
Args:
|
|
nac: NAC value (0x000-0xF7F / 0-3967)
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message)
|
|
"""
|
|
if not isinstance(nac, int):
|
|
return False, "NAC must be an integer"
|
|
|
|
if nac < P25_NAC_MIN or nac > P25_NAC_MAX:
|
|
return False, f"NAC must be between {P25_NAC_MIN} (0x{P25_NAC_MIN:03X}) and {P25_NAC_MAX} (0x{P25_NAC_MAX:03X})"
|
|
|
|
return True, ""
|
|
|
|
|
|
def validate_p25_network_id(net_id: int) -> Tuple[bool, str]:
|
|
"""
|
|
Validate P25 Network ID (WACN).
|
|
|
|
Args:
|
|
net_id: Network ID value (1-0xFFFFE / 1-1048574)
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message)
|
|
"""
|
|
if not isinstance(net_id, int):
|
|
return False, "Network ID must be an integer"
|
|
|
|
if net_id < P25_NET_ID_MIN or net_id > P25_NET_ID_MAX:
|
|
return False, f"Network ID must be between {P25_NET_ID_MIN} and {P25_NET_ID_MAX} (0x{P25_NET_ID_MAX:X})"
|
|
|
|
return True, ""
|
|
|
|
|
|
def validate_p25_system_id(sys_id: int) -> Tuple[bool, str]:
|
|
"""
|
|
Validate P25 System ID.
|
|
|
|
Args:
|
|
sys_id: System ID value (1-0xFFE / 1-4094)
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message)
|
|
"""
|
|
if not isinstance(sys_id, int):
|
|
return False, "System ID must be an integer"
|
|
|
|
if sys_id < P25_SYS_ID_MIN or sys_id > P25_SYS_ID_MAX:
|
|
return False, f"System ID must be between {P25_SYS_ID_MIN} and {P25_SYS_ID_MAX} (0x{P25_SYS_ID_MAX:X})"
|
|
|
|
return True, ""
|
|
|
|
|
|
def validate_p25_rfss_id(rfss_id: int) -> Tuple[bool, str]:
|
|
"""
|
|
Validate P25 RFSS ID.
|
|
|
|
Args:
|
|
rfss_id: RFSS ID value (1-0xFE / 1-254)
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message)
|
|
"""
|
|
if not isinstance(rfss_id, int):
|
|
return False, "RFSS ID must be an integer"
|
|
|
|
if rfss_id < P25_RFSS_ID_MIN or rfss_id > P25_RFSS_ID_MAX:
|
|
return False, f"RFSS ID must be between {P25_RFSS_ID_MIN} and {P25_RFSS_ID_MAX} (0x{P25_RFSS_ID_MAX:X})"
|
|
|
|
return True, ""
|
|
|
|
|
|
def validate_p25_site_id(site_id: int) -> Tuple[bool, str]:
|
|
"""
|
|
Validate P25 Site ID.
|
|
|
|
Args:
|
|
site_id: Site ID value (1-0xFE / 1-254)
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message)
|
|
"""
|
|
if not isinstance(site_id, int):
|
|
return False, "Site ID must be an integer"
|
|
|
|
if site_id < P25_SITE_ID_MIN or site_id > P25_SITE_ID_MAX:
|
|
return False, f"Site ID must be between {P25_SITE_ID_MIN} and {P25_SITE_ID_MAX} (0x{P25_SITE_ID_MAX:X})"
|
|
|
|
return True, ""
|
|
|
|
|
|
def validate_nxdn_ran(ran: int) -> Tuple[bool, str]:
|
|
"""
|
|
Validate NXDN Random Access Number.
|
|
|
|
Args:
|
|
ran: RAN value (0-63)
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message)
|
|
"""
|
|
if not isinstance(ran, int):
|
|
return False, "RAN must be an integer"
|
|
|
|
if ran < NXDN_RAN_MIN or ran > NXDN_RAN_MAX:
|
|
return False, f"RAN must be between {NXDN_RAN_MIN} and {NXDN_RAN_MAX}"
|
|
|
|
return True, ""
|
|
|
|
|
|
def validate_nxdn_location_id(loc_id: int) -> Tuple[bool, str]:
|
|
"""
|
|
Validate NXDN Location ID (used as System ID).
|
|
|
|
Args:
|
|
loc_id: Location ID value (1-0xFFFFFF / 1-16777215)
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message)
|
|
"""
|
|
if not isinstance(loc_id, int):
|
|
return False, "Location ID must be an integer"
|
|
|
|
if loc_id < NXDN_LOC_ID_MIN or loc_id > NXDN_LOC_ID_MAX:
|
|
return False, f"Location ID must be between {NXDN_LOC_ID_MIN} and {NXDN_LOC_ID_MAX} (0x{NXDN_LOC_ID_MAX:X})"
|
|
|
|
return True, ""
|
|
|
|
|
|
def validate_nxdn_site_id(site_id: int) -> Tuple[bool, str]:
|
|
"""
|
|
Validate NXDN Site ID.
|
|
|
|
Args:
|
|
site_id: Site ID value (1-0xFFFFFF / 1-16777215)
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error_message)
|
|
"""
|
|
return validate_nxdn_location_id(site_id) # Same limits
|
|
|
|
|
|
def get_dmr_site_model_from_string(model_str: str) -> DMRSiteModel:
|
|
"""
|
|
Convert site model string to enum.
|
|
|
|
Args:
|
|
model_str: Site model string ('tiny', 'small', 'large', 'huge')
|
|
|
|
Returns:
|
|
DMRSiteModel enum value
|
|
"""
|
|
model_map = {
|
|
'tiny': DMRSiteModel.TINY,
|
|
'small': DMRSiteModel.SMALL,
|
|
'large': DMRSiteModel.LARGE,
|
|
'huge': DMRSiteModel.HUGE,
|
|
}
|
|
return model_map.get(model_str.lower(), DMRSiteModel.SMALL)
|
|
|
|
|
|
def get_dmr_site_model_name(model: DMRSiteModel) -> str:
|
|
"""Get friendly name for DMR site model."""
|
|
return model.name.capitalize()
|
|
|
|
|
|
def get_network_id_info(protocol: str, site_model: str = 'small') -> Dict[str, Any]:
|
|
"""
|
|
Get network ID parameter information for a protocol.
|
|
|
|
Args:
|
|
protocol: Protocol name ('dmr', 'p25', 'nxdn')
|
|
site_model: DMR site model for DMR protocol
|
|
|
|
Returns:
|
|
Dictionary with parameter info
|
|
"""
|
|
protocol = protocol.lower()
|
|
|
|
if protocol == 'dmr':
|
|
model = get_dmr_site_model_from_string(site_model)
|
|
limits = DMR_SITE_MODEL_LIMITS[model]
|
|
return {
|
|
'colorCode': {
|
|
'min': DMR_COLOR_CODE_MIN,
|
|
'max': DMR_COLOR_CODE_MAX,
|
|
'default': 1,
|
|
'description': 'DMR Color Code',
|
|
},
|
|
'dmrNetId': {
|
|
'min': limits['netId'][0],
|
|
'max': limits['netId'][1],
|
|
'default': 1,
|
|
'description': f'DMR Network ID ({model.name} site model)',
|
|
},
|
|
'siteId': {
|
|
'min': limits['siteId'][0],
|
|
'max': limits['siteId'][1],
|
|
'default': 1,
|
|
'description': f'DMR Site ID ({model.name} site model)',
|
|
},
|
|
}
|
|
|
|
elif protocol == 'p25':
|
|
return {
|
|
'nac': {
|
|
'min': P25_NAC_MIN,
|
|
'max': P25_NAC_MAX,
|
|
'default': 0x293, # 659 decimal (common default)
|
|
'description': 'P25 Network Access Code (NAC)',
|
|
},
|
|
'netId': {
|
|
'min': P25_NET_ID_MIN,
|
|
'max': P25_NET_ID_MAX,
|
|
'default': 0xBB800, # 768000 decimal
|
|
'description': 'P25 Network ID (WACN)',
|
|
},
|
|
'sysId': {
|
|
'min': P25_SYS_ID_MIN,
|
|
'max': P25_SYS_ID_MAX,
|
|
'default': 0x001,
|
|
'description': 'P25 System ID',
|
|
},
|
|
'rfssId': {
|
|
'min': P25_RFSS_ID_MIN,
|
|
'max': P25_RFSS_ID_MAX,
|
|
'default': 1,
|
|
'description': 'P25 RFSS (RF Sub-System) ID',
|
|
},
|
|
'siteId': {
|
|
'min': P25_SITE_ID_MIN,
|
|
'max': P25_SITE_ID_MAX,
|
|
'default': 1,
|
|
'description': 'P25 Site ID',
|
|
},
|
|
}
|
|
|
|
elif protocol == 'nxdn':
|
|
return {
|
|
'ran': {
|
|
'min': NXDN_RAN_MIN,
|
|
'max': NXDN_RAN_MAX,
|
|
'default': 1,
|
|
'description': 'NXDN Random Access Number (RAN)',
|
|
},
|
|
'sysId': {
|
|
'min': NXDN_LOC_ID_MIN,
|
|
'max': NXDN_LOC_ID_MAX,
|
|
'default': 0x001,
|
|
'description': 'NXDN System ID (Location ID)',
|
|
},
|
|
'siteId': {
|
|
'min': NXDN_SITE_ID_MIN,
|
|
'max': NXDN_SITE_ID_MAX,
|
|
'default': 1,
|
|
'description': 'NXDN Site ID',
|
|
},
|
|
}
|
|
|
|
return {}
|
|
|
|
|
|
def format_hex_or_decimal(value: int, max_value: int) -> str:
|
|
"""
|
|
Format value as hex if it makes sense, otherwise decimal.
|
|
|
|
Args:
|
|
value: Value to format
|
|
max_value: Maximum value for the field
|
|
|
|
Returns:
|
|
Formatted string
|
|
"""
|
|
if max_value > 999:
|
|
return f"{value} (0x{value:X})"
|
|
return str(value)
|