- Port playback.py to clean architecture: PlaybackUseCases, parrot_main.py, adn-parrot.py - Fix critical PEER auth bug: RPTACK/MSTNAK data offsets were off by 1 (5→6) preventing PEER login - Extract config normalization to shared config_normalizer.py (used by both main and parrot)pull/4/head
parent
30b9ef697f
commit
6f7ed179d5
@ -0,0 +1,75 @@
|
||||
# ADN DMR Parrot (playback) - example configuration
|
||||
# Copy to adn-parrot.yaml and set real secrets there (never commit adn-parrot.yaml).
|
||||
#
|
||||
# The parrot runs a MASTER. The ECHO PEER (defined in adn-server.yaml) connects to it.
|
||||
# When a user transmits to the ECHO peer, the parrot records the audio and plays it back.
|
||||
|
||||
GLOBAL:
|
||||
PATH: ./
|
||||
PING_TIME: 10
|
||||
MAX_MISSED: 3
|
||||
USE_ACL: true
|
||||
REG_ACL: DENY:1
|
||||
SUB_ACL: DENY:1
|
||||
TGID_TS1_ACL: PERMIT:ALL
|
||||
TGID_TS2_ACL: PERMIT:ALL
|
||||
ALLOW_NULL_PASSPHRASE: false
|
||||
ANNOUNCEMENT_LANGUAGES: es_ES
|
||||
SERVER_ID: 9990
|
||||
DATA_GATEWAY: false
|
||||
VALIDATE_SERVER_IDS: false
|
||||
|
||||
REPORTS:
|
||||
REPORT: false
|
||||
REPORT_INTERVAL: 60
|
||||
REPORT_PORT: 4821
|
||||
REPORT_CLIENTS: "127.0.0.1"
|
||||
|
||||
LOGGER:
|
||||
LOG_FILE: /var/log/adn-server/parrot.log
|
||||
LOG_HANDLERS: file-timed
|
||||
LOG_LEVEL: DEBUG
|
||||
LOG_NAME: ADN
|
||||
|
||||
ALIASES:
|
||||
TRY_DOWNLOAD: false
|
||||
PATH: ./json/
|
||||
PEER_FILE: peer_ids.json
|
||||
SUBSCRIBER_FILE: subscriber_ids.json
|
||||
TGID_FILE: talkgroup_ids.json
|
||||
PEER_URL: https://adn.systems/files/peer_ids.json
|
||||
SUBSCRIBER_URL: https://adn.systems/files/subscriber_ids.json
|
||||
TGID_URL: https://adn.systems/files/talkgroup_ids.json
|
||||
LOCAL_SUBSCRIBER_FILE: local_subscriber_ids.json
|
||||
STALE_DAYS: 1
|
||||
SUB_MAP_FILE: ""
|
||||
SERVER_ID_URL: https://adn.systems/files/server_ids.tsv
|
||||
SERVER_ID_FILE: server_ids.tsv
|
||||
|
||||
SYSTEMS:
|
||||
PARROT:
|
||||
MODE: MASTER
|
||||
ENABLED: true
|
||||
REPEAT: true
|
||||
MAX_PEERS: 1
|
||||
EXPORT_AMBE: false
|
||||
IP: 127.0.0.1
|
||||
PORT: 54915
|
||||
PASSPHRASE: "<set-in-adn-parrot.yaml>"
|
||||
GROUP_HANGTIME: 5
|
||||
USE_ACL: true
|
||||
REG_ACL: DENY:1
|
||||
SUB_ACL: DENY:1
|
||||
TGID_TS1_ACL: PERMIT:ALL
|
||||
TGID_TS2_ACL: PERMIT:ALL
|
||||
DEFAULT_UA_TIMER: 10
|
||||
SINGLE_MODE: true
|
||||
VOICE_IDENT: false
|
||||
TS1_STATIC: ""
|
||||
TS2_STATIC: ""
|
||||
DEFAULT_REFLECTOR: 0
|
||||
GENERATOR: 1
|
||||
ANNOUNCEMENT_LANGUAGE: es_ES
|
||||
ALLOW_UNREG_ID: true
|
||||
PROXY_CONTROL: false
|
||||
OVERRIDE_IDENT_TG: ""
|
||||
@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
# ADN DMR Parrot (playback) - launcher script
|
||||
# Copyright (C) 2016-2019 Cortney T. Buffington, N0MJS & Mike Zingman, N4IRR
|
||||
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
|
||||
# GPLv3. Derived from playback.py / ADN DMR Server / HBlink.
|
||||
|
||||
"""
|
||||
Run the ADN DMR Parrot (playback) from the project root.
|
||||
|
||||
python adn-parrot.py
|
||||
python adn-parrot.py -c adn-parrot.yaml
|
||||
python adn-parrot.py --logging DEBUG
|
||||
|
||||
Config default: adn-parrot.yaml in this directory.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_ROOT = Path(__file__).resolve().parent
|
||||
if str(_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_ROOT))
|
||||
if str(_ROOT / "src") not in sys.path:
|
||||
sys.path.insert(0, str(_ROOT / "src"))
|
||||
|
||||
from adn_server.parrot_main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,81 @@
|
||||
# ADN DMR Peer Server - playback (parrot) use case
|
||||
# Copyright (C) 2016-2019 Cortney T. Buffington, N0MJS & Mike Zingman, N4IRR
|
||||
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
|
||||
# Derived from playback.py. GPLv3.
|
||||
|
||||
"""Exact port of playback.py dmrd_received: record group voice, then play back with new stream ID."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from random import randint
|
||||
from time import sleep, time
|
||||
from typing import Any
|
||||
|
||||
from ..domain import int_id, bytes_4
|
||||
from ..infrastructure.hbp_constants import HBPF_DATA_SYNC, HBPF_SLT_VTERM
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlaybackUseCases:
|
||||
"""Exact port of legacy playback class. One instance per system."""
|
||||
|
||||
def __init__(self, system_name: str, get_protocol: Any = None) -> None:
|
||||
self._system = system_name
|
||||
self._get_protocol = get_protocol
|
||||
self.STATUS: dict[str, Any] = {}
|
||||
self.CALL_DATA: list[bytes] = []
|
||||
|
||||
def dmrd_received(
|
||||
self,
|
||||
system_name: str,
|
||||
peer_id: bytes,
|
||||
rf_src: bytes,
|
||||
dst_id: bytes,
|
||||
seq: int,
|
||||
slot: int,
|
||||
call_type: str,
|
||||
frame_type: int,
|
||||
dtype_vseq: int,
|
||||
stream_id: bytes,
|
||||
data: bytes,
|
||||
) -> None:
|
||||
"""Exact port of playback.dmrd_received (playback.py lines 114-161)."""
|
||||
pkt_time = time()
|
||||
|
||||
if call_type == "group":
|
||||
proto = self._get_protocol() if self._get_protocol else None
|
||||
slot_status = getattr(proto, "STATUS", {}).get(slot, {}) if proto else {}
|
||||
|
||||
if stream_id != slot_status.get("RX_STREAM_ID", b"\x00"):
|
||||
self.STATUS["RX_START"] = pkt_time
|
||||
logger.info(
|
||||
"(%s) *START RECORDING* STREAM ID: %s SUB: %s REPEATER: %s TGID %s, TS %s",
|
||||
self._system, int_id(stream_id), int_id(rf_src), int_id(peer_id), int_id(dst_id), slot,
|
||||
)
|
||||
self.CALL_DATA.append(data)
|
||||
return
|
||||
|
||||
if (frame_type == HBPF_DATA_SYNC) and (dtype_vseq == HBPF_SLT_VTERM) and (slot_status.get("RX_TYPE") != HBPF_SLT_VTERM) and self.CALL_DATA:
|
||||
call_duration = pkt_time - self.STATUS.get("RX_START", pkt_time)
|
||||
self.CALL_DATA.append(data)
|
||||
logger.info("(%s) *END RECORDING* STREAM ID: %s", self._system, int_id(stream_id))
|
||||
sleep(2)
|
||||
_new_stream_id = bytes_4(randint(0x00, 0xFFFFFFFF))
|
||||
logger.info(
|
||||
"(%s) *START PLAYBACK* STREAM ID: %s SUB: %s REPEATER: %s TGID %s, TS %s, Duration: %s",
|
||||
self._system, int_id(_new_stream_id), int_id(rf_src), int_id(peer_id), int_id(dst_id), slot, call_duration,
|
||||
)
|
||||
|
||||
if proto:
|
||||
for i in self.CALL_DATA:
|
||||
i = i[:16] + _new_stream_id + i[20:]
|
||||
proto.send_system(i)
|
||||
sleep(0.06)
|
||||
self.CALL_DATA = []
|
||||
logger.info("(%s) *END PLAYBACK* STREAM ID: %s", self._system, int_id(_new_stream_id))
|
||||
|
||||
else:
|
||||
if self.CALL_DATA:
|
||||
self.CALL_DATA.append(data)
|
||||
@ -0,0 +1,144 @@
|
||||
# ADN DMR Peer Server - config normalization helpers
|
||||
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
|
||||
# Derived from ADN DMR Server / HBlink. GPLv3.
|
||||
|
||||
"""Shared config normalization: PEER, OBP, MASTER runtime state. Used by main.py and parrot_main.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import socket
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
|
||||
def expand_generator(config: dict, logger: logging.Logger) -> None:
|
||||
"""Replace MASTER systems with GENERATOR > 1 by SYSTEM-0, SYSTEM-1, ... (legacy generator)."""
|
||||
systems = config.get("SYSTEMS", {})
|
||||
to_remove: list[str] = []
|
||||
new_systems: dict = {}
|
||||
for system_name, sys_cfg in list(systems.items()):
|
||||
if not sys_cfg.get("ENABLED", True):
|
||||
continue
|
||||
if sys_cfg.get("MODE") != "MASTER":
|
||||
continue
|
||||
generator = int(sys_cfg.get("GENERATOR", 1))
|
||||
if generator <= 1:
|
||||
continue
|
||||
for count in range(generator):
|
||||
new_name = f"{system_name}-{count}"
|
||||
new_cfg = copy.deepcopy(sys_cfg)
|
||||
base_port = int(new_cfg.get("PORT", 56400))
|
||||
new_cfg["PORT"] = base_port + count
|
||||
new_cfg["_default_options"] = "SINGLE={};DEFAULT_UA_TIMER={};VOICE={};LANG={}".format(
|
||||
int(new_cfg.get("SINGLE_MODE", False)),
|
||||
new_cfg.get("DEFAULT_UA_TIMER", 60),
|
||||
int(new_cfg.get("VOICE_IDENT", False)),
|
||||
new_cfg.get("ANNOUNCEMENT_LANGUAGE", "en_GB"),
|
||||
)
|
||||
new_systems[new_name] = new_cfg
|
||||
logger.debug("(GLOBAL) Generator - generated system %s", new_name)
|
||||
to_remove.append(system_name)
|
||||
for name in to_remove:
|
||||
systems.pop(name, None)
|
||||
for name, cfg in new_systems.items():
|
||||
systems[name] = cfg
|
||||
|
||||
|
||||
def ensure_system_runtime_config(config: dict) -> None:
|
||||
"""Ensure MASTER has PEERS and PEER has STATS (legacy config.py runtime state)."""
|
||||
for name, sys_cfg in config.get("SYSTEMS", {}).items():
|
||||
if sys_cfg.get("MODE") == "MASTER":
|
||||
sys_cfg.setdefault("PEERS", {})
|
||||
elif sys_cfg.get("MODE") == "PEER":
|
||||
sys_cfg.setdefault("STATS", {
|
||||
"CONNECTION": "NO",
|
||||
"CONNECTED": None,
|
||||
"PINGS_SENT": 0,
|
||||
"PINGS_ACKD": 0,
|
||||
"NUM_OUTSTANDING": 0,
|
||||
"PING_OUTSTANDING": False,
|
||||
"LAST_PING_TX_TIME": 0,
|
||||
"LAST_PING_ACK_TIME": 0,
|
||||
})
|
||||
|
||||
|
||||
def normalize_peer_config(config: dict) -> None:
|
||||
"""Convert PEER systems from YAML to legacy format: MASTER_SOCKADDR, RADIO_ID/CALLSIGN/OPTIONS as bytes (config.py)."""
|
||||
for name, sys_cfg in config.get("SYSTEMS", {}).items():
|
||||
if sys_cfg.get("MODE") != "PEER":
|
||||
continue
|
||||
master_ip_str = str(sys_cfg.get("MASTER_IP", "127.0.0.1"))
|
||||
master_port = int(sys_cfg.get("MASTER_PORT", 56400))
|
||||
try:
|
||||
resolved_ip = socket.gethostbyname(master_ip_str)
|
||||
except OSError:
|
||||
resolved_ip = master_ip_str
|
||||
sys_cfg["_MASTER_IP"] = master_ip_str
|
||||
sys_cfg["MASTER_IP"] = resolved_ip
|
||||
sys_cfg["MASTER_PORT"] = master_port
|
||||
sys_cfg["MASTER_SOCKADDR"] = (resolved_ip, master_port)
|
||||
radio_id = int(sys_cfg.get("RADIO_ID", 0))
|
||||
sys_cfg["RADIO_ID"] = (radio_id & 0xFFFFFFFF).to_bytes(4, "big")
|
||||
for field, length in [
|
||||
("CALLSIGN", 8), ("RX_FREQ", 9), ("TX_FREQ", 9), ("TX_POWER", 2), ("COLORCODE", 2),
|
||||
("LATITUDE", 8), ("LONGITUDE", 9), ("HEIGHT", 3), ("LOCATION", 20), ("DESCRIPTION", 19),
|
||||
("SLOTS", 1), ("URL", 124), ("SOFTWARE_ID", 40), ("PACKAGE_ID", 40),
|
||||
]:
|
||||
val = sys_cfg.get(field, "")
|
||||
if isinstance(val, (int, float)):
|
||||
val = str(val)
|
||||
b = val.encode("utf-8") if isinstance(val, str) else val
|
||||
if field == "CALLSIGN":
|
||||
sys_cfg[field] = b.ljust(length)[:length]
|
||||
elif field in ("RX_FREQ", "TX_FREQ", "LATITUDE", "LONGITUDE", "LOCATION", "DESCRIPTION", "URL", "SOFTWARE_ID", "PACKAGE_ID"):
|
||||
sys_cfg[field] = b.ljust(length)[:length]
|
||||
else:
|
||||
sys_cfg[field] = b.rjust(length, b"0")[:length] if length <= 3 else b.ljust(length)[:length]
|
||||
opt = sys_cfg.get("OPTIONS", "")
|
||||
sys_cfg["OPTIONS"] = opt.encode("utf-8") if isinstance(opt, str) else (opt or b"")
|
||||
passphrase = sys_cfg.get("PASSPHRASE", "")
|
||||
sys_cfg["PASSPHRASE"] = passphrase.encode("utf-8") if isinstance(passphrase, str) else (passphrase or b"")
|
||||
sys_cfg.setdefault("LOOSE", False)
|
||||
stats = sys_cfg.get("STATS", {})
|
||||
stats["DNS_TIME"] = time.time()
|
||||
|
||||
|
||||
def normalize_obp_config(config: dict) -> None:
|
||||
"""Normalize OPENBRIDGE systems and GLOBAL SERVER_ID (legacy config.py)."""
|
||||
g = config.setdefault("GLOBAL", {})
|
||||
sid = g.get("SERVER_ID", 0)
|
||||
g["SERVER_ID"] = (int(sid) & 0xFFFFFFFF).to_bytes(4, "big") if not isinstance(sid, bytes) else sid
|
||||
for name, sys_cfg in config.get("SYSTEMS", {}).items():
|
||||
if sys_cfg.get("MODE") != "OPENBRIDGE":
|
||||
continue
|
||||
net_id = int(sys_cfg.get("NETWORK_ID", 0))
|
||||
sys_cfg["NETWORK_ID"] = (net_id & 0xFFFFFFFF).to_bytes(4, "big")
|
||||
target_ip = str(sys_cfg.get("TARGET_IP", ""))
|
||||
target_port = int(sys_cfg.get("TARGET_PORT", 62044))
|
||||
if target_ip:
|
||||
try:
|
||||
resolved = socket.gethostbyname(target_ip)
|
||||
sys_cfg["TARGET_IP"] = resolved
|
||||
sys_cfg["TARGET_SOCK"] = (resolved, target_port)
|
||||
except OSError:
|
||||
sys_cfg["TARGET_IP"] = None
|
||||
sys_cfg["TARGET_SOCK"] = (None, target_port)
|
||||
else:
|
||||
sys_cfg["TARGET_IP"] = None
|
||||
sys_cfg["TARGET_SOCK"] = (None, target_port)
|
||||
ver = int(sys_cfg.get("PROTO_VER", sys_cfg.get("VER", 5)))
|
||||
if ver in (0, 2, 3) or ver > 5:
|
||||
ver = 5
|
||||
sys_cfg["VER"] = ver
|
||||
p = sys_cfg.get("PASSPHRASE") or b""
|
||||
if isinstance(p, str):
|
||||
p = p.strip().encode("utf-8")
|
||||
else:
|
||||
p = p or b""
|
||||
sys_cfg["PASSPHRASE"] = (p + b"\x00" * 20)[:20]
|
||||
sys_cfg.setdefault("RELAX_CHECKS", True)
|
||||
sys_cfg.setdefault("ENHANCED_OBP", True)
|
||||
if "TG1_ACL" not in sys_cfg and "TGID_ACL" in sys_cfg:
|
||||
sys_cfg["TG1_ACL"] = sys_cfg["TGID_ACL"]
|
||||
@ -0,0 +1,145 @@
|
||||
# ADN DMR Peer Server - parrot (playback) entrypoint
|
||||
# Copyright (C) 2016-2019 Cortney T. Buffington, N0MJS & Mike Zingman, N4IRR
|
||||
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
|
||||
# Derived from playback.py. GPLv3.
|
||||
|
||||
"""
|
||||
ADN DMR Parrot (playback) entrypoint.
|
||||
|
||||
Exact port of legacy playback.py: record group voice, play back with new stream ID.
|
||||
Uses the same YAML config as adn-server (only MASTER/PEER systems, no OPENBRIDGE).
|
||||
|
||||
python adn-parrot.py
|
||||
python adn-parrot.py -c adn-parrot.yaml
|
||||
python adn-parrot.py --logging DEBUG
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
if str(_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_ROOT))
|
||||
|
||||
from twisted.internet import reactor, task
|
||||
|
||||
from .infrastructure import YamlConfigLoader, setup_logging
|
||||
from .infrastructure.config_normalizer import (
|
||||
ensure_system_runtime_config,
|
||||
normalize_peer_config,
|
||||
normalize_obp_config,
|
||||
)
|
||||
from .infrastructure.bridge_router_impl import InMemoryBridgeRouter
|
||||
from .infrastructure.twisted_adapters.report_server import ReportServerFactory
|
||||
from .infrastructure.twisted_adapters.udp_hbp import HBPProtocolFactory
|
||||
from .application.playback_use_cases import PlaybackUseCases
|
||||
|
||||
|
||||
def _looping_errback(logger_obj: logging.Logger, failure):
|
||||
logger_obj.error("(GLOBAL) Unhandled error in timed loop: %s", failure.getTraceback())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="ADN DMR Parrot (playback)")
|
||||
parser.add_argument("-c", "--config", dest="CONFIG_FILE", default=None, help="Path to YAML config")
|
||||
parser.add_argument("--logging", dest="LOG_LEVEL", default=None, help="Override log level")
|
||||
args = parser.parse_args()
|
||||
|
||||
project_root = os.path.dirname(os.path.abspath(__file__))
|
||||
if project_root.endswith("/adn_server"):
|
||||
project_root = str(Path(project_root).parent.parent)
|
||||
config_path = args.CONFIG_FILE or os.path.join(project_root, "adn-parrot.yaml")
|
||||
|
||||
loader = YamlConfigLoader(project_root)
|
||||
config = loader.load(config_path)
|
||||
|
||||
if args.LOG_LEVEL:
|
||||
config.setdefault("LOGGER", {})["LOG_LEVEL"] = args.LOG_LEVEL
|
||||
logger = setup_logging(config.get("LOGGER", {}))
|
||||
logger.info("\n\nCopyright (c) 2026 Rodrigo Pérez, CE5RPY ce5rpy@qmd.cl")
|
||||
logger.info("\n\nCopyright (c) 2026 Joaquin Madrid Belando, EA5GVK ea5gvk@gmail.com")
|
||||
logger.info("\nCopyright (c) 2024-2026 Esteban Mackay, HP3ICC setcom40@gmail.com")
|
||||
logger.info("\nCopyright (c) 2020-2023 Simon G7RZU simon@gb7fr.org.uk")
|
||||
logger.info("\nCopyright (c) 2013, 2014, 2015, 2016, 2018, 2019\n\tThe Regents of the K0USY Group. All rights reserved.")
|
||||
logger.debug("\n\n(GLOBAL) Logging system started, anything from here on gets logged")
|
||||
|
||||
ensure_system_runtime_config(config)
|
||||
normalize_peer_config(config)
|
||||
normalize_obp_config(config)
|
||||
|
||||
g = config.setdefault("GLOBAL", {})
|
||||
sid = g.get("SERVER_ID", 0)
|
||||
g["SERVER_ID"] = (int(sid) & 0xFFFFFFFF).to_bytes(4, "big") if not isinstance(sid, bytes) else sid
|
||||
|
||||
protocols: dict[str, Any] = {}
|
||||
router = InMemoryBridgeRouter()
|
||||
report_factory = ReportServerFactory(config)
|
||||
systems_cfg = config.get("SYSTEMS", {})
|
||||
report_factory.set_systems(systems_cfg)
|
||||
|
||||
if config.get("REPORTS", {}).get("REPORT", False):
|
||||
port = config["REPORTS"].get("REPORT_PORT", 4321)
|
||||
reactor.listenTCP(port, report_factory)
|
||||
logger.info("(REPORT) Report server listening on TCP %s", port)
|
||||
|
||||
def reporting_loop():
|
||||
report_factory.set_systems(config.get("SYSTEMS", {}))
|
||||
report_factory.send_config()
|
||||
|
||||
report_interval = config.get("REPORTS", {}).get("REPORT_INTERVAL", 60)
|
||||
task.LoopingCall(reporting_loop).start(report_interval).addErrback(_looping_errback, logger)
|
||||
|
||||
playback_instances: dict[str, PlaybackUseCases] = {}
|
||||
|
||||
def sig_handler(sig, frame):
|
||||
logger.info("SHUTDOWN: PARROT IS TERMINATING WITH SIGNAL %s", sig)
|
||||
if reactor.running:
|
||||
reactor.stop()
|
||||
|
||||
signal.signal(signal.SIGTERM, sig_handler)
|
||||
signal.signal(signal.SIGINT, sig_handler)
|
||||
|
||||
logger.info("ADN Parrot -- SYSTEM STARTING...")
|
||||
for system_name, sys_cfg in systems_cfg.items():
|
||||
if not sys_cfg.get("ENABLED", True):
|
||||
continue
|
||||
if sys_cfg.get("MODE") == "OPENBRIDGE":
|
||||
logger.critical(
|
||||
"%s FATAL: Instance is mode 'OPENBRIDGE', which would be tragic for playback. "
|
||||
"playback only works with MMDVM-based systems",
|
||||
system_name,
|
||||
)
|
||||
sys.exit(
|
||||
"playback cannot function with OPENBRIDGE systems. System {} is configured as OPENBRIDGE".format(
|
||||
system_name
|
||||
)
|
||||
)
|
||||
|
||||
pb = PlaybackUseCases(system_name, get_protocol=lambda sn=system_name: protocols.get(sn))
|
||||
playback_instances[system_name] = pb
|
||||
|
||||
protocol = HBPProtocolFactory(
|
||||
system_name,
|
||||
config,
|
||||
report_factory,
|
||||
router=router,
|
||||
dmrd_received=pb.dmrd_received,
|
||||
)
|
||||
protocols[system_name] = protocol
|
||||
ip = sys_cfg.get("IP", "")
|
||||
udp_port = sys_cfg.get("PORT", 56400)
|
||||
reactor.listenUDP(udp_port, protocol, interface=ip or "0.0.0.0")
|
||||
logger.debug("%s instance created: %s, %s", sys_cfg.get("MODE", "?"), system_name, protocol)
|
||||
|
||||
reactor.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in new issue