Merge branch 'feature/setup' into 'develop'
Feature/setup See merge request nav/aman/aman-sys!1
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
Backend.ini
|
||||
main.py
|
||||
.vscode/
|
||||
*_pb2.py
|
||||
__pycache__
|
||||
*.egg-info
|
||||
build/
|
||||
dist/
|
||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "src/protobuf"]
|
||||
path = src/protobuf
|
||||
url = git@git.vatsim-germany.org:nav/aman-com.git
|
||||
branch = feature/protobuf
|
||||
29
README.md
29
README.md
@@ -1,7 +1,23 @@
|
||||
# aman-sys
|
||||
# Arrival MANanager (AMAN)
|
||||
|
||||
Dieses Repository stellt den VATGER AMAN Server bereit, dessen Aufgabe es ist, einen optimalen Arrivalflow für die Flugplätze in der VACC zu errechnen.
|
||||
## System description
|
||||
|
||||
AMAN is splitted up into four different components.
|
||||
* aman-com defines the diffent message types
|
||||
* aman-es implements an EuroScope plugin to communicate with [aman-sys](https://git.vatsim-germany.org/nav/aman-sys)
|
||||
* aman-sys implements the backend system to plan an optimal arrival sequence for the different airports
|
||||
* aman-web implements a web-interface to configure [aman-sys](https://git.vatsim-germany.org/nav/aman-sys) and visualize sequences
|
||||
|
||||
## Component description
|
||||
|
||||
AMAN uses [Protocol Buffers](https://developers.google.com/protocol-buffers)
|
||||
for the message serialization and message definition between the EuroScope instance and the AMAN backend.
|
||||
|
||||
Additionally is [ZeroMQ](https://zeromq.org/) used for the communication abstraction layer.
|
||||
|
||||
This component provides the server backend with the planning and optimization system per airport.
|
||||
It is designed as a python framework that can run on a webserver.
|
||||
ZMQ based encryption and authentication methods are used to authenticate controllers.
|
||||
|
||||
## RHC-ACS-ASS Algorithm
|
||||
Step 1: Initialization. Set up parameters for
|
||||
@@ -42,3 +58,12 @@ the best solution. Moreover, the current best solution is compared with the hist
|
||||
to determine the historically best solution.
|
||||
|
||||
Step 3.4: Perform the global pheromone updating as (10).
|
||||
|
||||
# Additional libraries
|
||||
|
||||
* [ZeroMQ](https://github.com/zeromq) - GNU GPLv3
|
||||
* [Protocol Buffers](https://github.com/protocolbuffers/protobuf) - BSD-3
|
||||
|
||||
# License
|
||||
|
||||
AMAN is released under the [GNU General Public License v3](LICENSE)
|
||||
|
||||
82
aman/AMAN.py
Normal file
82
aman/AMAN.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
|
||||
from aman.com import AircraftReport_pb2
|
||||
from aman.com.Euroscope import Euroscope
|
||||
from aman.config.AircraftPerformance import AircraftPerformance
|
||||
from aman.config.Airport import Airport
|
||||
from aman.config.System import System
|
||||
from aman.sys.Worker import Worker
|
||||
|
||||
class AMAN:
|
||||
def findConfigPath():
|
||||
envvar = os.environ.get('AMAN_CONFIG_PATH')
|
||||
if None == envvar:
|
||||
print('No AMAN_CONFIG_PATH in environment variables found. Using execution directory.')
|
||||
path = os.getcwd()
|
||||
else:
|
||||
print('AMAN_CONFIG_PATH found.')
|
||||
path = envvar
|
||||
|
||||
print('Config-path: ' + path)
|
||||
return path
|
||||
|
||||
def __init__(self):
|
||||
# default initialization of members
|
||||
self.systemConfig = None
|
||||
self.aircraftPerformance = None
|
||||
self.receiver = None
|
||||
self.workers = []
|
||||
self.inbounds = {}
|
||||
|
||||
configPath = AMAN.findConfigPath()
|
||||
|
||||
# read all system relevant configuration files
|
||||
self.systemConfig = System(os.path.join(configPath, 'System.ini'))
|
||||
print('Parsed System.ini')
|
||||
|
||||
# read the aircraft performance data
|
||||
self.aircraftPerformance = AircraftPerformance(os.path.join(configPath, 'PerformanceData.ini'))
|
||||
if None == self.aircraftPerformance:
|
||||
sys.stderr.write('No aircraft performance data found!')
|
||||
sys.exit(-1)
|
||||
else:
|
||||
print('Parsed PerformanceData.ini. Extracted ' + str(len(self.aircraftPerformance.aircrafts)) + ' aircrafts')
|
||||
|
||||
# find the airport configurations and create the workers
|
||||
airportsPath = os.path.join(os.path.join(configPath, 'airports'), '*.ini')
|
||||
for file in glob.glob(airportsPath):
|
||||
icao = os.path.splitext(os.path.basename(file))[0]
|
||||
|
||||
print('Parsing planner configuration for ' + icao)
|
||||
airportConfig = Airport(file, icao)
|
||||
|
||||
# initialize the worker thread
|
||||
worker = Worker(icao, airportConfig)
|
||||
worker.start()
|
||||
self.workers.append(worker)
|
||||
print('Starter worker for ' + icao)
|
||||
|
||||
# create the EuroScope receiver
|
||||
self.receiver = Euroscope(configPath, self.systemConfig.Server, self)
|
||||
|
||||
def __del__(self):
|
||||
if None != self.receiver:
|
||||
del self.receiver
|
||||
self.receiver = None
|
||||
|
||||
for worker in self.workers:
|
||||
worker.stop()
|
||||
|
||||
def updateAircraftReport(self, report : AircraftReport_pb2.AircraftReport):
|
||||
# find the correct worker for the inbound
|
||||
for worker in self.workers:
|
||||
if worker.icao == report.destination:
|
||||
print('Updated ' + report.aircraft.callsign + ' for ' + worker.icao)
|
||||
worker.acquire()
|
||||
worker.reportQueue[report.aircraft.callsign] = report
|
||||
worker.release()
|
||||
break
|
||||
2
aman/__init__.py
Normal file
2
aman/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
import com
|
||||
import tools
|
||||
104
aman/com/Euroscope.py
Normal file
104
aman/com/Euroscope.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import ctypes
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import zmq
|
||||
import zmq.auth
|
||||
|
||||
from aman.com import AircraftReport_pb2
|
||||
from aman.config.Server import Server
|
||||
|
||||
class ReceiverThread(threading.Thread):
|
||||
def __init__(self, socket, aman):
|
||||
threading.Thread.__init__(self)
|
||||
self.socket = socket
|
||||
self.aman = aman
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
try:
|
||||
msg = self.socket.recv(zmq.NOBLOCK)
|
||||
|
||||
# parse the received message
|
||||
report = AircraftReport_pb2.AircraftReport()
|
||||
report.ParseFromString(msg)
|
||||
|
||||
# try to associate the received aircraft to an airport
|
||||
self.aman.updateAircraftReport(report)
|
||||
|
||||
except zmq.ZMQError as error:
|
||||
if zmq.EAGAIN == error.errno:
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
else:
|
||||
return
|
||||
|
||||
def threadId(self):
|
||||
if hasattr(self, '_thread_id'):
|
||||
return self._thread_id
|
||||
for id, thread in threading._active.items():
|
||||
if thread is self:
|
||||
return id
|
||||
|
||||
def stopThread(self):
|
||||
id = self.threadId()
|
||||
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(id, ctypes.py_object(SystemExit))
|
||||
if 1 < res:
|
||||
ctypes.pythonapi.PyThreadState_SetAsyncExc(id, 0)
|
||||
|
||||
# @brief Receives and sends messages to EuroScope plugins
|
||||
class Euroscope:
|
||||
# @brief Initializes the ZMQ socket
|
||||
# @param[in] config The server configuration
|
||||
def __init__(self, configPath : str, config : Server, aman):
|
||||
self.context = zmq.Context()
|
||||
|
||||
# find the key directories
|
||||
serverKeyPath = os.path.join(os.path.join(configPath, 'keys'), 'server')
|
||||
if False == os.path.isdir(serverKeyPath):
|
||||
sys.stderr.write('No directory for the server key found')
|
||||
sys.exit(-1)
|
||||
print('Path to the server key: ' + serverKeyPath)
|
||||
|
||||
clientKeyPath = os.path.join(os.path.join(configPath, 'keys'), 'clients')
|
||||
if False == os.path.isdir(clientKeyPath):
|
||||
sys.stderr.write('No directory for the client keys found')
|
||||
sys.exit(-1)
|
||||
print('Path to the client keys: ' + clientKeyPath)
|
||||
|
||||
# read the certificates
|
||||
keyPairPath = glob.glob(os.path.join(serverKeyPath, '*.key_secret'))
|
||||
if 1 != len(keyPairPath):
|
||||
sys.stderr.write('No public-private keypair found for the server certificate')
|
||||
sys.exit(-1)
|
||||
keyPair = zmq.auth.load_certificate(keyPairPath[0])
|
||||
|
||||
# initialize the receiver
|
||||
self.receiverSocket = zmq.Socket(self.context, zmq.SUB)
|
||||
self.receiverSocket.setsockopt(zmq.CURVE_PUBLICKEY, keyPair[0])
|
||||
self.receiverSocket.setsockopt(zmq.CURVE_SECRETKEY, keyPair[1])
|
||||
self.receiverSocket.setsockopt(zmq.CURVE_SERVER, True)
|
||||
self.receiverSocket.bind('tcp://' + config.Address + ':' + str(config.PortReceiver))
|
||||
self.receiverSocket.setsockopt(zmq.SUBSCRIBE, b'')
|
||||
self.receiverThread = ReceiverThread(self.receiverSocket, aman)
|
||||
self.receiverThread.start()
|
||||
print('Listening at tcp://' + config.Address + ':' + str(config.PortReceiver))
|
||||
|
||||
# initialize the notification
|
||||
self.notificationSocket = zmq.Socket(self.context, zmq.PUB)
|
||||
self.notificationSocket.setsockopt(zmq.CURVE_PUBLICKEY, keyPair[0])
|
||||
self.notificationSocket.setsockopt(zmq.CURVE_SECRETKEY, keyPair[1])
|
||||
self.notificationSocket.setsockopt(zmq.CURVE_SERVER, True)
|
||||
self.notificationSocket.bind('tcp://' + config.Address + ':' + str(config.PortNotification))
|
||||
print('Publishing at tcp://' + config.Address + ':' + str(config.PortNotification))
|
||||
|
||||
def __del__(self):
|
||||
self.receiverThread.stopThread()
|
||||
self.receiverThread.join()
|
||||
self.receiverSocket.close()
|
||||
self.notificationSocket.close()
|
||||
0
aman/com/__init__.py
Normal file
0
aman/com/__init__.py
Normal file
28
aman/config/AircraftPerformance.py
Normal file
28
aman/config/AircraftPerformance.py
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import configparser
|
||||
|
||||
from aman.types.PerformanceData import PerformanceData
|
||||
|
||||
class AircraftPerformance:
|
||||
def __init__(self, filepath : str):
|
||||
config = configparser.ConfigParser()
|
||||
config.read(filepath)
|
||||
self.aircrafts = { }
|
||||
|
||||
# iterate over all entries
|
||||
for key in config:
|
||||
if 'DEFAULT' == key:
|
||||
continue
|
||||
|
||||
aircraft = PerformanceData(key)
|
||||
|
||||
aircraft.speedAboveFL240 = config[key]['speedabovefl240']
|
||||
aircraft.rodAboveFL240 = config[key]['rodabovefl240']
|
||||
aircraft.speedAboveFL100 = config[key]['speedabovefl100']
|
||||
aircraft.rodAboveFL100 = config[key]['rodabovefl100']
|
||||
aircraft.speedBelowFL100 = config[key]['speedbelowfl100']
|
||||
aircraft.rodBelowFL100 = config[key]['rodbelowfl100']
|
||||
aircraft.speedApproach = config[key]['speedapproach']
|
||||
|
||||
self.aircrafts[aircraft.icao] = aircraft
|
||||
66
aman/config/Airport.py
Normal file
66
aman/config/Airport.py
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import configparser
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
|
||||
from formats.SctEseFormat import SctEseFormat
|
||||
|
||||
class Airport:
|
||||
def findGngData(data, path):
|
||||
if None == data.get('gngwildcard'):
|
||||
return None, None
|
||||
|
||||
# find the newest ESE file
|
||||
files = glob.glob(os.path.join(path, data['gngwildcard'] + '.ese'))
|
||||
latestEse = max(files, key=os.path.getctime)
|
||||
|
||||
# search for the corresponding SCT file
|
||||
latestSct = os.path.splitext(latestEse)[0] + '.sct'
|
||||
|
||||
# check if the files exist
|
||||
if False == os.path.isfile(latestEse) or False == os.path.isfile(latestSct):
|
||||
return None, None
|
||||
|
||||
return latestSct, latestEse
|
||||
|
||||
def parsePlanning(self, planning):
|
||||
if None == planning.get('routes'):
|
||||
return []
|
||||
return planning['routes'].split(':')
|
||||
|
||||
def __init__(self, filepath : str, icao : str):
|
||||
self.arrivalRoutes = {}
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(filepath)
|
||||
|
||||
dataConfig = None
|
||||
planningConfig = None
|
||||
|
||||
# search the required sections
|
||||
for key in config:
|
||||
if 'DATA' == key:
|
||||
dataConfig = config['DATA']
|
||||
elif 'PLANNING' == key:
|
||||
planningConfig = config['PLANNING']
|
||||
|
||||
# find the GNG-file data
|
||||
sctFile, eseFile = Airport.findGngData(dataConfig, os.path.dirname(filepath))
|
||||
if None == sctFile or None == eseFile:
|
||||
sys.stderr.write('No GNG-files found')
|
||||
sys.exit(-1)
|
||||
|
||||
# parse the planning information
|
||||
if None == planningConfig or False == self.parsePlanning(planningConfig):
|
||||
sys.stderr.write('No planning configuration found')
|
||||
sys.exit(-1)
|
||||
requiredArrivalRoutes = self.parsePlanning(planningConfig)
|
||||
if 0 == len(requiredArrivalRoutes):
|
||||
sys.stderr.write('No valid planning configuration found')
|
||||
sys.exit(-1)
|
||||
|
||||
# parse the GNG data
|
||||
print('Used GNG-Data: ' + eseFile)
|
||||
self.gngData = SctEseFormat(sctFile, eseFile, icao, requiredArrivalRoutes)
|
||||
29
aman/config/Server.py
Normal file
29
aman/config/Server.py
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import configparser;
|
||||
import sys
|
||||
|
||||
class Server():
|
||||
def __init__(self, config : configparser.ConfigParser):
|
||||
self.Address = None
|
||||
self.PortReceiver = None
|
||||
self.PortNotification = None
|
||||
|
||||
# search the required sections
|
||||
for key in config:
|
||||
if 'address' == key:
|
||||
self.Address = config['address']
|
||||
elif 'portreceiver' == key:
|
||||
self.PortReceiver = int(config['portreceiver'])
|
||||
elif 'portnotification' == key:
|
||||
self.PortNotification = int(config['portnotification'])
|
||||
|
||||
if self.Address is None:
|
||||
sys.stderr.write('No server-address configuration found!')
|
||||
sys.exit(-1)
|
||||
if self.PortReceiver is None:
|
||||
sys.stderr.write('No server-port-receiver configuration found!')
|
||||
sys.exit(-1)
|
||||
if self.PortNotification is None:
|
||||
sys.stderr.write('No server-port-notification configuration found!')
|
||||
sys.exit(-1)
|
||||
23
aman/config/System.py
Normal file
23
aman/config/System.py
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import configparser
|
||||
import sys
|
||||
|
||||
from aman.config.Server import Server
|
||||
|
||||
class System:
|
||||
def __init__(self, filepath : str):
|
||||
config = configparser.ConfigParser()
|
||||
config.read(filepath)
|
||||
|
||||
# search the required sections
|
||||
serverSectionAvailable = False
|
||||
for key in config:
|
||||
if 'SERVER' == key:
|
||||
serverSectionAvailable = True
|
||||
|
||||
if not serverSectionAvailable:
|
||||
sys.stderr.write('No server-configuration section found!')
|
||||
sys.exit(-1)
|
||||
|
||||
self.Server = Server(config['SERVER'])
|
||||
0
aman/config/__init__.py
Normal file
0
aman/config/__init__.py
Normal file
152
aman/formats/SctEseFormat.py
Normal file
152
aman/formats/SctEseFormat.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
|
||||
from aman.types.ArrivalRoute import ArrivalRoute
|
||||
from aman.types.Waypoint import Waypoint
|
||||
|
||||
class SctEseFormat:
|
||||
def readFile(filename : str):
|
||||
fileBlocks = {}
|
||||
block = None
|
||||
|
||||
# read the file line by line and create segments based on []-entries
|
||||
with open(filename) as file:
|
||||
for line in file:
|
||||
line = line.strip()
|
||||
|
||||
# found a new segment
|
||||
if line.startswith('['):
|
||||
block = line[1:-1]
|
||||
fileBlocks.setdefault(block, [])
|
||||
# append the last backend
|
||||
elif None != block and 0 != len(line):
|
||||
fileBlocks[block].append(line)
|
||||
|
||||
return fileBlocks
|
||||
|
||||
def parseWaypoint(waypoint : str, nameIdx : int, latitudeIdx : int, longitudeIdx : int):
|
||||
split = list(filter(None, waypoint.split(' ')))
|
||||
if len(split) <= longitudeIdx:
|
||||
sys.stderr.write('Invalid waypoint format: ' + waypoint)
|
||||
sys.exit(-1)
|
||||
return Waypoint(split[nameIdx], Waypoint.dms2dd(split[latitudeIdx]), Waypoint.dms2dd(split[longitudeIdx]))
|
||||
|
||||
def extractWaypoints(self, sctFilepath : str):
|
||||
config = SctEseFormat.readFile(sctFilepath)
|
||||
foundAirports = False
|
||||
foundVOR = False
|
||||
foundNDB = False
|
||||
foundFix = False
|
||||
|
||||
for key in config:
|
||||
if 'VOR' == key:
|
||||
foundVOR = True
|
||||
elif 'NDB' == key:
|
||||
foundNDB = True
|
||||
elif 'FIXES' == key:
|
||||
foundFix = True
|
||||
elif 'AIRPORT' == key:
|
||||
foundAirports = True
|
||||
|
||||
if False == foundVOR:
|
||||
sys.stderr.write('Unable to find VOR-entries in the sector file')
|
||||
sys.exit(-1)
|
||||
if False == foundNDB:
|
||||
sys.stderr.write('Unable to find NDB-entries in the sector file')
|
||||
sys.exit(-1)
|
||||
if False == foundFix:
|
||||
sys.stderr.write('Unable to find FIX-entries in the sector file')
|
||||
sys.exit(-1)
|
||||
if False == foundAirports:
|
||||
sys.stderr.write('Unable to find AIRPORT-entries in the sector file')
|
||||
sys.exit(-1)
|
||||
|
||||
# extract all waypoints
|
||||
for waypoint in config['VOR']:
|
||||
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 2, 3)
|
||||
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
|
||||
for waypoint in config['NDB']:
|
||||
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 1, 2)
|
||||
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
|
||||
for waypoint in config['FIXES']:
|
||||
waypoint = SctEseFormat.parseWaypoint(waypoint, 0, 1, 2)
|
||||
self.waypoints.setdefault(waypoint.name, []).append(waypoint)
|
||||
|
||||
# extract the airports
|
||||
for airport in config['AIRPORT']:
|
||||
airport = SctEseFormat.parseWaypoint(airport,0, 2, 3)
|
||||
self.airports.setdefault(airport.name, []).append(airport)
|
||||
|
||||
def parseArrivalRoute(self, route : str, airport : Waypoint):
|
||||
# split the route and validate that it is a STAR for the airport
|
||||
split = route.split(':')
|
||||
if 5 != len(split) or 'STAR' != split[0] or split[1] != airport.name:
|
||||
return None
|
||||
|
||||
# find all waypoints
|
||||
waypoints = []
|
||||
route = list(filter(None, split[4].split(' ')))
|
||||
for waypoint in route:
|
||||
# find the waypoint in the route
|
||||
coordinates = self.waypoints[waypoint]
|
||||
# no waypoint with this name defined
|
||||
if None == coordinates:
|
||||
sys.stderr.write('Unable to find waypoint ' + waypoint)
|
||||
sys.exit(-1)
|
||||
# multiple waypoints, but use Haversine distance to distinct between candidates
|
||||
elif 1 != len(coordinates):
|
||||
minDistance = sys.float_info.max
|
||||
nearest = None
|
||||
|
||||
# we assume that waypoints with the same name are not close each other
|
||||
for coordinate in coordinates:
|
||||
distance = coordinate.haversine(airport)
|
||||
# found a closer waypoint
|
||||
if minDistance > distance:
|
||||
minDistance = distance
|
||||
nearest = coordinate
|
||||
|
||||
if None == nearest:
|
||||
sys.stderr.write('Unable to find a close waypoint for ' + waypoint)
|
||||
sys.exit(-1)
|
||||
|
||||
waypoints.append(nearest)
|
||||
# extend the list of waypoints
|
||||
else:
|
||||
waypoints.append(coordinates[0])
|
||||
|
||||
# create the arrival route
|
||||
return ArrivalRoute(split[3], split[2], waypoints)
|
||||
|
||||
def extractArrivalRoutes(self, eseFilepath : str, airport : str, allowedRoutes : list):
|
||||
config = SctEseFormat.readFile(eseFilepath)
|
||||
foundSidsStars = False
|
||||
|
||||
# search the airport in the extracted list
|
||||
if not airport in self.airports:
|
||||
sys.stderr.write(airport + 'in self.airports', 'Unable to find the requested airport')
|
||||
sys.exit(-1)
|
||||
airport = self.airports[airport][0]
|
||||
|
||||
for key in config:
|
||||
if 'SIDSSTARS' == key:
|
||||
foundSidsStars = True
|
||||
|
||||
if False == foundSidsStars:
|
||||
sys.stderr.write('Unable to find SIDSSTARS-entries in the sector file')
|
||||
sys.exit(-1)
|
||||
|
||||
# parse all arrival routes
|
||||
for line in config['SIDSSTARS']:
|
||||
route = self.parseArrivalRoute(line, airport)
|
||||
if None != route and route.name in allowedRoutes:
|
||||
self.arrivalRoutes.setdefault(route.runway, []).append(route)
|
||||
|
||||
def __init__(self, sctFilepath : str, eseFilepath : str, airport : str, allowedRoutes : list):
|
||||
self.arrivalRoutes = {}
|
||||
self.waypoints = {}
|
||||
self.airports = {}
|
||||
|
||||
self.extractWaypoints(sctFilepath)
|
||||
self.extractArrivalRoutes(eseFilepath, airport, allowedRoutes)
|
||||
0
aman/formats/__init__.py
Normal file
0
aman/formats/__init__.py
Normal file
33
aman/sys/Worker.py
Normal file
33
aman/sys/Worker.py
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from threading import Thread, Lock
|
||||
import time
|
||||
|
||||
from aman.config.Airport import Airport
|
||||
|
||||
class Worker(Thread):
|
||||
def __init__(self, icao : str, configuration : Airport):
|
||||
Thread.__init__(self)
|
||||
|
||||
self.stopThread = None
|
||||
self.icao = icao
|
||||
self.configuration = configuration
|
||||
self.arrivalRoutes = configuration.gngData.arrivalRoutes
|
||||
self.updateLock = Lock()
|
||||
self.reportQueue = {}
|
||||
|
||||
def stop(self):
|
||||
self.stopThread = True
|
||||
|
||||
def run(self):
|
||||
counter = 0
|
||||
|
||||
while None == self.stopThread:
|
||||
time.sleep(1)
|
||||
counter += 1
|
||||
if 0 != (counter % 60):
|
||||
continue
|
||||
|
||||
# TODO handle the report queue and update internal information
|
||||
# TODO execute planning, etc.
|
||||
continue
|
||||
0
aman/sys/__init__.py
Normal file
0
aman/sys/__init__.py
Normal file
44
aman/tools/KeyPairCreator.py
Normal file
44
aman/tools/KeyPairCreator.py
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from typing import Tuple
|
||||
import zmq.auth
|
||||
|
||||
# @brief Creates a new keypair for ZMQ encryption
|
||||
# @param[in] directory The location where to store the keys
|
||||
# @return The public and private key tuple
|
||||
def KeyPairCreator(directory: str, server: bool) -> Tuple[str, str]:
|
||||
if not server:
|
||||
print('Creating a new pair for a client...')
|
||||
target = 'client'
|
||||
else:
|
||||
print('Creating a new pair for the server...')
|
||||
target = 'server'
|
||||
|
||||
public, private = zmq.auth.create_certificates(directory, target)
|
||||
return (public, private)
|
||||
|
||||
def str2bool(value):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
elif value.lower() in ('yes', 'true', 't', 'y', '1'):
|
||||
return True
|
||||
elif value.lower() in ('no', 'false', 'f', 'n', '0'):
|
||||
return False
|
||||
else:
|
||||
raise argparse.ArgumentTypeError('Boolean value expected')
|
||||
|
||||
if __name__ == '__main__':
|
||||
# create the commandline parser
|
||||
parser = argparse.ArgumentParser(description='Create a new key-value pair')
|
||||
parser.add_argument('directory', help='Directory where to store the key pair')
|
||||
parser.add_argument('--server', default=False, action='store_true', help="Creates server key pair")
|
||||
args = parser.parse_args()
|
||||
|
||||
# create the directory if it does not exist
|
||||
if not os.path.exists(args.directory):
|
||||
os.makedirs(args.directory)
|
||||
|
||||
# create the keys
|
||||
KeyPairCreator(args.directory, args.server)
|
||||
151
aman/tools/SkybraryAircraftCrawler.py
Normal file
151
aman/tools/SkybraryAircraftCrawler.py
Normal file
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import configparser
|
||||
import os
|
||||
import urllib.request
|
||||
from bs4 import BeautifulSoup
|
||||
from aman.types.PerformanceData import PerformanceData
|
||||
|
||||
def findAircraftPages(rooturl : str, suburl : str):
|
||||
aircrafts = []
|
||||
|
||||
with urllib.request.urlopen(rooturl + suburl) as site:
|
||||
data = site.read().decode('utf-8')
|
||||
site.close()
|
||||
|
||||
parsed = BeautifulSoup(data, features='lxml')
|
||||
|
||||
for link in parsed.body.find_all('a', title=True):
|
||||
split = link['href'].split('/')
|
||||
if 3 == len(split) and split[2] == link['title'] and 'Category' not in link['title'] and 'Special' not in link['href']:
|
||||
aircrafts.append(rooturl + link['href'])
|
||||
|
||||
for link in parsed.body.find_all('a', attrs={ 'title': 'Category:Aircraft' }):
|
||||
if 'previous' not in link.text:
|
||||
aircrafts.extend(findAircraftPages(rooturl, link['href']))
|
||||
|
||||
return aircrafts
|
||||
|
||||
def findAndParseEntry(tableRow, startIdx, substring, default):
|
||||
while 0 < startIdx:
|
||||
if substring in tableRow[startIdx].text:
|
||||
split = tableRow[startIdx].text.split(' ')
|
||||
if 1 >= len(split):
|
||||
return default, startIdx - 2
|
||||
else:
|
||||
return int(split[0]), startIdx - 2
|
||||
else:
|
||||
startIdx -= 1
|
||||
|
||||
return 0, -1
|
||||
|
||||
def findAndParseSpeedEntry(tableRow, startIdx, default):
|
||||
return findAndParseEntry(tableRow, startIdx, 'kts', default)
|
||||
|
||||
def findAndParseRodEntry(tableRow, startIdx, default):
|
||||
return findAndParseEntry(tableRow, startIdx, 'ft/min', default)
|
||||
|
||||
def parsePerformanceEntries(tableRowSpeeds, tableRowRODs):
|
||||
speeds = []
|
||||
rods = []
|
||||
|
||||
# parse the speed data
|
||||
idx = len(tableRowSpeeds) - 1
|
||||
while 0 < idx:
|
||||
parsed = findAndParseSpeedEntry(tableRowSpeeds, idx, 140 if 0 == len(speeds) else 250)
|
||||
if 0 < idx:
|
||||
speeds.append(parsed[0])
|
||||
idx = parsed[1]
|
||||
|
||||
# parse the ROD data
|
||||
idx = len(tableRowRODs) - 1
|
||||
while 0 < idx:
|
||||
parsed = findAndParseRodEntry(tableRowRODs, idx, 2000)
|
||||
if 0 < idx:
|
||||
rods.append(parsed[0])
|
||||
idx = parsed[1]
|
||||
|
||||
return speeds, rods
|
||||
|
||||
def parsePerformanceData(url : str):
|
||||
with urllib.request.urlopen(url) as site:
|
||||
data = site.read().decode('utf-8')
|
||||
site.close()
|
||||
|
||||
# check if we find the ICAO code
|
||||
parsed = BeautifulSoup(data, features='lxml')
|
||||
icao = parsed.body.find('h5', attrs={ 'id' : 'siteSub', 'class' : 'subtitle'})
|
||||
if None == icao or '' == icao.text:
|
||||
return False, None
|
||||
|
||||
aircraft = PerformanceData(icao.text)
|
||||
performanceTable = parsed.body.find('table', attrs={ 'class' : 'wikitable', 'style' : 'font-size: 90%;' })
|
||||
if None == performanceTable or None == performanceTable.find_all('tr')[1] or None == performanceTable.find_all('tr')[2]:
|
||||
return False, None
|
||||
|
||||
speeds, rods = parsePerformanceEntries(performanceTable.find_all('tr')[1].find_all('td'),
|
||||
performanceTable.find_all('tr')[2].find_all('td'))
|
||||
if 10 > len(speeds):
|
||||
speeds.insert(1, speeds[1])
|
||||
|
||||
# create the speed data
|
||||
if len(speeds) >= 4:
|
||||
aircraft.speedApproach = speeds[0]
|
||||
aircraft.speedBelowFL100 = speeds[1]
|
||||
aircraft.speedAboveFL100 = speeds[2]
|
||||
aircraft.speedAboveFL240 = speeds[3]
|
||||
# create the ROD data
|
||||
if len(rods) >= 3:
|
||||
aircraft.rodBelowFL100 = rods[0]
|
||||
aircraft.rodAboveFL100 = rods[1]
|
||||
aircraft.rodAboveFL240 = rods[2]
|
||||
|
||||
return len(speeds) >= 4 and len(rods) >= 3, aircraft
|
||||
|
||||
if __name__ == '__main__':
|
||||
# create the commandline parser
|
||||
parser = argparse.ArgumentParser(description='Extract the aircraft performace data')
|
||||
parser.add_argument('directory', help='Directory where to store the performance data configuration')
|
||||
args = parser.parse_args()
|
||||
|
||||
# create the directory if it does not exist
|
||||
if not os.path.exists(args.directory):
|
||||
os.makedirs(args.directory)
|
||||
|
||||
# parse the aircrafts
|
||||
links = findAircraftPages('https://www.skybrary.aero', '/index.php?title=Category:Aircraft')
|
||||
print('Found ' + str(len(links)) + ' aircrafts')
|
||||
|
||||
aircrafts = []
|
||||
parsed = 0
|
||||
for link in links:
|
||||
valid, aircraft = parsePerformanceData(link)
|
||||
|
||||
parsed += 1
|
||||
print('Parsed ' + str(parsed) + ' of ' + str(len(links)), end='\r')
|
||||
|
||||
if False == valid:
|
||||
print('Unable to find performance data for ' + link)
|
||||
continue
|
||||
|
||||
aircrafts.append(aircraft)
|
||||
|
||||
print('Successfully parsed ' + str(len(aircrafts)) + ' of ' + str(len(links)) + ' aircrafts')
|
||||
|
||||
# create the configuration file
|
||||
config = configparser.ConfigParser()
|
||||
for aircraft in aircrafts:
|
||||
config[aircraft.icao] = {
|
||||
'speedAboveFL240' : aircraft.speedAboveFL240,
|
||||
'rodAboveFL240' : aircraft.rodAboveFL240,
|
||||
'speedAboveFL100' : aircraft.speedAboveFL100,
|
||||
'rodAboveFL100' : aircraft.rodAboveFL100,
|
||||
'speedBelowFL100' : aircraft.speedBelowFL100,
|
||||
'rodBelowFL100' : aircraft.rodBelowFL100,
|
||||
'speedApproach' : aircraft.speedApproach
|
||||
}
|
||||
|
||||
# write the configuration data
|
||||
with open(args.directory + '/PerformanceData.ini', 'w') as file:
|
||||
config.write(file)
|
||||
0
aman/tools/__init__.py
Normal file
0
aman/tools/__init__.py
Normal file
11
aman/types/ArrivalRoute.py
Normal file
11
aman/types/ArrivalRoute.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
class ArrivalRoute:
|
||||
def __init__(self, name : str, runway : str, waypoints : list):
|
||||
self.name = name
|
||||
self.runway = runway
|
||||
self.iaf = waypoints[0]
|
||||
self.route = waypoints
|
||||
|
||||
def __str__(self):
|
||||
return 'Name: ' + self.name + ', IAF: ' + self.iaf.name + ', RWY: ' + self.runway
|
||||
7
aman/types/Inbound.py
Normal file
7
aman/types/Inbound.py
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from aman.com import AircraftReport_pb2
|
||||
|
||||
class Inbound:
|
||||
def __init__(self, report : AircraftReport_pb2.AircraftReport):
|
||||
self.report = report
|
||||
18
aman/types/PerformanceData.py
Normal file
18
aman/types/PerformanceData.py
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
class PerformanceData:
|
||||
def __init__(self, icao : str):
|
||||
self.icao = icao
|
||||
self.speedAboveFL240 = 0.0
|
||||
self.speedAboveFL100 = 0.0
|
||||
self.speedBelowFL100 = 0.0
|
||||
self.speedApproach = 0.0
|
||||
self.rodAboveFL240 = 0.0
|
||||
self.rodAboveFL100 = 0.0
|
||||
self.rodBelowFL100 = 2000.0
|
||||
|
||||
def __str__(self):
|
||||
return 'ICAO: ' + self.icao + ', FL240: ' + str(self.rodAboveFL240) + '@' + str(self.speedAboveFL240) + \
|
||||
', +FL100: ' + str(self.rodAboveFL100) + '@' + str(self.speedAboveFL100) + \
|
||||
', -FL100: ' + str(self.rodBelowFL100) + '@' + str(self.speedBelowFL100) + \
|
||||
', Vapp: ' + str(self.speedApproach)
|
||||
33
aman/types/Waypoint.py
Normal file
33
aman/types/Waypoint.py
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from sklearn.metrics.pairwise import haversine_distances
|
||||
import numpy as np
|
||||
|
||||
class Waypoint:
|
||||
def dms2dd(coordinate : str):
|
||||
split = coordinate.split('.')
|
||||
if 4 != len(split):
|
||||
return 0.0
|
||||
|
||||
direction = split[0][1]
|
||||
degrees = float(split[0][1:])
|
||||
minutes = float(split[1])
|
||||
seconds = float(split[2]) * (float(split[3]) / 1000.0)
|
||||
|
||||
dd = degrees + minutes / 60.0 + seconds / (60 * 60)
|
||||
if 'E' == direction or 'S' == direction:
|
||||
dd *= -1.0
|
||||
|
||||
return dd
|
||||
|
||||
def __init__(self, name : str, latitude : float, longitude : float):
|
||||
self.name = name
|
||||
self.coordinate = np.array([ latitude, longitude ])
|
||||
|
||||
def __str__(self):
|
||||
return 'Name: ' + self.name + ', Lat: ' + str(self.coordinate[0]) + ', Lon: ' + str(self.coordinate[1])
|
||||
|
||||
def haversine(self, other):
|
||||
self_radians = [np.radians(_) for _ in self.coordinate]
|
||||
other_radians = [np.radians(_) for _ in other.coordinate]
|
||||
return 6371.0 * haversine_distances([self_radians, other_radians])[0][1]
|
||||
0
aman/types/__init__.py
Normal file
0
aman/types/__init__.py
Normal file
BIN
external/bin/protoc.exe
vendored
Normal file
BIN
external/bin/protoc.exe
vendored
Normal file
Binary file not shown.
32
external/licenses/ProtoBuf-3.17.3
vendored
Normal file
32
external/licenses/ProtoBuf-3.17.3
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
Copyright 2008 Google Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Code generated by the Protocol Buffer compiler is owned by the owner
|
||||
of the input file used when generating it. This code is not
|
||||
standalone and requires a support library to be linked with it. This
|
||||
support library is itself covered by the above license.
|
||||
101
setup.py
Normal file
101
setup.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from setuptools import setup
|
||||
from distutils.command.clean import clean as _clean
|
||||
from distutils.command.build_py import build_py_2to3 as _build_py
|
||||
|
||||
# @brief Creates Protobuf python files to encode and decode messages
|
||||
# @param[in] source The protobuf source file
|
||||
def generateProtobuf(source):
|
||||
output = source.replace('.proto', '_pb2.py').replace('src/protobuf/', 'aman/com/')
|
||||
|
||||
if (not os.path.exists(output) or (os.path.exists(source) and os.path.getmtime(source) > os.path.getmtime(output))):
|
||||
print('Generating %s...' % output)
|
||||
|
||||
if not os.path.exists(source):
|
||||
sys.stderr.write('Cannot find %s' % source)
|
||||
sys.exit(-1)
|
||||
|
||||
if not os.path.exists('external/bin/protoc.exe'):
|
||||
sys.stderr.write('Cannot find proto-compiler')
|
||||
sys.exit(-1)
|
||||
|
||||
command = [ 'external/bin/protoc.exe', '-Isrc/protobuf/', '-I.', '--python_out=aman/com/', source]
|
||||
if 0 != subprocess.call(command):
|
||||
sys.exit(-1)
|
||||
|
||||
# check if we need to replace some import commands
|
||||
replaced = False
|
||||
content = open(output, 'r').read()
|
||||
for entry in re.findall('import.[A-Z].*.as.*', content):
|
||||
content = content.replace(entry, 'from . ' + entry)
|
||||
replaced = True
|
||||
|
||||
# update the content
|
||||
if replaced:
|
||||
with open(output, 'w') as file:
|
||||
file.write(content)
|
||||
|
||||
# @brief Cleans up all auto-generated files and folders
|
||||
# @param[in] _clean Instance of setuptools to clean up the system
|
||||
class clean(_clean):
|
||||
def run(self):
|
||||
for (dirpath, dirnames, filenames) in os.walk('.'):
|
||||
for filename in filenames:
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
if filepath.endswith('_pb2.py') or filepath.endswith('.pyc'):
|
||||
os.remove(filepath)
|
||||
for dirname in dirnames:
|
||||
if 'Arrival_MANager.egg-info' == dirname or 'build' == dirname or 'dist' == dirname or '__pycache__' == dirname:
|
||||
shutil.rmtree(os.path.join(dirpath, dirname))
|
||||
_clean.run(self)
|
||||
|
||||
# @brief Generates the python files and folders to set up the development/runtime environment
|
||||
# @param[in] _build_py Instance of setuptools to build the system
|
||||
class build_py(_build_py):
|
||||
def run(self):
|
||||
generateProtobuf('src/protobuf/Aircraft.proto')
|
||||
generateProtobuf('src/protobuf/AircraftReport.proto')
|
||||
generateProtobuf('src/protobuf/AircraftSchedule.proto')
|
||||
generateProtobuf('src/protobuf/BaseTypes.proto')
|
||||
_build_py.run(self)
|
||||
|
||||
with open('README.md', 'r') as f:
|
||||
longDescription = f.read()
|
||||
|
||||
setup(
|
||||
name = 'Arrival MANager',
|
||||
version = '0.1.0',
|
||||
packages = [
|
||||
'aman',
|
||||
'aman.com',
|
||||
'aman.config',
|
||||
'aman.formats',
|
||||
'aman.sys',
|
||||
'aman.tools',
|
||||
'aman.types'
|
||||
],
|
||||
namespace_packages = [ 'aman' ],
|
||||
description = 'AMAN optimization backend',
|
||||
long_description = longDescription,
|
||||
author = 'Sven Czarnian',
|
||||
author_email = 'devel@svcz.de',
|
||||
license = 'GPLv3',
|
||||
cmdclass = { 'clean': clean, 'build_py': build_py },
|
||||
install_requires=[
|
||||
'argparse',
|
||||
'bs4',
|
||||
'configparser',
|
||||
'numpy',
|
||||
'protobuf',
|
||||
'pyzmq',
|
||||
'scikit-learn',
|
||||
'scipy',
|
||||
'setuptools'
|
||||
]
|
||||
)
|
||||
1
src/protobuf
Submodule
1
src/protobuf
Submodule
Submodule src/protobuf added at 3c74c96cfd
Reference in New Issue
Block a user