Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
NommonSolutionsAndTechnologies committed Dec 3, 2021
2 parents 2f4b09d + 8382e60 commit f7af4fa
Show file tree
Hide file tree
Showing 75 changed files with 2,890 additions and 1,847 deletions.
10 changes: 7 additions & 3 deletions bluesky/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from bluesky import settings
from bluesky.core import Signal
from bluesky import stack
from bluesky import tools


# Constants
Expand Down Expand Up @@ -35,9 +36,6 @@ def init(mode='sim', pygame=False, discovery=False, cfgfile='', scnfile=''):
- pygame: indicate if BlueSky is started with BlueSky_pygame.py
- discovery: Enable network discovery
'''
# Initialize global settings first, possibly loading a custom config file
settings.init(cfgfile)

# Is this a server running headless?
headless = (mode[-8:] == 'headless')

Expand All @@ -46,6 +44,12 @@ def init(mode='sim', pygame=False, discovery=False, cfgfile='', scnfile=''):
gui_type = 'pygame' if pygame else \
'none' if headless or mode[:3] == 'sim' else 'qtgl'

# Initialize global settings first, possibly loading a custom config file
settings.init(cfgfile)

# Initialise tools
tools.init()

# Load navdatabase in all versions of BlueSky
# Only the headless server doesn't need this
if not headless:
Expand Down
5 changes: 3 additions & 2 deletions bluesky/core/plugin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
""" Implementation of BlueSky's plugin system. """
import ast
import glob

from bluesky.stack.stackbase import sender
from os import path
from pathlib import Path
import sys
import imp
import bluesky as bs
Expand Down Expand Up @@ -90,7 +91,7 @@ def load(cls, name):
@classmethod
def find_plugins(cls, reqtype):
''' Create plugin wrapper objects based on source code of potential plug-in files. '''
for fname in Path(settings.plugin_path).rglob('*.py'):
for fname in glob.glob('{}/**/*.py'.format(settings.plugin_path), recursive=True):
with open(fname, 'rb') as f:
source = f.read()
try:
Expand Down
48 changes: 45 additions & 3 deletions bluesky/network/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@


class Client:
''' Base class for (GUI) clients of a BlueSky server. '''
def __init__(self, actnode_topics=b''):
ctx = zmq.Context.instance()
self.event_io = ctx.socket(zmq.DEALER)
Expand Down Expand Up @@ -38,20 +39,24 @@ def __init__(self, actnode_topics=b''):
bluesky.scr = self

def start_discovery(self):
''' Start UDP-based discovery of available BlueSky servers. '''
if not self.discovery:
self.discovery = Discovery(self.client_id)
self.poller.register(self.discovery.handle, zmq.POLLIN)
self.discovery.send_request()

def stop_discovery(self):
''' Stop UDP-based discovery. '''
if self.discovery:
self.poller.unregister(self.discovery.handle)
self.discovery = None

def get_hostid(self):
''' Return the id of the host that this client is connected to. '''
return self.host_id

def sender(self):
''' Return the id of the sender of the most recent event. '''
return self.sender_id

def event(self, name, data, sender_id):
Expand All @@ -69,15 +74,42 @@ def actnode_changed(self, newact):
to implement actual actnode change handling. '''
print('Client active node changed.')

def subscribe(self, streamname, node_id=b''):
''' Subscribe to a stream. '''
def subscribe(self, streamname, node_id=b'', actonly=False):
''' Subscribe to a stream.
Arguments:
- streamname: The name of the stream to subscribe to
- node_id: The id of the node from which to receive the stream (optional)
- actonly: Set to true if you only want to receive this stream from
the active node.
'''
if actonly and not node_id and streamname not in self.acttopics:
self.acttopics.append(streamname)
node_id = self.act
self.stream_in.setsockopt(zmq.SUBSCRIBE, streamname + node_id)

def unsubscribe(self, streamname, node_id=b''):
''' Unsubscribe from a stream. '''
''' Unsubscribe from a stream.
Arguments:
- streamname: The name of the stream to unsubscribe from.
- node_id: ID of the specific node to unsubscribe from.
This is also used when switching active nodes.
'''
if not node_id and streamname in self.acttopics:
self.acttopics.remove(streamname)
node_id = self.act
self.stream_in.setsockopt(zmq.UNSUBSCRIBE, streamname + node_id)

