changes to move sdt calls internal to core interactions, which allows it to work with both guis

This commit is contained in:
Blake Harnden 2020-02-27 21:39:18 -08:00
parent 20e3fbc7d9
commit 67da3e5c22
3 changed files with 156 additions and 295 deletions

View file

@ -526,11 +526,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
logging.debug( logging.debug(
"%s handling message:\n%s", threading.currentThread().getName(), message "%s handling message:\n%s", threading.currentThread().getName(), message
) )
# provide to sdt, if enabled
if self.session and self.session.sdt.is_enabled():
self.session.sdt.handle_distributed(message)
if message.message_type not in self.message_handlers: if message.message_type not in self.message_handlers:
logging.error("no handler for message type: %s", message.type_str()) logging.error("no handler for message type: %s", message.type_str())
return return
@ -2042,7 +2037,6 @@ class CoreUdpHandler(CoreHandler):
logging.debug("session handling message: %s", session.session_id) logging.debug("session handling message: %s", session.session_id)
self.session = session self.session = session
self.handle_message(message) self.handle_message(message)
self.session.sdt.handle_distributed(message)
self.broadcast(message) self.broadcast(message)
else: else:
logging.error( logging.error(
@ -2067,7 +2061,6 @@ class CoreUdpHandler(CoreHandler):
if session or message.message_type == MessageTypes.REGISTER.value: if session or message.message_type == MessageTypes.REGISTER.value:
self.session = session self.session = session
self.handle_message(message) self.handle_message(message)
self.session.sdt.handle_distributed(message)
self.broadcast(message) self.broadcast(message)
else: else:
logging.error( logging.error(

View file

@ -432,6 +432,7 @@ class Session:
if node_two: if node_two:
node_two.lock.release() node_two.lock.release()
self.sdt.add_link(node_one_id, node_two_id, is_wireless=False)
return node_one_interface, node_two_interface return node_one_interface, node_two_interface
def delete_link( def delete_link(
@ -540,6 +541,8 @@ class Session:
if node_two: if node_two:
node_two.lock.release() node_two.lock.release()
self.sdt.delete_link(node_one_id, node_two_id)
def update_link( def update_link(
self, self,
node_one_id: int, node_one_id: int,
@ -757,6 +760,7 @@ class Session:
self.add_remove_control_interface(node=node, remove=False) self.add_remove_control_interface(node=node, remove=False)
self.services.boot_services(node) self.services.boot_services(node)
self.sdt.add_node(node)
return node return node
def edit_node(self, node_id: int, options: NodeOptions) -> None: def edit_node(self, node_id: int, options: NodeOptions) -> None:
@ -765,7 +769,7 @@ class Session:
:param node_id: id of node to update :param node_id: id of node to update
:param options: data to update node with :param options: data to update node with
:return: True if node updated, False otherwise :return: nothing
:raises core.CoreError: when node to update does not exist :raises core.CoreError: when node to update does not exist
""" """
# get node to update # get node to update
@ -778,6 +782,8 @@ class Session:
node.canvas = options.canvas node.canvas = options.canvas
node.icon = options.icon node.icon = options.icon
self.sdt.edit_node(node)
def set_node_position(self, node: NodeBase, options: NodeOptions) -> None: def set_node_position(self, node: NodeBase, options: NodeOptions) -> None:
""" """
Set position for a node, use lat/lon/alt if needed. Set position for a node, use lat/lon/alt if needed.
@ -1402,6 +1408,7 @@ class Session:
if node: if node:
node.shutdown() node.shutdown()
self.check_shutdown() self.check_shutdown()
self.sdt.delete_node(_id)
return node is not None return node is not None
@ -1413,6 +1420,7 @@ class Session:
funcs = [] funcs = []
while self.nodes: while self.nodes:
_, node = self.nodes.popitem() _, node = self.nodes.popitem()
self.sdt.delete_node(node.id)
funcs.append((node.shutdown, [], {})) funcs.append((node.shutdown, [], {}))
utils.threadpool(funcs) utils.threadpool(funcs)
self.node_id_gen.id = 0 self.node_id_gen.id = 0

View file

@ -4,22 +4,15 @@ sdt.py: Scripted Display Tool (SDT3D) helper
import logging import logging
import socket import socket
from typing import TYPE_CHECKING, Any, Optional import threading
from typing import TYPE_CHECKING, Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
from core import constants from core import constants
from core.api.tlv.coreapi import CoreLinkMessage, CoreMessage, CoreNodeMessage
from core.constants import CORE_DATA_DIR from core.constants import CORE_DATA_DIR
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.data import LinkData, NodeData from core.emulator.data import LinkData, NodeData
from core.emulator.enumerations import ( from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags
EventTypes,
LinkTlvs,
LinkTypes,
MessageFlags,
NodeTlvs,
NodeTypes,
)
from core.errors import CoreError from core.errors import CoreError
from core.nodes.base import CoreNetworkBase, NodeBase from core.nodes.base import CoreNetworkBase, NodeBase
from core.nodes.network import WlanNode from core.nodes.network import WlanNode
@ -28,19 +21,11 @@ if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
# TODO: A named tuple may be more appropriate, than abusing a class dict like this def link_data_params(link_data: LinkData) -> Tuple[int, int, bool]:
class Bunch: node_one = link_data.node1_id
""" node_two = link_data.node2_id
Helper class for recording a collection of attributes. is_wireless = link_data.link_type == LinkTypes.WIRELESS.value
""" return node_one, node_two, is_wireless
def __init__(self, **kwargs: Any) -> None:
"""
Create a Bunch instance.
:param kwargs: keyword arguments
"""
self.__dict__.update(kwargs)
class Sdt: class Sdt:
@ -74,53 +59,16 @@ class Sdt:
:param session: session this manager is tied to :param session: session this manager is tied to
""" """
self.session = session self.session = session
self.lock = threading.Lock()
self.sock = None self.sock = None
self.connected = False self.connected = False
self.showerror = True self.showerror = True
self.url = self.DEFAULT_SDT_URL self.url = self.DEFAULT_SDT_URL
# node information for remote nodes not in session._objs self.address = None
# local nodes also appear here since their obj may not exist yet self.protocol = None
self.remotes = {}
# add handler for node updates
self.session.node_handlers.append(self.handle_node_update) self.session.node_handlers.append(self.handle_node_update)
# add handler for link updates
self.session.link_handlers.append(self.handle_link_update) self.session.link_handlers.append(self.handle_link_update)
def handle_node_update(self, node_data: NodeData) -> None:
"""
Handler for node updates, specifically for updating their location.
:param node_data: node data being updated
:return: nothing
"""
x = node_data.x_position
y = node_data.y_position
lat = node_data.latitude
lon = node_data.longitude
alt = node_data.altitude
if all([lat is not None, lon is not None, alt is not None]):
self.updatenodegeo(node_data.id, lat, lon, alt)
elif node_data.message_type == 0:
# TODO: z is not currently supported by node messages
self.updatenode(node_data.id, 0, x, y, 0)
def handle_link_update(self, link_data: LinkData) -> None:
"""
Handler for link updates, checking for wireless link/unlink messages.
:param link_data: link data being updated
:return: nothing
"""
if link_data.link_type == LinkTypes.WIRELESS.value:
self.updatelink(
link_data.node1_id,
link_data.node2_id,
link_data.message_type,
wireless=True,
)
def is_enabled(self) -> bool: def is_enabled(self) -> bool:
""" """
Check for "enablesdt" session option. Return False by default if Check for "enablesdt" session option. Return False by default if
@ -137,9 +85,7 @@ class Sdt:
:return: nothing :return: nothing
""" """
url = self.session.options.get_config("stdurl") url = self.session.options.get_config("stdurl", default=self.DEFAULT_SDT_URL)
if not url:
url = self.DEFAULT_SDT_URL
self.url = urlparse(url) self.url = urlparse(url)
self.address = (self.url.hostname, self.url.port) self.address = (self.url.hostname, self.url.port)
self.protocol = self.url.scheme self.protocol = self.url.scheme
@ -178,7 +124,6 @@ class Sdt:
# refresh all objects in SDT3D when connecting after session start # refresh all objects in SDT3D when connecting after session start
if not flags & MessageFlags.ADD.value and not self.sendobjs(): if not flags & MessageFlags.ADD.value and not self.sendobjs():
return False return False
return True return True
def initialize(self) -> bool: def initialize(self) -> bool:
@ -234,8 +179,10 @@ class Sdt:
""" """
if self.sock is None: if self.sock is None:
return False return False
try: try:
cmd = f"{cmdstr}\n".encode() cmd = f"{cmdstr}\n".encode()
logging.debug("sdt cmd: %s", cmd)
self.sock.sendall(cmd) self.sock.sendall(cmd)
return True return True
except IOError: except IOError:
@ -244,91 +191,6 @@ class Sdt:
self.connected = False self.connected = False
return False return False
def updatenode(
self,
nodenum: int,
flags: int,
x: Optional[float],
y: Optional[float],
z: Optional[float],
name: str = None,
node_type: str = None,
icon: str = None,
) -> None:
"""
Node is updated from a Node Message or mobility script.
:param nodenum: node id to update
:param flags: update flags
:param x: x position
:param y: y position
:param z: z position
:param name: node name
:param node_type: node type
:param icon: node icon
:return: nothing
"""
if not self.connect():
return
if flags & MessageFlags.DELETE.value:
self.cmd(f"delete node,{nodenum}")
return
if x is None or y is None:
return
lat, lon, alt = self.session.location.getgeo(x, y, z)
pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
if flags & MessageFlags.ADD.value:
if icon is not None:
node_type = name
icon = icon.replace("$CORE_DATA_DIR", constants.CORE_DATA_DIR)
icon = icon.replace("$CORE_CONF_DIR", constants.CORE_CONF_DIR)
self.cmd(f"sprite {node_type} image {icon}")
self.cmd(f'node {nodenum} type {node_type} label on,"{name}" {pos}')
else:
self.cmd(f"node {nodenum} {pos}")
def updatenodegeo(self, nodenum: int, lat: float, lon: float, alt: float) -> None:
"""
Node is updated upon receiving an EMANE Location Event.
:param nodenum: node id to update geospatial for
:param lat: latitude
:param lon: longitude
:param alt: altitude
:return: nothing
"""
# TODO: received Node Message with lat/long/alt.
if not self.connect():
return
pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
self.cmd(f"node {nodenum} {pos}")
def updatelink(
self, node1num: int, node2num: int, flags: int, wireless: bool = False
) -> None:
"""
Link is updated from a Link Message or by a wireless model.
:param node1num: node one id
:param node2num: node two id
:param flags: link flags
:param wireless: flag to check if wireless or not
:return: nothing
"""
if node1num is None or node2num is None:
return
if not self.connect():
return
if flags & MessageFlags.DELETE.value:
self.cmd(f"delete link,{node1num},{node2num}")
elif flags & MessageFlags.ADD.value:
if wireless:
attr = " line green,2"
else:
attr = " line red,2"
self.cmd(f"link {node1num},{node2num}{attr}")
def sendobjs(self) -> None: def sendobjs(self) -> None:
""" """
Session has already started, and the SDT3D GUI later connects. Session has already started, and the SDT3D GUI later connects.
@ -345,171 +207,169 @@ class Sdt:
nets.append(node) nets.append(node)
if not isinstance(node, NodeBase): if not isinstance(node, NodeBase):
continue continue
(x, y, z) = node.getposition() self.add_node(node)
if x is None or y is None:
continue
self.updatenode(
node.id,
MessageFlags.ADD.value,
x,
y,
z,
node.name,
node.type,
node.icon,
)
for nodenum in sorted(self.remotes.keys()):
r = self.remotes[nodenum]
x, y, z = r.pos
self.updatenode(
nodenum, MessageFlags.ADD.value, x, y, z, r.name, r.type, r.icon
)
for net in nets: for net in nets:
all_links = net.all_link_data(flags=MessageFlags.ADD.value) all_links = net.all_link_data(flags=MessageFlags.ADD.value)
for link_data in all_links: for link_data in all_links:
is_wireless = isinstance(net, (WlanNode, EmaneNet)) is_wireless = isinstance(net, (WlanNode, EmaneNet))
wireless_link = link_data.message_type == LinkTypes.WIRELESS.value
if is_wireless and link_data.node1_id == net.id: if is_wireless and link_data.node1_id == net.id:
continue continue
params = link_data_params(link_data)
self.add_link(*params)
self.updatelink( def get_node_position(self, node: NodeBase) -> Optional[str]:
link_data.node1_id,
link_data.node2_id,
MessageFlags.ADD.value,
wireless_link,
)
for n1num in sorted(self.remotes.keys()):
r = self.remotes[n1num]
for n2num, wireless_link in r.links:
self.updatelink(n1num, n2num, MessageFlags.ADD.value, wireless_link)
def handle_distributed(self, message: CoreMessage) -> None:
""" """
Broker handler for processing CORE API messages as they are Convenience to generate an SDT position string, given a node.
received. This is used to snoop the Node messages and update
node positions.
:param message: message to handle :param node:
:return:
"""
x, y, z = node.position.get()
if x is None or y is None:
return None
lat, lon, alt = self.session.location.getgeo(x, y, z)
return f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
def add_node(self, node: NodeBase) -> None:
"""
Handle adding a node in SDT.
:param node: node to add
:return: nothing :return: nothing
""" """
if isinstance(message, CoreLinkMessage): logging.debug("sdt add node: %s - %s", node.id, node.name)
self.handlelinkmsg(message) if not self.connect():
elif isinstance(message, CoreNodeMessage): return
self.handlenodemsg(message) pos = self.get_node_position(node)
if not pos:
return
node_type = node.type
if node_type is None:
node_type = type(node).type
icon = node.icon
if icon:
node_type = node.name
icon = icon.replace("$CORE_DATA_DIR", constants.CORE_DATA_DIR)
icon = icon.replace("$CORE_CONF_DIR", constants.CORE_CONF_DIR)
self.cmd(f"sprite {node_type} image {icon}")
self.cmd(f'node {node.id} type {node_type} label on,"{node.name}" {pos}')
def handlenodemsg(self, msg: CoreNodeMessage) -> None: def edit_node(self, node: NodeBase) -> None:
""" """
Process a Node Message to add/delete or move a node on Handle updating a node in SDT.
the SDT display. Node properties are found in a session or
self.remotes for remote nodes (or those not yet instantiated).
:param msg: node message to handle :param node: node to update
:return: nothing :return: nothing
""" """
# for distributed sessions to work properly, the SDT option should be logging.debug("sdt update node: %s - %s", node.id, node.name)
# enabled prior to starting the session if not self.connect():
if not self.is_enabled():
return return
# node.(_id, type, icon, name) are used. pos = self.get_node_position(node)
nodenum = msg.get_tlv(NodeTlvs.NUMBER.value) if not pos:
if not nodenum:
return return
x = msg.get_tlv(NodeTlvs.X_POSITION.value) self.cmd(f"node {node.id} {pos}")
y = msg.get_tlv(NodeTlvs.Y_POSITION.value)
z = None
name = msg.get_tlv(NodeTlvs.NAME.value)
nodetype = msg.get_tlv(NodeTlvs.TYPE.value) def delete_node(self, node_id: int) -> None:
model = msg.get_tlv(NodeTlvs.MODEL.value) """
icon = msg.get_tlv(NodeTlvs.ICON.value) Handle deleting a node in SDT.
net = False :param node_id: node id to delete
if nodetype == NodeTypes.DEFAULT.value or nodetype == NodeTypes.PHYSICAL.value: :return: nothing
if model is None: """
model = "router" logging.debug("sdt delete node: %s", node_id)
nodetype = model if not self.connect():
elif nodetype is not None: return
nodetype = NodeTypes(nodetype) self.cmd(f"delete node,{node_id}")
nodetype = self.session.get_node_class(nodetype).type
net = True def handle_node_update(self, node_data: NodeData) -> None:
"""
Handler for node updates, specifically for updating their location.
:param node_data: node data being updated
:return: nothing
"""
logging.debug("sdt handle node update: %s - %s", node_data.id, node_data.name)
if not self.connect():
return
# delete node
if node_data.message_type == MessageFlags.DELETE.value:
self.cmd(f"delete node,{node_data.id}")
else: else:
nodetype = None x = node_data.x_position
y = node_data.y_position
lat = node_data.latitude
lon = node_data.longitude
alt = node_data.altitude
if all([lat is not None, lon is not None, alt is not None]):
pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
self.cmd(f"node {node_data.id} {pos}")
elif node_data.message_type == 0:
lat, lon, alt = self.session.location.getgeo(x, y, 0)
pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
self.cmd(f"node {node_data.id} {pos}")
def wireless_net_check(self, node_id: int) -> bool:
"""
Determines if a node is either a wireless node type.
:param node_id: node id to check
:return: True is a wireless node type, False otherwise
"""
result = False
try: try:
node = self.session.get_node(nodenum) node = self.session.get_node(node_id)
result = isinstance(node, (WlanNode, EmaneNet))
except CoreError: except CoreError:
node = None pass
if node: return result
self.updatenode(
node.id, msg.flags, x, y, z, node.name, node.type, node.icon
)
else:
if nodenum in self.remotes:
remote = self.remotes[nodenum]
if name is None:
name = remote.name
if nodetype is None:
nodetype = remote.type
if icon is None:
icon = remote.icon
else:
remote = Bunch(
_id=nodenum,
type=nodetype,
icon=icon,
name=name,
net=net,
links=set(),
)
self.remotes[nodenum] = remote
remote.pos = (x, y, z)
self.updatenode(nodenum, msg.flags, x, y, z, name, nodetype, icon)
def handlelinkmsg(self, msg: CoreLinkMessage) -> None: def add_link(self, node_one: int, node_two: int, is_wireless: bool) -> None:
""" """
Process a Link Message to add/remove links on the SDT display. Handle adding a link in SDT.
Links are recorded in the remotes[nodenum1].links set for updating
the SDT display at a later time.
:param msg: link message to handle :param node_one: node one id
:param node_two: node two id
:param is_wireless: True if link is wireless, False otherwise
:return: nothing :return: nothing
""" """
if not self.is_enabled(): logging.debug("sdt add link: %s, %s, %s", node_one, node_two, is_wireless)
if not self.connect():
return return
nodenum1 = msg.get_tlv(LinkTlvs.N1_NUMBER.value) if self.wireless_net_check(node_one) or self.wireless_net_check(node_two):
nodenum2 = msg.get_tlv(LinkTlvs.N2_NUMBER.value)
link_msg_type = msg.get_tlv(LinkTlvs.TYPE.value)
# this filters out links to WLAN and EMANE nodes which are not drawn
if self.wlancheck(nodenum1):
return return
wl = link_msg_type == LinkTypes.WIRELESS.value if is_wireless:
if nodenum1 in self.remotes: attr = "green,2"
r = self.remotes[nodenum1]
if msg.flags & MessageFlags.DELETE.value:
if (nodenum2, wl) in r.links:
r.links.remove((nodenum2, wl))
else:
r.links.add((nodenum2, wl))
self.updatelink(nodenum1, nodenum2, msg.flags, wireless=wl)
def wlancheck(self, nodenum: int) -> bool:
"""
Helper returns True if a node number corresponds to a WLAN or EMANE node.
:param nodenum: node id to check
:return: True if node is wlan or emane, False otherwise
"""
if nodenum in self.remotes:
node_type = self.remotes[nodenum].type
if node_type in ("wlan", "emane"):
return True
else: else:
try: attr = "red,2"
n = self.session.get_node(nodenum) self.cmd(f"link {node_one},{node_two} line {attr}")
except CoreError:
return False def delete_link(self, node_one: int, node_two: int) -> None:
if isinstance(n, (WlanNode, EmaneNet)): """
return True Handle deleting a node in SDT.
return False
:param node_one: node one id
:param node_two: node two id
:return: nothing
"""
logging.debug("sdt delete link: %s, %s", node_one, node_two)
if not self.connect():
return
if self.wireless_net_check(node_one) or self.wireless_net_check(node_two):
return
self.cmd(f"delete link,{node_one},{node_two}")
def handle_link_update(self, link_data: LinkData) -> None:
"""
Handle link broadcast messages and push changes to SDT.
:param link_data: link data to handle
:return: nothing
"""
if link_data.message_type == MessageFlags.ADD.value:
params = link_data_params(link_data)
self.add_link(*params)
elif link_data.message_type == MessageFlags.DELETE.value:
params = link_data_params(link_data)
self.delete_link(*params[:2])