daemon/gui: added support to configure wireless network for position calculations or not

This commit is contained in:
Blake Harnden 2022-04-14 16:31:14 -07:00
parent d124820a86
commit d20cb1ef58
10 changed files with 216 additions and 15 deletions

View file

@ -20,6 +20,7 @@ from core.api.grpc.configservices_pb2 import (
from core.api.grpc.core_pb2 import ( from core.api.grpc.core_pb2 import (
ExecuteScriptRequest, ExecuteScriptRequest,
GetConfigRequest, GetConfigRequest,
GetWirelessConfigRequest,
LinkedRequest, LinkedRequest,
WirelessConfigRequest, WirelessConfigRequest,
WirelessLinkedRequest, WirelessLinkedRequest,
@ -1142,6 +1143,13 @@ class CoreGrpcClient:
) )
self.stub.WirelessConfig(request) 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: def connect(self) -> None:
""" """
Open connection to server, must be closed manually. Open connection to server, must be closed manually.

View file

@ -29,6 +29,7 @@ from core.nodes.docker import DockerNode
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.nodes.lxd import LxcNode from core.nodes.lxd import LxcNode
from core.nodes.network import CoreNetwork, CtrlNet, PtpNet, WlanNode from core.nodes.network import CoreNetwork, CtrlNet, PtpNet, WlanNode
from core.nodes.wireless import WirelessNode
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -827,6 +828,9 @@ def configure_node(
if node.mobility_config: if node.mobility_config:
config = {k: v.value for k, v in node.mobility_config.items()} config = {k: v.value for k, v in node.mobility_config.items()}
session.mobility.set_model_config(node.id, Ns2ScriptedMobility.name, config) 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(): for service_name, service_config in node.service_configs.items():
data = service_config.data data = service_config.data
config = ServiceConfig( config = ServiceConfig(

View file

@ -30,6 +30,8 @@ from core.api.grpc.configservices_pb2 import (
) )
from core.api.grpc.core_pb2 import ( from core.api.grpc.core_pb2 import (
ExecuteScriptResponse, ExecuteScriptResponse,
GetWirelessConfigRequest,
GetWirelessConfigResponse,
LinkedRequest, LinkedRequest,
LinkedResponse, LinkedResponse,
WirelessConfigRequest, WirelessConfigRequest,
@ -1382,3 +1384,25 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
options2 = grpcutils.convert_options_proto(options2) options2 = grpcutils.convert_options_proto(options2)
wireless.link_config(request.node1_id, request.node2_id, options1, options2) wireless.link_config(request.node1_id, request.node2_id, options1, options2)
return WirelessConfigResponse() 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)

View file

@ -737,6 +737,7 @@ class Node:
Tuple[str, Optional[int]], Dict[str, ConfigOption] Tuple[str, Optional[int]], Dict[str, ConfigOption]
] = field(default_factory=dict, repr=False) ] = field(default_factory=dict, repr=False)
wlan_config: 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) mobility_config: Dict[str, ConfigOption] = field(default_factory=dict, repr=False)
service_configs: Dict[str, NodeServiceData] = field( service_configs: Dict[str, NodeServiceData] = field(
default_factory=dict, repr=False default_factory=dict, repr=False
@ -789,6 +790,7 @@ class Node:
service_file_configs=service_file_configs, service_file_configs=service_file_configs,
config_service_configs=config_service_configs, config_service_configs=config_service_configs,
emane_model_configs=emane_configs, emane_model_configs=emane_configs,
wireless_config=ConfigOption.from_dict(proto.wireless_config),
) )
def to_proto(self) -> core_pb2.Node: def to_proto(self) -> core_pb2.Node:
@ -840,6 +842,7 @@ class Node:
service_configs=service_configs, service_configs=service_configs,
config_service_configs=config_service_configs, config_service_configs=config_service_configs,
emane_configs=emane_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: def set_wlan(self, config: Dict[str, str]) -> None:

View file

@ -44,6 +44,7 @@ class Configuration:
label: str = None label: str = None
default: str = "" default: str = ""
options: List[str] = field(default_factory=list) options: List[str] = field(default_factory=list)
group: str = "Configuration"
def __post_init__(self) -> None: def __post_init__(self) -> None:
self.label = self.label if self.label else self.id self.label = self.label if self.label else self.id
@ -78,6 +79,7 @@ class ConfigBool(Configuration):
""" """
type: ConfigDataTypes = ConfigDataTypes.BOOL type: ConfigDataTypes = ConfigDataTypes.BOOL
value: bool = False
@dataclass @dataclass
@ -87,6 +89,7 @@ class ConfigFloat(Configuration):
""" """
type: ConfigDataTypes = ConfigDataTypes.FLOAT type: ConfigDataTypes = ConfigDataTypes.FLOAT
value: float = 0.0
@dataclass @dataclass
@ -96,6 +99,7 @@ class ConfigInt(Configuration):
""" """
type: ConfigDataTypes = ConfigDataTypes.INT32 type: ConfigDataTypes = ConfigDataTypes.INT32
value: int = 0
@dataclass @dataclass
@ -105,6 +109,7 @@ class ConfigString(Configuration):
""" """
type: ConfigDataTypes = ConfigDataTypes.STRING type: ConfigDataTypes = ConfigDataTypes.STRING
value: str = ""
class ConfigurableOptions: class ConfigurableOptions:

View file

@ -779,6 +779,9 @@ class CoreClient:
) )
return config 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]: def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]:
config = self.client.get_mobility_config(self.session.id, node_id) config = self.client.get_mobility_config(self.session.id, node_id)
logger.debug( logger.debug(

View file

@ -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()

View file

@ -16,6 +16,7 @@ from core.gui.dialogs.mobilityconfig import MobilityConfigDialog
from core.gui.dialogs.nodeconfig import NodeConfigDialog from core.gui.dialogs.nodeconfig import NodeConfigDialog
from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog from core.gui.dialogs.nodeconfigservice import NodeConfigServiceDialog
from core.gui.dialogs.nodeservice import NodeServiceDialog from core.gui.dialogs.nodeservice import NodeServiceDialog
from core.gui.dialogs.wirelessconfig import WirelessConfigDialog
from core.gui.dialogs.wlanconfig import WlanConfigDialog from core.gui.dialogs.wlanconfig import WlanConfigDialog
from core.gui.frames.node import NodeInfoFrame from core.gui.frames.node import NodeInfoFrame
from core.gui.graph import tags from core.gui.graph import tags
@ -219,6 +220,7 @@ class CanvasNode:
# clear existing menu # clear existing menu
self.context.delete(0, tk.END) self.context.delete(0, tk.END)
is_wlan = self.core_node.type == NodeType.WIRELESS_LAN 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_emane = self.core_node.type == NodeType.EMANE
is_mobility = is_wlan or is_emane is_mobility = is_wlan or is_emane
if self.app.core.is_runtime(): if self.app.core.is_runtime():
@ -231,6 +233,10 @@ class CanvasNode:
self.context.add_command( self.context.add_command(
label="WLAN Config", command=self.show_wlan_config 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: if is_mobility and self.core_node.id in self.app.core.mobility_players:
self.context.add_command( self.context.add_command(
label="Mobility Player", command=self.show_mobility_player label="Mobility Player", command=self.show_mobility_player
@ -268,6 +274,10 @@ class CanvasNode:
self.context.add_command( self.context.add_command(
label="WLAN Config", command=self.show_wlan_config 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: if is_mobility:
self.context.add_command( self.context.add_command(
label="Mobility Config", command=self.show_mobility_config label="Mobility Config", command=self.show_mobility_config
@ -346,6 +356,10 @@ class CanvasNode:
dialog = NodeConfigDialog(self.app, self) dialog = NodeConfigDialog(self.app, self)
dialog.show() dialog.show()
def show_wireless_config(self) -> None:
dialog = WirelessConfigDialog(self.app, self)
dialog.show()
def show_wlan_config(self) -> None: def show_wlan_config(self) -> None:
dialog = WlanConfigDialog(self.app, self) dialog = WlanConfigDialog(self.app, self)
if not dialog.has_error: if not dialog.has_error:

View file

@ -2,12 +2,13 @@
Defines a wireless node that allows programmatic link connectivity and Defines a wireless node that allows programmatic link connectivity and
configuration between pairs of nodes. configuration between pairs of nodes.
""" """
import copy
import logging import logging
import math import math
from dataclasses import dataclass 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.data import LinkData, LinkOptions
from core.emulator.enumerations import LinkTypes, MessageFlags from core.emulator.enumerations import LinkTypes, MessageFlags
from core.errors import CoreError from core.errors import CoreError
@ -20,6 +21,18 @@ if TYPE_CHECKING:
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
logger = logging.getLogger(__name__) 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( def calc_distance(
@ -47,6 +60,33 @@ class WirelessLink:
class WirelessNode(CoreNetworkBase): 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__( def __init__(
self, self,
session: "Session", session: "Session",
@ -57,6 +97,12 @@ class WirelessNode(CoreNetworkBase):
super().__init__(session, _id, name, server) super().__init__(session, _id, name, server)
self.bridges: Dict[int, Tuple[CoreInterface, str]] = {} self.bridges: Dict[int, Tuple[CoreInterface, str]] = {}
self.links: Dict[Tuple[int, int], WirelessLink] = {} 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: def startup(self) -> None:
if self.up: if self.up:
@ -94,8 +140,9 @@ class WirelessNode(CoreNetworkBase):
) )
# associate node iface with bridge # associate node iface with bridge
iface.net_client.set_iface_master(bridge_name, iface.localname) iface.net_client.set_iface_master(bridge_name, iface.localname)
# assign position callback # assign position callback, when enabled
iface.poshook = self.position_callback if self.position_enabled:
iface.poshook = self.position_callback
# save created bridge # save created bridge
self.bridges[iface.node.id] = (iface, bridge_name) self.bridges[iface.node.id] = (iface, bridge_name)
@ -226,21 +273,47 @@ class WirelessNode(CoreNetworkBase):
for oiface, bridge_name in self.bridges.values(): for oiface, bridge_name in self.bridges.values():
if iface == oiface: if iface == oiface:
continue continue
key = get_key(iface.node.id, oiface.node.id) self.calc_link(iface, oiface)
link = self.links.get(key)
if link.linked:
self.calc_link(iface, oiface)
def calc_link(self, iface1: CoreInterface, iface2: CoreInterface) -> None: 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() point1 = iface1.node.position.get()
point2 = iface2.node.position.get() point2 = iface2.node.position.get()
distance = calc_distance(point1, point2) - 250 distance = calc_distance(point1, point2)
distance = max(distance, 0.0) if distance >= self.max_range:
loss = min((distance / 500) * 100.0, 100.0) if link.linked:
node1_id = iface1.node.id self.link_control(iface1.node.id, iface2.node.id, False)
node2_id = iface2.node.id else:
options = LinkOptions(loss=loss, delay=0) if not link.linked:
self.link_config(node1_id, node2_id, options, options) 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: def adopt_iface(self, iface: CoreInterface, name: str) -> None:
raise CoreError(f"{type(self)} does not support adopt interface") 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])

View file

@ -107,6 +107,8 @@ service CoreApi {
} }
rpc WirelessConfig (WirelessConfigRequest) returns (WirelessConfigResponse) { rpc WirelessConfig (WirelessConfigRequest) returns (WirelessConfigResponse) {
} }
rpc GetWirelessConfig (GetWirelessConfigRequest) returns (GetWirelessConfigResponse) {
}
// emane rpc // emane rpc
rpc GetEmaneModelConfig (emane.GetEmaneModelConfigRequest) returns (emane.GetEmaneModelConfigResponse) { rpc GetEmaneModelConfig (emane.GetEmaneModelConfigRequest) returns (emane.GetEmaneModelConfigResponse) {
@ -624,6 +626,7 @@ message Node {
map<string, services.NodeServiceConfig> service_configs = 18; map<string, services.NodeServiceConfig> service_configs = 18;
map<string, configservices.ConfigServiceConfig> config_service_configs= 19; map<string, configservices.ConfigServiceConfig> config_service_configs= 19;
repeated emane.NodeEmaneConfig emane_configs = 20; repeated emane.NodeEmaneConfig emane_configs = 20;
map<string, common.ConfigOption> wireless_config = 21;
} }
message Link { message Link {
@ -728,3 +731,12 @@ message WirelessConfigRequest {
message WirelessConfigResponse { message WirelessConfigResponse {
} }
message GetWirelessConfigRequest {
int32 session_id = 1;
int32 node_id = 2;
}
message GetWirelessConfigResponse {
map<string, common.ConfigOption> config = 1;
}