def connect(self, hostname='localhost', event_port=0, stream_port=0, protocol='tcp'):
''' Connect client to a server.
Arguments:
- hostname: Network name or ip of the server to connect to
- event_port: Network port to use for event communication
- stream_port: Network port to use for stream communication
- protocol: Network protocol to use
'''
conbase = '{}://{}'.format(protocol, hostname)
econ = conbase + (':{}'.format(event_port) if event_port else '')
scon = conbase + (':{}'.format(stream_port) if stream_port else '')
Expand Down Expand Up @@ -160,6 +192,7 @@ def _getroute(self, target):
return None

def actnode(self, newact=None):
''' Set the new active node, or return the current active node. '''
if newact:
route = self._getroute(newact)
if route is None:
Expand All @@ -178,9 +211,18 @@ def actnode(self, newact=None):
return self.act

def addnodes(self, count=1):
''' Tell the server to add 'count' nodes. '''
self.send_event(b'ADDNODES', count)

def send_event(self, name, data=None, target=None):
''' Send an event to one or all simulation node(s).
Arguments:
- name: Name of the event
- data: Data to send as payload
- target: Destination of this event. Event is sent to all nodes
if * is specified as target.
'''
pydata = msgpack.packb(data, default=encode_ndarray, use_bin_type=True)
if not target:
self.event_io.send_multipart(self.actroute + [self.act, name, pydata])
Expand Down
28 changes: 28 additions & 0 deletions bluesky/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import shutil
import site
import inspect
from pathlib import Path


def init(cfgfile=''):
Expand All @@ -30,11 +31,19 @@ def init(cfgfile=''):
root_dirs += site.getsitepackages()

# search for bluesky shared data directory
found_dir = False
for root_dir in root_dirs:
dirpath = os.path.join(root_dir, 'share', 'bluesky')
if os.path.exists(dirpath):
srcdir = dirpath
found_dir = True
break

# if the path does not exist, it's worth trying the project root. This
# would work if the package was cloned from the git and is installed
# with "pip install -e ."
if not found_dir:
srcdir = get_project_root()

datadir = os.path.join(rundir, 'data')
cachedir = os.path.join(rundir, 'data/cache')
Expand Down Expand Up @@ -99,6 +108,18 @@ def init(cfgfile=''):

exec(compile(open(cfgfile).read(), cfgfile, 'exec'), globals())

# Use the path specified in cfgfile if available
if 'cache_path' in globals():
cachedir = globals()['cache_path']
if 'log_path' in globals():
outdir = globals()['log_path']
if 'perf_path_bada' in globals():
badadir = globals()['perf_path_bada']
if 'scenario_path' in globals():
scndir = globals()['scenario_path']
if 'plugin_path' in globals():
plgdir = globals()['plugin_path']

# Update cachedir with python version-specific subfolder
cachedir = os.path.join(cachedir, 'py%d' % sys.version_info[0])
globals()['cache_path'] = cachedir
Expand Down Expand Up @@ -196,3 +217,10 @@ def save(fname=None, changes=None):
file_out.write(f'{key} = {value}\n')

return True, f'Saved settings to {fname}'

def get_project_root() -> str:
''' Return the absolute path of the project root. '''

# return root dir relative to this file, make sure you update it if this
# file is moved in the project directory
return str(Path(__file__).absolute().parent.parent)
78 changes: 55 additions & 23 deletions bluesky/simulation/screenio.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ def __init__(self):
# Screen state defaults
self.def_pan = (0.0, 0.0)
self.def_zoom = 1.0
self.route_all = ""

# Screen state overrides per client
self.client_pan = dict()
self.client_zoom = dict()
self.client_ar = dict()
self.route_acid = dict()
self.client_route = dict()

# Dicts of custom aircraft and group colors
self.custacclr = dict()
Expand All @@ -58,12 +60,20 @@ def step(self):
self.samplecount += 1

def reset(self):
self.client_pan = dict()
self.client_zoom = dict()
self.client_ar = dict()
self.client_route = dict()
self.route_all = ''
self.custacclr = dict()
self.custgrclr = dict()
self.samplecount = 0
self.prevcount = 0
self.prevtime = 0.0

self.def_pan = (0.0, 0.0)
self.def_zoom = 1.0

# Communicate reset to gui
bs.net.send_event(b'RESET', b'ALL')

Expand Down Expand Up @@ -164,7 +174,11 @@ def trails(self,sw):

def showroute(self, acid):
''' Toggle show route for this aircraft '''
self.route_acid[stack.sender()] = acid
if not stack.sender():
self.route_all = acid
self.client_route.clear()
else:
self.client_route[stack.sender()] = acid
return True

