Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1c48d7851 | ||
|
|
a86dfa01d8 | ||
|
|
c6d22d2067 | ||
|
|
23add20513 | ||
|
|
fd324ea747 | ||
|
|
ebea408267 | ||
|
|
8b43991c50 | ||
|
|
11eae85e35 | ||
|
|
7e17bf0103 | ||
|
|
667829b03d | ||
|
|
dc2a435e8e | ||
|
|
484be00e8c | ||
|
|
b69c584fb4 | ||
|
|
af52103ec8 | ||
|
|
11e76a3f24 | ||
|
|
b54f7dfc50 | ||
|
|
2687f543ad | ||
|
|
3743f31b84 | ||
|
|
d851efcd4d | ||
|
|
9fd05aa932 | ||
|
|
d07751cf77 | ||
|
|
715433bac6 | ||
|
|
9de9b813ba | ||
|
|
1561335e1b | ||
|
|
b516333ede | ||
|
|
793d92ff83 | ||
|
|
fa38924936 | ||
|
|
87d813d0a4 | ||
|
|
cf191a6ff1 | ||
|
|
aaa37a5f62 | ||
|
|
744ad71b6c | ||
|
|
479d7b2d44 | ||
|
|
64c238899a | ||
|
|
518e80e2fe | ||
|
|
46cc87eb3b | ||
|
|
bd7cbe91ed | ||
|
|
f6643d899f | ||
|
|
b10fae513e | ||
|
|
d60d5cb716 | ||
|
|
b3b5b3e547 | ||
|
|
9c7d8db006 | ||
|
|
8cd5aa6baf | ||
|
|
df455df689 | ||
|
|
1374ad95c9 | ||
|
|
31150adb9e | ||
|
|
e4715abda3 | ||
|
|
51b4013e6b | ||
|
|
f4fbd6245b | ||
|
|
1d50f0e9af | ||
|
|
8f81b65df8 | ||
|
|
c7738346bb | ||
|
|
4e8e8f15e4 | ||
|
|
5a2b9983b6 | ||
|
|
36d2bfa8a0 | ||
|
|
0fdcf8e99e | ||
|
|
6b2072f43b | ||
|
|
04b299730a | ||
|
|
8199b33d53 | ||
|
|
153930e73c | ||
|
|
7b26e27c9d | ||
|
|
a3f4f8f41b | ||
|
|
59e458c70b | ||
|
|
a0c9676c78 | ||
|
|
b92f437fcb | ||
|
|
355a5463e5 | ||
|
|
d4c07824c6 | ||
|
|
bbd45778db | ||
|
|
bd2d431c41 | ||
|
|
3076821b3a | ||
|
|
0f885c1e00 | ||
|
|
e112ee9694 | ||
|
|
7d540a9b85 | ||
|
|
30729676ac | ||
|
|
c80d230946 | ||
|
|
a0d4a1e0d3 |
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
|
||||||
69
README.md
69
README.md
@@ -1,2 +1,69 @@
|
|||||||
# aman-sys
|
# Arrival MANanager (AMAN)
|
||||||
|
|
||||||
|
## 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
|
||||||
|
the RHC, and set the current receding horizon k = 1.
|
||||||
|
|
||||||
|
Step 2: Find out all the M aircraft whose PLTs belong to
|
||||||
|
the kth receding horizon.
|
||||||
|
|
||||||
|
Step 3: Schedule the M aircraft in the kth receding horizon
|
||||||
|
by using an ACS.
|
||||||
|
|
||||||
|
Step 4: Assign the aircraft whose ALTs belong to kth scheduled window ω(k) to land on
|
||||||
|
the runway.
|
||||||
|
|
||||||
|
Step 5: Modify the PLT for those aircraft whose PLT belongs to ω(k) but the ALT does not belong to ω(k). The modification is to set their PLT to kTTI, making them belong to Ω(k + 1), such that they can be scheduled in the next receding horizon.
|
||||||
|
|
||||||
|
Step 6: Termination check. When all the aircraft have been assigned to land at the runway, the algorithm terminates. Otherwise, set k = k + 1 and go to Step 2 for the next receding horizon optimization.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
In the preceding steps, Step 3 is the major process of the
|
||||||
|
algorithm. The flowchart is illustrated on the right side of Fig. 3,
|
||||||
|
and the details are given below.
|
||||||
|
|
||||||
|
Step 3.1: Schedule the M aircraft by the FCFS approach and
|
||||||
|
calculate the fitness value through (3). Calculate
|
||||||
|
the initial pheromone τ0 and set the pheromone for
|
||||||
|
each aircraft pair as τ0.
|
||||||
|
|
||||||
|
Step 3.2: For each ant, do the following.
|
||||||
|
|
||||||
|
a) Determine the first landing aircraft s and construct the whole landing sequence using the state transition rule as (5) and (6).
|
||||||
|
|
||||||
|
b) Perform the local pheromone updating as (9).
|
||||||
|
|
||||||
|
Step 3.3: Calculate the fitness of each ant and determine
|
||||||
|
the best solution. Moreover, the current best solution is compared with the historically best solution
|
||||||
|
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)
|
||||||
|
|||||||
97
aman/AMAN.py
Normal file
97
aman/AMAN.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from aman.com import AircraftReport_pb2
|
||||||
|
from aman.com.Euroscope import Euroscope
|
||||||
|
from aman.com.Weather import Weather
|
||||||
|
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.weather = None
|
||||||
|
self.workers = []
|
||||||
|
self.inbounds = {}
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.release()
|
||||||
|
|
||||||
|
def aquire(self):
|
||||||
|
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')
|
||||||
|
|
||||||
|
self.weather = Weather()
|
||||||
|
self.weather.acquire(self.systemConfig.Weather)
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
worker.acquire(icao, airportConfig)
|
||||||
|
self.workers.append(worker)
|
||||||
|
print('Started worker for ' + icao)
|
||||||
|
|
||||||
|
# create the EuroScope receiver
|
||||||
|
self.receiver = Euroscope()
|
||||||
|
self.receiver.acquire(configPath, self.systemConfig.Server, self)
|
||||||
|
|
||||||
|
def release(self):
|
||||||
|
if None != self.receiver:
|
||||||
|
self.receiver.release()
|
||||||
|
self.receiver = None
|
||||||
|
|
||||||
|
if None != self.weather:
|
||||||
|
self.weather.release()
|
||||||
|
self.weather = None
|
||||||
|
|
||||||
|
if None != self.workers:
|
||||||
|
for worker in self.workers:
|
||||||
|
worker.release()
|
||||||
|
self.workers = None
|
||||||
|
|
||||||
|
def updateAircraftReport(self, report : AircraftReport_pb2.AircraftReport):
|
||||||
|
# find the correct worker for the inbound
|
||||||
|
for worker in self.workers:
|
||||||
|
if worker.icao == report.destination:
|
||||||
|
worker.acquireLock()
|
||||||
|
worker.reportQueue[report.aircraft.callsign] = report
|
||||||
|
worker.releaseLock()
|
||||||
|
break
|
||||||
0
aman/__init__.py
Normal file
0
aman/__init__.py
Normal file
157
aman/com/DwdCrawler.py
Normal file
157
aman/com/DwdCrawler.py
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from datetime import datetime as dt
|
||||||
|
|
||||||
|
# @brief Checks the DWD pages for wind information
|
||||||
|
# Format:
|
||||||
|
# Provides next update tine (updateTime) of the DWD page in UTC
|
||||||
|
# Provides a list of wind information (windData)
|
||||||
|
# - organized as a list of tuples
|
||||||
|
# - first element of tuple: GAFOR-IDs for the following wind information
|
||||||
|
# - second element of tuple: list of tuples of wind data
|
||||||
|
# - first element of wind data tuple: minimum altitude AMSL for this wind information
|
||||||
|
# - second element of wind data tuple: wind direction
|
||||||
|
# - third element of wind data tuple: wind speed (KT)
|
||||||
|
class DwdCrawler():
|
||||||
|
def __init__(self):
|
||||||
|
self.updateTime = None
|
||||||
|
self.windData = None
|
||||||
|
|
||||||
|
def parseGaforAreas(areas : str):
|
||||||
|
areas = areas.replace(':', '')
|
||||||
|
areas = areas.split(' ')[1]
|
||||||
|
areaIds = []
|
||||||
|
|
||||||
|
# some IDs are lists
|
||||||
|
for segment in areas.split(','):
|
||||||
|
# check if we have range definitions or single IDs
|
||||||
|
borders = segment.split('-')
|
||||||
|
if 2 == len(borders):
|
||||||
|
areaIds.extend(range(int(borders[0]), int(borders[1]) + 1))
|
||||||
|
else:
|
||||||
|
areaIds.append(int(borders[0]))
|
||||||
|
|
||||||
|
return areaIds
|
||||||
|
|
||||||
|
def parseWindTableRow(row : str, table):
|
||||||
|
# get the columns
|
||||||
|
entries = row.split('|')
|
||||||
|
|
||||||
|
# check if the line is invalid or we have the header
|
||||||
|
if 2 > len(entries) or 'AMSL' in entries[0]:
|
||||||
|
return table
|
||||||
|
|
||||||
|
# parse the wind data
|
||||||
|
windData = entries[1].strip().split(' ')[0].split('/')
|
||||||
|
if 2 != len(windData):
|
||||||
|
return table
|
||||||
|
|
||||||
|
# extend the table
|
||||||
|
altitude = entries[0].strip()
|
||||||
|
if 'FL' in altitude:
|
||||||
|
altitude = int(altitude.replace('FL', '')) * 100
|
||||||
|
else:
|
||||||
|
altitude = int(altitude.replace('FT', ''))
|
||||||
|
row = ( altitude, int(windData[0]), int(windData[1].replace('KT', '')) )
|
||||||
|
table.append(row)
|
||||||
|
|
||||||
|
return table
|
||||||
|
|
||||||
|
def parseNextUpdateTime(line : str):
|
||||||
|
entries = line.split(' ')
|
||||||
|
if 4 <= len(entries):
|
||||||
|
utcIndex = 2
|
||||||
|
if 'UTC' in entries[len(entries) - 2]:
|
||||||
|
utcIndex = len(entries) - 3
|
||||||
|
elif 'UTC' in entries[len(entries) - 1]:
|
||||||
|
utcIndex = len(entries - 2)
|
||||||
|
|
||||||
|
currentUtc = dt.utcfromtimestamp(int(time.time()))
|
||||||
|
currentHour = int(currentUtc.strftime('%H'))
|
||||||
|
|
||||||
|
# check if we have a day overlap
|
||||||
|
if currentHour > int(entries[utcIndex].split('.')[0]):
|
||||||
|
nextDay = currentUtc + datetime.timedelta(days=1)
|
||||||
|
date = nextDay.strftime('%Y-%m-%d')
|
||||||
|
else:
|
||||||
|
date = currentUtc.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# create the new UTC update time
|
||||||
|
return dt.strptime(date + ' ' + entries[utcIndex] + '+0000', '%Y-%m-%d %H.%M%z')
|
||||||
|
|
||||||
|
def parseGaforPage(self, url : str):
|
||||||
|
with urllib.request.urlopen(url) as site:
|
||||||
|
data = site.read().decode('utf-8')
|
||||||
|
site.close()
|
||||||
|
|
||||||
|
parsed = BeautifulSoup(data, features='lxml')
|
||||||
|
|
||||||
|
# search the info about the GAFOR areas
|
||||||
|
content = None
|
||||||
|
for element in parsed.body.find_all('pre'):
|
||||||
|
content = element.text
|
||||||
|
|
||||||
|
# analyze the received data
|
||||||
|
if None != content:
|
||||||
|
windInformation = []
|
||||||
|
nextUpdate = None
|
||||||
|
windTable = []
|
||||||
|
areaIds = None
|
||||||
|
|
||||||
|
# find all relevant information
|
||||||
|
for line in content.splitlines():
|
||||||
|
if '' == line:
|
||||||
|
if 0 != len(windTable):
|
||||||
|
windInformation.append(( areaIds, windTable ))
|
||||||
|
areaIds = None
|
||||||
|
windTable = []
|
||||||
|
elif line.startswith('GAFOR-Gebiete'):
|
||||||
|
areaIds = DwdCrawler.parseGaforAreas(line)
|
||||||
|
windTable = []
|
||||||
|
elif None != areaIds:
|
||||||
|
windTable = DwdCrawler.parseWindTableRow(line, windTable)
|
||||||
|
elif 'Aktualisierung erfolgt um ' in line:
|
||||||
|
nextUpdate = DwdCrawler.parseNextUpdateTime(line)
|
||||||
|
|
||||||
|
# return the collected information
|
||||||
|
if 0 == len(windInformation) or None == nextUpdate:
|
||||||
|
return None, None
|
||||||
|
else:
|
||||||
|
return nextUpdate, windInformation
|
||||||
|
|
||||||
|
def receiveWindData(self):
|
||||||
|
self.updateTime = None
|
||||||
|
self.windData = None
|
||||||
|
|
||||||
|
with urllib.request.urlopen('https://www.dwd.de/DE/fachnutzer/luftfahrt/teaser/luftsportberichte/luftsportberichte_node.html') as site:
|
||||||
|
data = site.read().decode('utf-8')
|
||||||
|
site.close()
|
||||||
|
|
||||||
|
# find the pages of the GAFOR reports
|
||||||
|
pages = []
|
||||||
|
parsed = BeautifulSoup(data, features='lxml')
|
||||||
|
for link in parsed.body.find_all('a', title=True):
|
||||||
|
if 'node' in link['href'] and 'Flugwetterprognose' in link['title']:
|
||||||
|
# remove the jsession from the link
|
||||||
|
pages.append('https://www.dwd.de/' + link['href'].split(';')[0])
|
||||||
|
|
||||||
|
# receive the wind data
|
||||||
|
self.updateTime = None
|
||||||
|
self.windData = []
|
||||||
|
for page in pages:
|
||||||
|
next, wind = self.parseGaforPage(page)
|
||||||
|
if None != next:
|
||||||
|
if None == self.updateTime or self.updateTime > next:
|
||||||
|
self.updateTime = next
|
||||||
|
self.windData.extend(wind)
|
||||||
|
|
||||||
|
# indicate that new wind data is available
|
||||||
|
if None != self.updateTime:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
121
aman/com/Euroscope.py
Normal file
121
aman/com/Euroscope.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import zmq
|
||||||
|
import zmq.auth
|
||||||
|
|
||||||
|
from aman.com import AircraftReport_pb2
|
||||||
|
from aman.config.Server import Server
|
||||||
|
from threading import Thread, _active
|
||||||
|
|
||||||
|
class ReceiverThread(Thread):
|
||||||
|
def __init__(self, socket, aman):
|
||||||
|
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 _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:
|
||||||
|
def __init__(self):
|
||||||
|
self.context = None
|
||||||
|
self.receiverSocket = None
|
||||||
|
self.receiverThread = None
|
||||||
|
self.notificationSocket = None
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.release()
|
||||||
|
|
||||||
|
# @brief Initializes the ZMQ socket
|
||||||
|
# @param[in] config The server configuration
|
||||||
|
def acquire(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 to 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 to tcp://' + config.Address + ':' + str(config.PortNotification))
|
||||||
|
|
||||||
|
def release(self):
|
||||||
|
if None != self.receiverThread:
|
||||||
|
self.receiverThread.stopThread()
|
||||||
|
self.receiverThread.join()
|
||||||
|
self.receiverThread = None
|
||||||
|
|
||||||
|
if None != self.receiverSocket:
|
||||||
|
self.receiverSocket.close()
|
||||||
|
self.receiverSocket = None
|
||||||
|
|
||||||
|
if None != self.notificationSocket:
|
||||||
|
self.notificationSocket.close()
|
||||||
|
self.notificationSocket = None
|
||||||
56
aman/com/Weather.py
Normal file
56
aman/com/Weather.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from datetime import datetime as dt
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from aman.com.DwdCrawler import DwdCrawler
|
||||||
|
import aman.config.Weather
|
||||||
|
|
||||||
|
class Weather(Thread):
|
||||||
|
def __init__(self):
|
||||||
|
Thread.__init__(self)
|
||||||
|
|
||||||
|
self.nextUpdate = None
|
||||||
|
self.lastUpdateTried = None
|
||||||
|
self.stopThread = False
|
||||||
|
self.provider = None
|
||||||
|
|
||||||
|
def acquire(self, config : aman.config.Weather.Weather):
|
||||||
|
self.nextUpdate = dt.utcfromtimestamp(int(time.time()))
|
||||||
|
self.lastUpdateTried = None
|
||||||
|
self.stopThread = False
|
||||||
|
self.provider = None
|
||||||
|
|
||||||
|
if 'DWD' == config.Provider.upper():
|
||||||
|
self.provider = DwdCrawler()
|
||||||
|
else:
|
||||||
|
sys.stderr.write('Invalid or unknown weather-provider defined')
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def release(self):
|
||||||
|
self.stopThread = True
|
||||||
|
self.join()
|
||||||
|
|
||||||
|
def currentClock():
|
||||||
|
clock = dt.utcfromtimestamp(int(time.time())).replace(tzinfo = pytz.UTC)
|
||||||
|
return clock
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while False == self.stopThread and None != self.provider:
|
||||||
|
now = Weather.currentClock()
|
||||||
|
|
||||||
|
# check if an update is required
|
||||||
|
if None != self.provider.updateTime and self.provider.updateTime > now:
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if None == self.lastUpdateTried or self.lastUpdateTried <= now:
|
||||||
|
if True == self.provider.receiveWindData():
|
||||||
|
self.nextUpdate = self.provider.updateTime
|
||||||
|
print('Received new wind data')
|
||||||
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)
|
||||||
30
aman/config/System.py
Normal file
30
aman/config/System.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from aman.config.Server import Server
|
||||||
|
from aman.config.Weather import Weather
|
||||||
|
|
||||||
|
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
|
||||||
|
elif 'WEATHER' == key:
|
||||||
|
weatherSectionAvailable = True
|
||||||
|
|
||||||
|
if not serverSectionAvailable:
|
||||||
|
sys.stderr.write('No server-configuration section found!')
|
||||||
|
sys.exit(-1)
|
||||||
|
if not weatherSectionAvailable:
|
||||||
|
sys.stderr.write('No weather-configuration section found!')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
self.Server = Server(config['SERVER'])
|
||||||
|
self.Weather = Weather(config['WEATHER'])
|
||||||
19
aman/config/Weather.py
Normal file
19
aman/config/Weather.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import configparser;
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class Weather():
|
||||||
|
def __init__(self, config : configparser.ConfigParser):
|
||||||
|
self.Provider = None
|
||||||
|
self.PortReceiver = None
|
||||||
|
self.PortNotification = None
|
||||||
|
|
||||||
|
# search the required sections
|
||||||
|
for key in config:
|
||||||
|
if 'provider' == key:
|
||||||
|
self.Provider = config['provider']
|
||||||
|
|
||||||
|
if self.Provider is None:
|
||||||
|
sys.stderr.write('No weather-provider configuration found!')
|
||||||
|
sys.exit(-1)
|
||||||
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
148
aman/sys/WeatherModel.py
Normal file
148
aman/sys/WeatherModel.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from aman.com.Weather import Weather
|
||||||
|
|
||||||
|
import math
|
||||||
|
import scipy.interpolate
|
||||||
|
|
||||||
|
class WeatherModel:
|
||||||
|
def __init__(self, gaforId, weather : Weather):
|
||||||
|
self.gafor = gaforId
|
||||||
|
self.weather = weather
|
||||||
|
self.windDirectionModel = None
|
||||||
|
self.windSpeedModel = None
|
||||||
|
self.lastWeatherUpdate = None
|
||||||
|
self.minimumAltitude = 1000000
|
||||||
|
self.maximumAltitude = -1
|
||||||
|
|
||||||
|
# create the density interpolation model
|
||||||
|
# the density model is based on https://aerotoolbox.com/atmcalc/
|
||||||
|
altitudes = [
|
||||||
|
50000,
|
||||||
|
45000,
|
||||||
|
40000,
|
||||||
|
38000,
|
||||||
|
36000,
|
||||||
|
34000,
|
||||||
|
32000,
|
||||||
|
30000,
|
||||||
|
28000,
|
||||||
|
26000,
|
||||||
|
24000,
|
||||||
|
22000,
|
||||||
|
20000,
|
||||||
|
18000,
|
||||||
|
16000,
|
||||||
|
15000,
|
||||||
|
14000,
|
||||||
|
13000,
|
||||||
|
12000,
|
||||||
|
11000,
|
||||||
|
10000,
|
||||||
|
9000,
|
||||||
|
8000,
|
||||||
|
7000,
|
||||||
|
6000,
|
||||||
|
5000,
|
||||||
|
4000,
|
||||||
|
3000,
|
||||||
|
2000,
|
||||||
|
1000,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
densities = [
|
||||||
|
0.18648,
|
||||||
|
0.23714,
|
||||||
|
0.24617,
|
||||||
|
0.33199,
|
||||||
|
0.36518,
|
||||||
|
0.39444,
|
||||||
|
0.42546,
|
||||||
|
0.45831,
|
||||||
|
0.402506,
|
||||||
|
0.432497,
|
||||||
|
0.464169,
|
||||||
|
0.60954,
|
||||||
|
0.65269,
|
||||||
|
0.69815,
|
||||||
|
0.74598,
|
||||||
|
0.77082,
|
||||||
|
0.79628,
|
||||||
|
0.82238,
|
||||||
|
0.84914,
|
||||||
|
0.87655,
|
||||||
|
0.90464,
|
||||||
|
0.93341,
|
||||||
|
0.96287,
|
||||||
|
0.99304,
|
||||||
|
1.02393,
|
||||||
|
1.05555,
|
||||||
|
1.08791,
|
||||||
|
1.12102,
|
||||||
|
1.1549,
|
||||||
|
1.18955,
|
||||||
|
1.225
|
||||||
|
]
|
||||||
|
self.densityModel = scipy.interpolate.interp1d(altitudes, densities)
|
||||||
|
|
||||||
|
def calculateTAS(self, altitude : int, ias : int):
|
||||||
|
if altitude >= 50000:
|
||||||
|
altitude = 49999
|
||||||
|
if altitude <= 0:
|
||||||
|
altitude = 1
|
||||||
|
|
||||||
|
# calculation based on https://aerotoolbox.com/airspeed-conversions/
|
||||||
|
return ias * math.sqrt(1.225 / self.densityModel(altitude).item())
|
||||||
|
|
||||||
|
def updateWindModel(self):
|
||||||
|
if None == self.lastWeatherUpdate or self.lastWeatherUpdate != self.weather.provider.updateTime:
|
||||||
|
self.lastWeatherUpdate = self.weather.provider.updateTime
|
||||||
|
|
||||||
|
self.minimumAltitude = 1000000
|
||||||
|
self.maximumAltitude = -1
|
||||||
|
self.windDirectionModel = None
|
||||||
|
self.windSpeedModel = None
|
||||||
|
|
||||||
|
if None != self.weather.provider.windData and self.gafor in self.weather.provider.windData:
|
||||||
|
altitudes = []
|
||||||
|
directions = []
|
||||||
|
speeds = []
|
||||||
|
|
||||||
|
# collect the data for the wind model
|
||||||
|
for level in self.weather.provider.windData[self.gafor]:
|
||||||
|
altitudes.append(level[0])
|
||||||
|
directions.append(level[1])
|
||||||
|
speeds.append(level[2])
|
||||||
|
|
||||||
|
# define the thresholds for later boundary checks
|
||||||
|
if self.minimumAltitude > level[0]:
|
||||||
|
self.minimumAltitude = level[0]
|
||||||
|
if self.maximumAltitude < level[0]:
|
||||||
|
self.maximumAltitude = level[0]
|
||||||
|
|
||||||
|
# calculate the models
|
||||||
|
if 1 < len(altitudes):
|
||||||
|
self.windDirectionModel = scipy.interpolate.interp1d(altitudes, directions)
|
||||||
|
self.windSpeedModel = scipy.interpolate.interp1d(altitudes, speeds)
|
||||||
|
|
||||||
|
def calculateGS(self, altitude : int, ias : int, heading : int):
|
||||||
|
self.updateWindModel()
|
||||||
|
tas = self.calculateTAS(altitude, ias)
|
||||||
|
|
||||||
|
# initialize the wind data
|
||||||
|
if None != self.windDirectionModel and None != self.windSpeedModel:
|
||||||
|
direction = 0.0
|
||||||
|
speed = 0.0
|
||||||
|
if None != self.windSpeedModel and None != self.windDirectionModel:
|
||||||
|
if self.maximumAltitude <= altitude:
|
||||||
|
altitude = self.maximumAltitude - 1
|
||||||
|
if self.minimumAltitude >= altitude:
|
||||||
|
altitude = self.minimumAltitude + 1
|
||||||
|
direction = self.windDirectionModel(altitude).item()
|
||||||
|
speed = self.windSpeedModel(altitude).item()
|
||||||
|
else:
|
||||||
|
speed = 0
|
||||||
|
direction = 0
|
||||||
|
|
||||||
|
# calculate the ground speed based on the headwind component
|
||||||
|
return tas + speed * math.cos(math.radians(direction) - math.radians(heading))
|
||||||
53
aman/sys/Worker.py
Normal file
53
aman/sys/Worker.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from threading import Thread, Lock
|
||||||
|
import time
|
||||||
|
|
||||||
|
from aman.config.Airport import Airport
|
||||||
|
|
||||||
|
class Worker(Thread):
|
||||||
|
def __init__(self):
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.stopThread = None
|
||||||
|
self.icao = None
|
||||||
|
self.configuration = None
|
||||||
|
self.arrivalRoutes = None
|
||||||
|
self.updateLock = None
|
||||||
|
self.reportQueue = {}
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.release()
|
||||||
|
|
||||||
|
def acquire(self, icao : str, configuration : Airport):
|
||||||
|
self.stopThread = None
|
||||||
|
self.icao = icao
|
||||||
|
self.configuration = configuration
|
||||||
|
self.arrivalRoutes = configuration.gngData.arrivalRoutes
|
||||||
|
self.updateLock = Lock()
|
||||||
|
self.reportQueue = {}
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def acquireLock(self):
|
||||||
|
if None != self.updateLock:
|
||||||
|
self.updateLock.acquire()
|
||||||
|
|
||||||
|
def release(self):
|
||||||
|
self.stopThread = True
|
||||||
|
self.join()
|
||||||
|
|
||||||
|
def releaseLock(self):
|
||||||
|
if None != self.updateLock:
|
||||||
|
self.updateLock.release()
|
||||||
|
|
||||||
|
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.18.1
vendored
Normal file
32
external/licenses/ProtoBuf-3.18.1
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.
|
||||||
22
icao/recat.py
Normal file
22
icao/recat.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Recat departure separation in seconds
|
||||||
|
# x = CAT A -> CAT F
|
||||||
|
# y = CAT A -> CAT F
|
||||||
|
# https://www.skybrary.aero/index.php/RECAT_-_Wake_Turbulence_Re-categorisation
|
||||||
|
recatDeparture = [
|
||||||
|
[0, 100, 120, 140, 160, 180],
|
||||||
|
[0, 0, 0, 100, 120, 140],
|
||||||
|
[0, 0, 0, 80, 100, 120],
|
||||||
|
[0, 0, 0, 0, 0, 120],
|
||||||
|
[0, 0, 0, 0, 0, 100],
|
||||||
|
[0, 0, 0, 0, 0, 80],
|
||||||
|
]
|
||||||
|
|
||||||
|
#Recat Arrival in NM
|
||||||
|
recatArrival = [
|
||||||
|
[3, 4, 5, 5, 6, 8],
|
||||||
|
[0, 3, 4, 4, 5, 7],
|
||||||
|
[0, 0, 3, 3, 4, 6],
|
||||||
|
[0, 0, 0, 0, 0, 5],
|
||||||
|
[0, 0, 0, 0, 0, 4],
|
||||||
|
[0, 0, 0, 0, 0, 3],
|
||||||
|
]
|
||||||
10
main.py
Normal file
10
main.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from tcp.TCPServer import TCPServer
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
server = TCPServer()
|
||||||
|
server.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
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 0c5ed87078
Reference in New Issue
Block a user