diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 7eda0721..74f47c57 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -20,6 +20,7 @@ from core.api.grpc.configservices_pb2 import ( from core.api.grpc.core_pb2 import ( ExecuteScriptRequest, GetConfigRequest, + GetWirelessConfigRequest, LinkedRequest, WirelessConfigRequest, WirelessLinkedRequest, @@ -1142,6 +1143,13 @@ class CoreGrpcClient: ) self.stub.WirelessConfig(request) + def get_wireless_config( + self, session_id: int, node_id: int + ) -> Dict[str, wrappers.ConfigOption]: + request = GetWirelessConfigRequest(session_id=session_id, node_id=node_id) + response = self.stub.GetWirelessConfig(request) + return wrappers.ConfigOption.from_dict(response.config) + def connect(self) -> None: """ Open connection to server, must be closed manually. diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index c0771ced..05cdd3cd 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -29,6 +29,7 @@ from core.nodes.docker import DockerNode from core.nodes.interface import CoreInterface from core.nodes.lxd import LxcNode from core.nodes.network import CoreNetwork, CtrlNet, PtpNet, WlanNode +from core.nodes.wireless import WirelessNode from core.services.coreservices import CoreService logger = logging.getLogger(__name__) @@ -827,6 +828,9 @@ def configure_node( if node.mobility_config: config = {k: v.value for k, v in node.mobility_config.items()} session.mobility.set_model_config(node.id, Ns2ScriptedMobility.name, config) + if isinstance(core_node, WirelessNode) and node.wireless_config: + config = {k: v.value for k, v in node.wireless_config.items()} + core_node.set_config(config) for service_name, service_config in node.service_configs.items(): data = service_config.data config = ServiceConfig( diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index decb6703..5e18106b 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -30,6 +30,8 @@ from core.api.grpc.configservices_pb2 import ( ) from core.api.grpc.core_pb2 import ( ExecuteScriptResponse, + GetWirelessConfigRequest, + GetWirelessConfigResponse, LinkedRequest, LinkedResponse, WirelessConfigRequest, @@ -1382,3 +1384,25 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): options2 = grpcutils.convert_options_proto(options2) wireless.link_config(request.node1_id, request.node2_id, options1, options2) return WirelessConfigResponse() + + def GetWirelessConfig( + self, request: GetWirelessConfigRequest, context: ServicerContext + ) -> GetWirelessConfigResponse: + session = self.get_session(request.session_id, context) + try: + wireless = session.get_node(request.node_id, WirelessNode) + configs = wireless.get_config() + except CoreError: + configs = {x.id: x for x in WirelessNode.options} + config_options = {} + for config in configs.values(): + config_option = common_pb2.ConfigOption( + label=config.label, + name=config.id, + value=config.default, + type=config.type.value, + select=config.options, + group=config.group, + ) + config_options[config.id] = config_option + return GetWirelessConfigResponse(config=config_options) diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py index 1cb703f5..f3bb344c 100644 --- a/daemon/core/api/grpc/wrappers.py +++ b/daemon/core/api/grpc/wrappers.py @@ -737,6 +737,7 @@ class Node: Tuple[str, Optional[int]], Dict[str, ConfigOption] ] = field(default_factory=dict, repr=False) wlan_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False) + wireless_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False) mobility_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False) service_configs: Dict[str, NodeServiceData] = field( default_factory=dict, repr=False @@ -789,6 +790,7 @@ class Node: service_file_configs=service_file_configs, config_service_configs=config_service_configs, emane_model_configs=emane_configs, + wireless_config=ConfigOption.from_dict(proto.wireless_config), ) def to_proto(self) -> core_pb2.Node: @@ -840,6 +842,7 @@ class Node: service_configs=service_configs, config_service_configs=config_service_configs, emane_configs=emane_configs, + wireless_config={k: v.to_proto() for k, v in self.wireless_config.items()}, ) def set_wlan(self, config: Dict[str, str]) -> None: diff --git a/daemon/core/config.py b/daemon/core/config.py index de08649a..ae40627e 100644 --- a/daemon/core/config.py +++ b/daemon/core/config.py @@ -44,6 +44,7 @@ class Configuration: label: str = None default: str = "" options: List[str] = field(default_factory=list) + group: str = "Configuration" def __post_init__(self) -> None: self.label = self.label if self.label else self.id @@ -78,6 +79,7 @@ class ConfigBool(Configuration): """ type: ConfigDataTypes = ConfigDataTypes.BOOL + value: bool = False @dataclass @@ -87,6 +89,7 @@ class ConfigFloat(Configuration): """ type: ConfigDataTypes = ConfigDataTypes.FLOAT + value: float = 0.0 @dataclass @@ -96,6 +99,7 @@ class ConfigInt(Configuration): """ type: ConfigDataTypes = ConfigDataTypes.INT32 + value: int = 0 @dataclass @@ -105,6 +109,7 @@ class ConfigString(Configuration): """ type: ConfigDataTypes = ConfigDataTypes.STRING + value: str = "" class ConfigurableOptions: diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index d8233444..aab9cdb9 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -779,6 +779,9 @@ class CoreClient: ) return config + def get_wireless_config(self, node_id: int) -> Dict[str, ConfigOption]: + return self.client.get_wireless_config(self.session.id, node_id) + def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]: config = self.client.get_mobility_config(self.session.id, node_id) logger.debug( diff --git a/daemon/core/gui/dialogs/wirelessconfig.py b/daemon/core/gui/dialogs/wirelessconfig.py new file mode 100644 index 00000000..97e37b5f --- /dev/null +++ b/daemon/core/gui/dialogs/wirelessconfig.py @@ -0,0 +1,55 @@ +import tkinter as tk +from tkinter import ttk +from typing import TYPE_CHECKING, Dict, Optional + +import grpc + +from core.api.grpc.wrappers import ConfigOption, Node +from core.gui.dialogs.dialog import Dialog +from core.gui.themes import PADX, PADY +from core.gui.widgets import ConfigFrame + +if TYPE_CHECKING: + from core.gui.app import Application + from core.gui.graph.node import CanvasNode + + +class WirelessConfigDialog(Dialog): + def __init__(self, app: "Application", canvas_node: "CanvasNode"): + super().__init__(app, f"Wireless Configuration - {canvas_node.core_node.name}") + self.node: Node = canvas_node.core_node + self.config_frame: Optional[ConfigFrame] = None + self.config: Dict[str, ConfigOption] = {} + try: + config = self.node.wireless_config + if not config: + config = self.app.core.get_wireless_config(self.node.id) + self.config: Dict[str, ConfigOption] = config + self.draw() + except grpc.RpcError as e: + self.app.show_grpc_exception("Wireless Config Error", e) + self.has_error: bool = True + self.destroy() + + def draw(self) -> None: + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.config_frame = ConfigFrame(self.top, self.app, self.config) + self.config_frame.draw_config() + self.config_frame.grid(sticky=tk.NSEW, pady=PADY) + self.draw_buttons() + + def draw_buttons(self) -> None: + frame = ttk.Frame(self.top) + frame.grid(sticky=tk.EW) + for i in range(2): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, padx=PADX, sticky=tk.EW) + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky=tk.EW) + + def click_apply(self) -> None: + self.config_frame.parse_config() + self.node.wireless_config = self.config + self.destroy() diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 7f445f91..b3d0aae9 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -16,6 +16,7 @@ from core.gui.dialogs.mobilityconfig import MobilityConfigDialog from core.gui.dialogs.nodeconfig import NodeConfigDialog from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog from core.gui.dialogs.nodeservice import NodeServiceDialog +from core.gui.dialogs.wirelessconfig import WirelessConfigDialog from core.gui.dialogs.wlanconfig import WlanConfigDialog from core.gui.frames.node import NodeInfoFrame from core.gui.graph import tags @@ -219,6 +220,7 @@ class CanvasNode: # clear existing menu self.context.delete(0, tk.END) is_wlan = self.core_node.type == NodeType.WIRELESS_LAN + is_wireless = self.core_node.type == NodeType.WIRELESS is_emane = self.core_node.type == NodeType.EMANE is_mobility = is_wlan or is_emane if self.app.core.is_runtime(): @@ -231,6 +233,10 @@ class CanvasNode: self.context.add_command( label="WLAN Config", command=self.show_wlan_config ) + if is_wireless: + self.context.add_command( + label="Wireless Config", command=self.show_wireless_config + ) if is_mobility and self.core_node.id in self.app.core.mobility_players: self.context.add_command( label="Mobility Player", command=self.show_mobility_player @@ -268,6 +274,10 @@ class CanvasNode: self.context.add_command( label="WLAN Config", command=self.show_wlan_config ) + if is_wireless: + self.context.add_command( + label="Wireless Config", command=self.show_wireless_config + ) if is_mobility: self.context.add_command( label="Mobility Config", command=self.show_mobility_config @@ -346,6 +356,10 @@ class CanvasNode: dialog = NodeConfigDialog(self.app, self) dialog.show() + def show_wireless_config(self) -> None: + dialog = WirelessConfigDialog(self.app, self) + dialog.show() + def show_wlan_config(self) -> None: dialog = WlanConfigDialog(self.app, self) if not dialog.has_error: diff --git a/daemon/core/nodes/wireless.py b/daemon/core/nodes/wireless.py index 08e6922d..33ecb79d 100644 --- a/daemon/core/nodes/wireless.py +++ b/daemon/core/nodes/wireless.py @@ -2,12 +2,13 @@ Defines a wireless node that allows programmatic link connectivity and configuration between pairs of nodes. """ - +import copy import logging import math from dataclasses import dataclass -from typing import TYPE_CHECKING, Dict, Tuple +from typing import TYPE_CHECKING, Dict, List, Tuple +from core.config import ConfigBool, ConfigFloat, Configuration from core.emulator.data import LinkData, LinkOptions from core.emulator.enumerations import LinkTypes, MessageFlags from core.errors import CoreError @@ -20,6 +21,18 @@ if TYPE_CHECKING: from core.emulator.distributed import DistributedServer logger = logging.getLogger(__name__) +CONFIG_ENABLED: bool = True +CONFIG_RANGE: float = 400.0 +CONFIG_LOSS_RANGE: float = 300.0 +CONFIG_LOSS_FACTOR: float = 1.0 +CONFIG_DELAY_RANGE: float = 200.0 +CONFIG_DELAY_FACTOR: float = 1.0 +KEY_ENABLED: str = "movement" +KEY_RANGE: str = "max-range" +KEY_LOSS_RANGE: str = "loss-range" +KEY_LOSS_FACTOR: str = "loss-factor" +KEY_DELAY_RANGE: str = "delay-range" +KEY_DELAY_FACTOR: str = "delay-factor" def calc_distance( @@ -47,6 +60,33 @@ class WirelessLink: class WirelessNode(CoreNetworkBase): + options: List[Configuration] = [ + ConfigBool( + id=KEY_ENABLED, + default="1" if CONFIG_ENABLED else "0", + label="Movement Enabled?", + ), + ConfigFloat( + id=KEY_RANGE, default=str(CONFIG_RANGE), label="Max Range (pixels)" + ), + ConfigFloat( + id=KEY_LOSS_RANGE, + default=str(CONFIG_LOSS_RANGE), + label="Loss Start Range (pixels)", + ), + ConfigFloat( + id=KEY_LOSS_FACTOR, default=str(CONFIG_LOSS_FACTOR), label="Loss Factor" + ), + ConfigFloat( + id=KEY_DELAY_RANGE, + default=str(CONFIG_DELAY_RANGE), + label="Delay Start Range (pixels)", + ), + ConfigFloat( + id=KEY_DELAY_FACTOR, default=str(CONFIG_DELAY_FACTOR), label="Delay Factor" + ), + ] + def __init__( self, session: "Session", @@ -57,6 +97,12 @@ class WirelessNode(CoreNetworkBase): super().__init__(session, _id, name, server) self.bridges: Dict[int, Tuple[CoreInterface, str]] = {} self.links: Dict[Tuple[int, int], WirelessLink] = {} + self.position_enabled: bool = CONFIG_ENABLED + self.max_range: float = CONFIG_RANGE + self.loss_range: float = CONFIG_LOSS_RANGE + self.loss_factor: float = CONFIG_LOSS_FACTOR + self.delay_range: float = CONFIG_DELAY_RANGE + self.delay_factor: float = CONFIG_DELAY_FACTOR def startup(self) -> None: if self.up: @@ -94,8 +140,9 @@ class WirelessNode(CoreNetworkBase): ) # associate node iface with bridge iface.net_client.set_iface_master(bridge_name, iface.localname) - # assign position callback - iface.poshook = self.position_callback + # assign position callback, when enabled + if self.position_enabled: + iface.poshook = self.position_callback # save created bridge self.bridges[iface.node.id] = (iface, bridge_name) @@ -226,21 +273,47 @@ class WirelessNode(CoreNetworkBase): for oiface, bridge_name in self.bridges.values(): if iface == oiface: continue - key = get_key(iface.node.id, oiface.node.id) - link = self.links.get(key) - if link.linked: - self.calc_link(iface, oiface) + self.calc_link(iface, oiface) def calc_link(self, iface1: CoreInterface, iface2: CoreInterface) -> None: + key = get_key(iface1.node.id, iface2.node.id) + link = self.links.get(key) point1 = iface1.node.position.get() point2 = iface2.node.position.get() - distance = calc_distance(point1, point2) - 250 - distance = max(distance, 0.0) - loss = min((distance / 500) * 100.0, 100.0) - node1_id = iface1.node.id - node2_id = iface2.node.id - options = LinkOptions(loss=loss, delay=0) - self.link_config(node1_id, node2_id, options, options) + distance = calc_distance(point1, point2) + if distance >= self.max_range: + if link.linked: + self.link_control(iface1.node.id, iface2.node.id, False) + else: + if not link.linked: + self.link_control(iface1.node.id, iface2.node.id, True) + loss_distance = max(distance - self.loss_range, 0.0) + loss = min( + (loss_distance / self.max_range) * 100.0 * self.loss_factor, 100.0 + ) + delay_distance = max(distance - self.delay_range, 0.0) + delay = (delay_distance / self.max_range) * 100.0 * self.delay_factor + options = LinkOptions(loss=loss, delay=int(delay)) + self.link_config(iface1.node.id, iface2.node.id, options, options) def adopt_iface(self, iface: CoreInterface, name: str) -> None: raise CoreError(f"{type(self)} does not support adopt interface") + + def get_config(self) -> Dict[str, Configuration]: + config = {x.id: x for x in copy.copy(self.options)} + config[KEY_ENABLED].default = "1" if self.position_enabled else "0" + config[KEY_RANGE].default = str(self.max_range) + config[KEY_LOSS_RANGE].default = str(self.loss_range) + config[KEY_LOSS_FACTOR].default = str(self.loss_factor) + config[KEY_DELAY_RANGE].default = str(self.delay_range) + config[KEY_DELAY_FACTOR].default = str(self.delay_factor) + return config + + def set_config(self, config: Dict[str, str]) -> None: + logger.info("wireless config: %s", config) + self.position_enabled = config[KEY_ENABLED] == "1" + self.max_range = float(config[KEY_RANGE]) + self.loss_range = float(config[KEY_LOSS_RANGE]) + self.loss_factor = float(config[KEY_LOSS_FACTOR]) + self.delay_range = float(config[KEY_DELAY_RANGE]) + self.delay_factor = float(config[KEY_DELAY_FACTOR]) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 7678358b..4969e9c9 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -107,6 +107,8 @@ service CoreApi { } rpc WirelessConfig (WirelessConfigRequest) returns (WirelessConfigResponse) { } + rpc GetWirelessConfig (GetWirelessConfigRequest) returns (GetWirelessConfigResponse) { + } // emane rpc rpc GetEmaneModelConfig (emane.GetEmaneModelConfigRequest) returns (emane.GetEmaneModelConfigResponse) { @@ -624,6 +626,7 @@ message Node { map service_configs = 18; map config_service_configs= 19; repeated emane.NodeEmaneConfig emane_configs = 20; + map wireless_config = 21; } message Link { @@ -728,3 +731,12 @@ message WirelessConfigRequest { message WirelessConfigResponse { } + +message GetWirelessConfigRequest { + int32 session_id = 1; + int32 node_id = 2; +} + +message GetWirelessConfigResponse { + map config = 1; +}