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(
"%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:
logging.error("no handler for message type: %s", message.type_str())
return
@ -2042,7 +2037,6 @@ class CoreUdpHandler(CoreHandler):
logging.debug("session handling message: %s", session.session_id)
self.session = session
self.handle_message(message)
self.session.sdt.handle_distributed(message)
self.broadcast(message)
else:
logging.error(
@ -2067,7 +2061,6 @@ class CoreUdpHandler(CoreHandler):
if session or message.message_type == MessageTypes.REGISTER.value:
self.session = session
self.handle_message(message)
self.session.sdt.handle_distributed(message)
self.broadcast(message)
else:
logging.error(

View file

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

View file

@ -4,22 +4,15 @@ sdt.py: Scripted Display Tool (SDT3D) helper
import logging
import socket
from typing import TYPE_CHECKING, Any, Optional
import threading
from typing import TYPE_CHECKING, Optional, Tuple
from urllib.parse import urlparse
from core import constants
from core.api.tlv.coreapi import CoreLinkMessage, CoreMessage, CoreNodeMessage
from core.constants import CORE_DATA_DIR
from core.emane.nodes import EmaneNet
from core.emulator.data import LinkData, NodeData
from core.emulator.enumerations import (
EventTypes,
LinkTlvs,
LinkTypes,
MessageFlags,
NodeTlvs,
NodeTypes,
)
from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags
from core.errors import CoreError
from core.nodes.base import CoreNetworkBase, NodeBase
from core.nodes.network import WlanNode
@ -28,19 +21,11 @@ if TYPE_CHECKING:
from core.emulator.session import Session
# TODO: A named tuple may be more appropriate, than abusing a class dict like this
class Bunch:
"""
Helper class for recording a collection of attributes.
"""
def __init__(self, **kwargs: Any) -> None:
"""
Create a Bunch instance.
:param kwargs: keyword arguments
"""
self.__dict__.update(kwargs)
def link_data_params(link_data: LinkData) -> Tuple[int, int, bool]:
node_one = link_data.node1_id
node_two = link_data.node2_id
is_wireless = link_data.link_type == LinkTypes.WIRELESS.value
return node_one, node_two, is_wireless
class Sdt:
@ -74,53 +59,16 @@ class Sdt:
:param session: session this manager is tied to
"""
self.session = session
self.lock = threading.Lock()
self.sock = None
self.connected = False
self.showerror = True
self.url = self.DEFAULT_SDT_URL
# node information for remote nodes not in session._objs
# local nodes also appear here since their obj may not exist yet
self.remotes = {}
# add handler for node updates
self.address = None
self.protocol = None
self.session.node_handlers.append(self.handle_node_update)
# add handler for link updates
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:
"""
Check for "enablesdt" session option. Return False by default if
@ -137,9 +85,7 @@ class Sdt:
:return: nothing
"""
url = self.session.options.get_config("stdurl")
if not url:
url = self.DEFAULT_SDT_URL
url = self.session.options.get_config("stdurl", default=self.DEFAULT_SDT_URL)
self.url = urlparse(url)
self.address = (self.url.hostname, self.url.port)
self.protocol = self.url.scheme
@ -178,7 +124,6 @@ class Sdt:
# refresh all objects in SDT3D when connecting after session start
if not flags & MessageFlags.ADD.value and not self.sendobjs():
return False
return True
def initialize(self) -> bool:
@ -234,8 +179,10 @@ class Sdt:
"""
if self.sock is None:
return False
try:
cmd = f"{cmdstr}\n".encode()
logging.debug("sdt cmd: %s", cmd)
self.sock.sendall(cmd)
return True
except IOError:
@ -244,91 +191,6 @@ class Sdt:
self.connected = 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:
"""
Session has already started, and the SDT3D GUI later connects.
@ -345,171 +207,169 @@ class Sdt:
nets.append(node)
if not isinstance(node, NodeBase):
continue
(x, y, z) = node.getposition()
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
)
self.add_node(node)
for net in nets:
all_links = net.all_link_data(flags=MessageFlags.ADD.value)
for link_data in all_links:
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:
continue
params = link_data_params(link_data)
self.add_link(*params)
self.updatelink(
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:
def get_node_position(self, node: NodeBase) -> Optional[str]:
"""
Broker handler for processing CORE API messages as they are
received. This is used to snoop the Node messages and update
node positions.
Convenience to generate an SDT position string, given a node.
: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
"""
if isinstance(message, CoreLinkMessage):
self.handlelinkmsg(message)
elif isinstance(message, CoreNodeMessage):
self.handlenodemsg(message)
logging.debug("sdt add node: %s - %s", node.id, node.name)
if not self.connect():
return
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
the SDT display. Node properties are found in a session or
self.remotes for remote nodes (or those not yet instantiated).
Handle updating a node in SDT.
:param msg: node message to handle
:param node: node to update
:return: nothing
"""
# for distributed sessions to work properly, the SDT option should be
# enabled prior to starting the session
if not self.is_enabled():
logging.debug("sdt update node: %s - %s", node.id, node.name)
if not self.connect():
return
# node.(_id, type, icon, name) are used.
nodenum = msg.get_tlv(NodeTlvs.NUMBER.value)
if not nodenum:
pos = self.get_node_position(node)
if not pos:
return
x = msg.get_tlv(NodeTlvs.X_POSITION.value)
y = msg.get_tlv(NodeTlvs.Y_POSITION.value)
z = None
name = msg.get_tlv(NodeTlvs.NAME.value)
self.cmd(f"node {node.id} {pos}")
nodetype = msg.get_tlv(NodeTlvs.TYPE.value)
model = msg.get_tlv(NodeTlvs.MODEL.value)
icon = msg.get_tlv(NodeTlvs.ICON.value)
def delete_node(self, node_id: int) -> None:
"""
Handle deleting a node in SDT.
net = False
if nodetype == NodeTypes.DEFAULT.value or nodetype == NodeTypes.PHYSICAL.value:
if model is None:
model = "router"
nodetype = model
elif nodetype is not None:
nodetype = NodeTypes(nodetype)
nodetype = self.session.get_node_class(nodetype).type
net = True
:param node_id: node id to delete
:return: nothing
"""
logging.debug("sdt delete node: %s", node_id)
if not self.connect():
return
self.cmd(f"delete node,{node_id}")
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:
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:
node = self.session.get_node(nodenum)
node = self.session.get_node(node_id)
result = isinstance(node, (WlanNode, EmaneNet))
except CoreError:
node = None
if node:
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)
pass
return result
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.
Links are recorded in the remotes[nodenum1].links set for updating
the SDT display at a later time.
Handle adding a link in SDT.
: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
"""
if not self.is_enabled():
logging.debug("sdt add link: %s, %s, %s", node_one, node_two, is_wireless)
if not self.connect():
return
nodenum1 = msg.get_tlv(LinkTlvs.N1_NUMBER.value)
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):
if self.wireless_net_check(node_one) or self.wireless_net_check(node_two):
return
wl = link_msg_type == LinkTypes.WIRELESS.value
if nodenum1 in self.remotes:
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
if is_wireless:
attr = "green,2"
else:
try:
n = self.session.get_node(nodenum)
except CoreError:
return False
if isinstance(n, (WlanNode, EmaneNet)):
return True
return False
attr = "red,2"
self.cmd(f"link {node_one},{node_two} line {attr}")
def delete_link(self, node_one: int, node_two: int) -> None:
"""
Handle deleting a node in SDT.
: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])