Files
qt5-legatus-legacy/Lts/Tools/strefu_sim.py
T
2026-08-13 18:33:10 +02:00

431 lines
13 KiB
Python
Executable File

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
# @RDE_HEADER@
# $Id$
# $HeadURL$
# $Author$
# Created by Christian Fröbel, NLE, 2019-01-29
#
import socket
import sys
import textwrap
import threading
import weakref
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from collections import namedtuple
from contextlib import ExitStack, contextmanager
from queue import Empty, Queue
from socketserver import StreamRequestHandler, TCPServer, ThreadingMixIn
from threading import Lock, Thread
@contextmanager
def autoShutdown(target):
try:
yield target
finally:
target.initShutdown()
@contextmanager
def autoJoin(thread):
try:
yield thread
finally:
thread.join()
@contextmanager
def startThreadContext(target, thread):
with ExitStack() as stack:
thread.start()
stack.enter_context(autoJoin(thread))
stack.enter_context(autoShutdown(target))
yield
#
# Hier registrieren sich die Sender für die Verbindungen zu den AVS_Interface-Instanzen.
#
class _Registry(object):
def __init__(self):
self._senders = []
self._mutex = Lock()
@property
def senders(self):
with self._mutex:
return tuple(self._senders)
def register(self, sender):
with self._mutex:
assert sender not in self._senders, 'sender is registered already'
self._senders.append(sender)
print('client count: {}'.format(len(self._senders)))
def unregister(self, sender):
with self._mutex:
try:
self._senders.remove(sender)
except ValueError:
assert False, 'sender is not registered'
print('client count: {}'.format(len(self._senders)))
class _Server(ThreadingMixIn, TCPServer):
allow_reuse_address = True
timeout = 1
class _Acceptor(object):
def __init__(self, port, registry, handlerFactory):
self._port = port
self._registry = registry
self._handlerFactory = handlerFactory
self._doRun = True
def run(self):
server = _Server(('0.0.0.0', self._port), self._handlerFactory)
while self._doRun:
server.handle_request() # mit Timeout
self._joinFinishedHandlers()
self._joinAllHandlers()
def initShutdown(self):
self._doRun = False
def _joinFinishedHandlers(self):
for handler in self._registry.senders:
if not handler.isAlive():
handler.join(0) # ohne Timeout
def _joinAllHandlers(self):
for handler in self._registry.senders:
handler.initShutdown()
for handler in self._registry.senders:
handler.join()
class _RequestHandlerFactory(object):
def __init__(self, cmdQueue, registry, channelManager):
self._cmdQueue = cmdQueue
self._registry = registry
self._channelManager = channelManager
def __call__(self, *args):
return _RequestHandler(self._cmdQueue, self._registry, self._channelManager, *args)
#
# Läuft in einem separaten Thread. Den Thread setzen wir aber nicht selbst auf.
# Daher müssen wir den erst ermitteln.
#
class _RequestHandler(StreamRequestHandler):
def __init__(self, cmdQueue, registry, channelManager, *args):
# Reihenfolge wichtig; super() muss am Ende stehen
self._cmdQueue = cmdQueue
self._registry = registry
self._channelManager = channelManager
self._thread = None
super().__init__(*args)
def handle(self):
print('new connection from: {}'.format(self.client_address))
self._thread = threading.current_thread()
self._registry.register(self)
self._channelManager.activateAllChannels(self)
while True:
line = self.rfile.readline().decode('utf-8')
if len(line) == 0: # EOF
break
self._process(line)
def send(self, line):
terminatedLine = line + '\r\n'
self.wfile.write(terminatedLine.encode('utf-8'))
def isAlive(self):
return self._thread.is_alive()
def join(self, timeout=None):
self._thread.join(timeout)
self._registry.unregister(self)
# Schließen des Sockets (der Leseseite), damit der Service aus dem blockierenden read() rauskommt.
def initShutdown(self):
_shutdownAndCloseSocket(self.request)
def _process(self, line):
cmd = self._parseLine(line)
self.send('200 ok')
self._cmdQueue.put(cmd)
# Zeile parsen und AvsCommand liefern
def _parseLine(self, line):
parts = line.split()
token = parts.pop(0).lower()
args = parts
if token == 'speechreq':
assert len(args) == 1, 'invalid command format'
elif token == 'speechreqwd':
assert len(args) == 1, 'invalid command format'
elif token == 'channelusage':
assert len(args) == 3, 'invalid command format'
elif token == 'configchanged':
assert len(args) == 0, 'invalid command format'
elif token == 'keepalive':
assert len(args) == 0, 'invalid command format'
else:
assert False, 'unknown command: {}'.format(line)
return AvsCommand(token, args)
AvsCommand = namedtuple('AvsCommand', 'token, args')
#
# Liest die Control-Commands aus einem file-like Stream (z.B. stdin).
# Sollte im Main-Thread laufen, weil hier auch die Behandlung von KeyboardInterrupt (Ctrl-C) gemacht wird.
#
class _ControlRequestHandler(object):
def __init__(self, inputStream, cmdQueue):
self._inputStream = inputStream
self._cmdQueue = cmdQueue
def handle(self):
try:
self._handle()
except KeyboardInterrupt:
pass
def _handle(self):
while True:
line = self._inputStream.readline()
if len(line) == 0: # EOF
break
self._process(line)
def _process(self, line):
cmd = self._parseLine(line.rstrip())
if cmd is not None:
self._cmdQueue.put(cmd)
def _parseLine(self, line):
parts = line.split()
if len(parts) == 0:
return None
token = parts.pop(0).lower()
args = parts
if token == 'speechreq':
assert len(args) == 1, 'invalid command format'
elif token == 'speechreqwd':
assert len(args) == 1, 'invalid command format'
else:
print('unknown command: {}'.format(line))
return None
return CtrlCommand(token, args)
CtrlCommand = namedtuple('CtrlCommand', 'token, args')
class _CommandExecutor(object):
def __init__(self, cmdQueue, channelManager):
self._cmdQueue = cmdQueue
self._channelManager = channelManager
self._doRun = True
def run(self):
while True:
try:
cmd = self._cmdQueue.get(timeout=1)
except Empty:
if self._doRun:
continue
else:
break
self._process(cmd)
def initShutdown(self):
self._doRun = False
def _process(self, cmd):
if cmd.token in ('speechreq', 'speechreqwd'):
self._channelManager.handle(cmd)
return
# den Rest ignorieren
class _ChannelState(object):
def __init__(self, channel, registry):
self._channel = channel
self._registry = registry
def handle(self, cmd):
if isinstance(cmd, AvsCommand):
return self._handleAvsCommand(cmd)
else:
return self._handleCtrlCommand(cmd)
def _handleAvsCommand(self, cmd):
assert False, 'must be implemented'
def _handleCtrlCommand(self, cmd):
assert False, 'must be implemented'
def _send(self, line):
for sender in self._registry.senders:
sender.send(line)
class _Idle(_ChannelState):
def _handleAvsCommand(self, cmd):
if cmd.token == 'speechreq':
self._send('205 {} speechgrantop'.format(self._channel.ssrc))
return self._channel.avsTalking
return self
def _handleCtrlCommand(self, cmd):
if cmd.token == 'speechreq':
self._send('206 {} speechgrant'.format(self._channel.ssrc))
return self._channel.fuGeTalking
return self
def __str__(self):
return '{}: Idle'.format(self._channel.ssrc)
class _AvsTalking(_ChannelState):
def _handleAvsCommand(self, cmd):
if cmd.token == 'speechreqwd':
self._send('207 {} speechreqwd'.format(self._channel.ssrc))
return self._channel.idle
return self
def _handleCtrlCommand(self, cmd):
return self
def __str__(self):
return '{}: AvsTalking'.format(self._channel.ssrc)
class _FuGeTalking(_ChannelState):
def _handleAvsCommand(self, cmd):
return self
def _handleCtrlCommand(self, cmd):
if cmd.token == 'speechreqwd':
self._send('207 {} speechreqwd'.format(self._channel.ssrc))
return self._channel.idle
return self
def __str__(self):
return '{}: FuGeTalking'.format(self._channel.ssrc)
class _Channel(object):
def __init__(self, ssrc, registry):
self._ssrc = ssrc
self._idle = _Idle(weakref.proxy(self), registry)
self._avsTalking = _AvsTalking(weakref.proxy(self), registry)
self._fuGeTalking = _FuGeTalking(weakref.proxy(self), registry)
self._currentState = self._idle
@property
def ssrc(self): return self._ssrc
@property
def idle(self): return self._idle
@property
def avsTalking(self): return self._avsTalking
@property
def fuGeTalking(self): return self._fuGeTalking
def handle(self, cmd):
self._currentState = self._currentState.handle(cmd)
print(self._currentState)
class _ChannelManager(object):
def __init__(self, ssrcStart, channelCount, registry):
self._ssrcStart = ssrcStart
self._channels = []
for i in range(channelCount):
ssrc = i + self._ssrcStart
self._channels.append(_Channel(ssrc, registry))
def handle(self, cmd):
assert cmd.token in ('speechreq', 'speechreqwd'), 'cannot handle command: {} {}'.format(cmd.token, cmd.args)
try:
ssrc = int(cmd.args[0])
except (ValueError, IndexError):
assert False, 'ssrc expected as first arg: {} {}'.format(cmd.token, cmd.args)
index = ssrc - self._ssrcStart
assert index >= 0 and index < len(self._channels), 'ssrc out of range: {}'.format(ssrc)
channel = self._channels[index]
channel.handle(cmd)
def activateAllChannels(self, sender):
for c in self._channels:
sender.send('208 {} trcallactiv'.format(c.ssrc))
def _shutdownAndCloseSocket(sock):
try:
sock.shutdown(socket.SHUT_RD) # wichtig, close() alleine reicht nicht
except socket.error:
print('socket error')
# kann passieren, wenn der Socket schon geschlossen wurde
pass
sock.close()
def _parseCmdLine():
parser = ArgumentParser(description=textwrap.dedent('''
Simulator for the StReFu (Steuerrechner Funk).
The program simulates the interface described in GUEZSB60_ICD-0059_Anb-TrSpFu_AVS.
It provides a port where AVS_Interface instances can connect to.
At the console (stdin) it waits for control messages. With the following messages
a user can indicate that a radio unit was triggered or that the trigger was released:
- speechreq <ssrc>
- speechreqwd <ssrc>
The implemented channel management is very simple. The first request for a channel --
either from AVS or from the user -- wins. The channel stays allocated until it is
explicitly released.
The data streams for each channel must be provided separately.
'''),
formatter_class=RawDescriptionHelpFormatter)
parser.add_argument('--listen', '-l', type=int, metavar='N', default=8300, help='Listening port for connections from AVS_Interface; default: 8300')
parser.add_argument('--ssrc-start', type=int, metavar='N', default=1001, help='SSRC of the first channel; default: 1001')
parser.add_argument('--channel-count', '-c', type=int, metavar='N', default=56, help='Number of CNR channels; default: 56')
return parser.parse_args()
def _main():
options = _parseCmdLine()
cmdQueue = Queue()
registry = _Registry()
channelManager = _ChannelManager(options.ssrc_start, options.channel_count, registry)
handlerFactory = _RequestHandlerFactory(cmdQueue, registry, channelManager)
acceptor = _Acceptor(options.listen, registry, handlerFactory)
acceptorThread = Thread(target=acceptor.run)
executor = _CommandExecutor(cmdQueue, channelManager)
executorThread = Thread(target=executor.run)
controlRequestHandler = _ControlRequestHandler(sys.stdin, cmdQueue)
with ExitStack() as stack:
stack.enter_context(startThreadContext(executor, executorThread))
stack.enter_context(startThreadContext(acceptor, acceptorThread))
controlRequestHandler.handle() # beendet sich bei EOF oder Ctrl-C
if __name__ == '__main__':
_main()