def addnavwpt(self, name, lat, lon):
Expand Down Expand Up @@ -260,24 +274,42 @@ def send_aircraft_data(self):
bs.net.send_stream(b'ACDATA', data)

def send_route_data(self):
for sender, acid in self.route_acid.items():
data = dict()
data['acid'] = acid
idx = bs.traf.id2idx(acid)
if idx >= 0:
route = bs.traf.ap.route[idx]
data['iactwp'] = route.iactwp

# We also need the corresponding aircraft position
data['aclat'] = bs.traf.lat[idx]
data['aclon'] = bs.traf.lon[idx]

data['wplat'] = route.wplat
data['wplon'] = route.wplon

data['wpalt'] = route.wpalt
data['wpspd'] = route.wpspd

data['wpname'] = route.wpname

bs.net.send_stream(b'ROUTEDATA' + (sender or b'*'), data) # Send route data to GUI
''' Send route data to client(s) '''
# print(self.client_route, self.route_all)
# Case 1: A route is selected by one or more specific clients
if self.client_route:
for sender, acid in self.client_route.items():
_sendrte(sender, acid)
# Check if there are other senders and also a scenario-selected route
if self.route_all:
remclients = bs.sim.clients.difference(self.client_route.keys())
#print(bs.sim.clients, remclients)
for sender in remclients:
_sendrte(sender, self.route_all)
# Case 2: only a route selected from scenario file:
# Broadcast the same route to everyone
elif self.route_all:
_sendrte(b'*', self.route_all)

def _sendrte(sender, acid):
''' Local shorthand function to send route. '''
data = dict()
data['acid'] = acid
idx = bs.traf.id2idx(acid)
if idx >= 0:
route = bs.traf.ap.route[idx]
data['iactwp'] = route.iactwp

# We also need the corresponding aircraft position
data['aclat'] = bs.traf.lat[idx]
data['aclon'] = bs.traf.lon[idx]

data['wplat'] = route.wplat
data['wplon'] = route.wplon

data['wpalt'] = route.wpalt
data['wpspd'] = route.wpspd

data['wpname'] = route.wpname

bs.net.send_stream(b'ROUTEDATA' + (sender or b'*'), data) # Send route data to GUI
5 changes: 5 additions & 0 deletions bluesky/simulation/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def __init__(self):
# Flag indicating whether timestep can be varied to ensure realtime op
self.rtmode = False

# Keep track of known clients
self.clients = set()

def step(self):
''' Perform a simulation timestep. '''
# Simulation starts as soon as there is traffic, or pending commands
Expand Down Expand Up @@ -218,6 +221,8 @@ def event(self, eventname, eventdata, sender_rte):
event_processed = True

elif eventname == b'GETSIMSTATE':
# Add this client to the list of known clients
self.clients.add(sender_rte[-1])
# Send list of stack functions available in this sim to gui at start
stackdict = {cmd : val.brief[len(cmd) + 1:] for cmd, val in bs.stack.get_commands().items()}
shapes = [shape.raw for shape in areafilter.basic_shapes.values()]
Expand Down
9 changes: 4 additions & 5 deletions bluesky/stack/argparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# re_getarg = re.compile(r'[\'"]?((?<=[\'"])[^\'"]+|(?<![\'"])[^\s,]+)[\'"]?\s*,?\s*')

# Stack reference data namespace
refdata = SimpleNamespace(lat=None, lon=None, alt=None, ac=-1, hdg=None, cas=None)
refdata = SimpleNamespace(lat=None, lon=None, alt=None, acidx=-1, hdg=None, cas=None)


def getnextarg(cmdstring):
Expand Down Expand Up @@ -162,10 +162,9 @@ class WpinrouteArg(Parser):
def parse(self, argstring):
arg, argstring = re_getarg.match(argstring).groups()
wpname = arg.upper()
if refdata.acidx >= 0 and wpname not in bs.traf.ap.route[refdata.acidx].wpname:
raise ArgumentError(f'{wpname} not found in the route of {bs.traf.id[refdata.acidx]}')
return wpname, argstring

if refdata.acidx >= 0 and wpname in bs.traf.ap.route[refdata.acidx].wpname or wpname == '*':
return wpname, argstring
raise ArgumentError(f'{wpname} not found in the route of {bs.traf.id[refdata.acidx]}')

class WptArg(Parser):
''' Argument parser for waypoints.
Expand Down
Loading

0 comments on commit f7af4fa

Please sign in to comment.