A Discrete-Event Network Simulator
API
Loading...
Searching...
No Matches
core.py
Go to the documentation of this file.
1# -*- Mode: python; coding: utf-8 -*-
2from ctypes import c_double
3
4LAYOUT_ALGORITHM = "neato" # ['neato'|'dot'|'twopi'|'circo'|'fdp'|'nop']
5REPRESENT_CHANNELS_AS_NODES = 1
6DEFAULT_NODE_SIZE = 1.0 # default node size in meters
7DEFAULT_TRANSMISSIONS_MEMORY = (
8 5 # default number of of past intervals whose transmissions are remembered
9)
10BITRATE_FONT_SIZE = 10
11
12# internal constants, normally not meant to be changed
13SAMPLE_PERIOD = 0.1
14PRIORITY_UPDATE_MODEL = -100
15PRIORITY_UPDATE_VIEW = 200
16
17import platform
18import warnings
19
20if platform.system() == "Windows":
21 SHELL_FONT = "Lucida Console 9"
22else:
23 SHELL_FONT = "Luxi Mono 10"
24
25import math
26import os
27import sys
28
29try:
30 import threading
31except ImportError:
32 import dummy_threading as threading
33
34try:
35 import pygraphviz
36except ImportError:
37 print("Pygraphviz is required by the visualizer module and could not be found")
38 exit(1)
39
40try:
41 import cairo
42except ImportError:
43 print("Pycairo is required by the visualizer module and could not be found")
44 exit(1)
45
46try:
47 import gi
48except ImportError:
49 print("PyGObject is required by the visualizer module and could not be found")
50 exit(1)
51
52try:
53 import svgitem
54except ImportError:
55 svgitem = None
56
57try:
58 gi.require_version("GooCanvas", "2.0")
59 gi.require_version("Gtk", "3.0")
60 gi.require_version("Gdk", "3.0")
61 gi.require_foreign("cairo")
62 from gi.repository import Gdk, GLib, GObject, GooCanvas, Gtk, Pango
63
64 from . import hud
65except ImportError as e:
66 _import_error = e
67else:
68 _import_error = None
69
70try:
71 from . import ipython_view
72except ImportError:
73 ipython_view = None
74
75from .base import (
76 PIXELS_PER_METER,
77 InformationWindow,
78 Link,
79 PyVizObject,
80 load_plugins,
81 lookup_netdevice_traits,
82 plugins,
83 register_plugin,
84 transform_distance_canvas_to_simulation,
85 transform_distance_simulation_to_canvas,
86 transform_point_canvas_to_simulation,
87 transform_point_simulation_to_canvas,
88)
89
90PI_OVER_2 = math.pi / 2
91PI_TIMES_2 = math.pi * 2
92
93
94## Node class
96 ## @var visualizer
97 # visualier object
98 ## @var node_index
99 # node index
100 ## @var canvas_item
101 # canvas item
102 ## @var links
103 # links
104 ## @var _has_mobility
105 # has mobility model
106 ## @var _selected
107 # is selected
108 ## @var _highlighted
109 # is highlighted
110 ## @var _color
111 # color
112 ## @var _size
113 # size
114 ## @var menu
115 # menu
116 ## @var svg_item
117 # svg item
118 ## @var svg_align_x
119 # svg align X
120 ## @var svg_align_y
121 # svg align Y
122 ## @var _label
123 # label
124 ## @var _label_canvas_item
125 # label canvas
126 ## @var highlighted
127 # highlighted property
128 ## @var selected
129 # selected property
130 ## @var on_enter_notify_event
131 # on_enter_notify_event function
132 ## @var on_leave_notify_event
133 # on_leave_notify_event function
134
135 ## signal emitted whenever a tooltip is about to be shown for the node
136 ## the first signal parameter is a python list of strings, to which
137 ## information can be appended
138 __gsignals__ = {
139 "query-extra-tooltip-info": (GObject.SignalFlags.RUN_LAST, None, (object,)),
140 }
141
142 def __init__(self, visualizer, node_index):
143 """! Initialize function.
144 @param self The object pointer.
145 @param visualizer visualizer object
146 @param node_index node index
147 """
148 super(Node, self).__init__()
149
150 self.visualizer = visualizer
151 self.node_index = node_index
152 self.canvas_item = GooCanvas.CanvasEllipse()
153 self.canvas_item.pyviz_object = self
154 self.links = []
155 self._has_mobility = None
156 self._selected = False
157 self._highlighted = False
158 self._color = 0x808080FF
159 self._size = DEFAULT_NODE_SIZE
160 self.canvas_item.connect("enter-notify-event", self.on_enter_notify_event)
161 self.canvas_item.connect("leave-notify-event", self.on_leave_notify_event)
162 self.menu = None
163 self.svg_item = None
164 self.svg_align_x = None
165 self.svg_align_y = None
166 self._label = None
168
169 self._update_appearance() # call this last
170
171 def set_svg_icon(self, file_base_name, width=None, height=None, align_x=0.5, align_y=0.5):
172 """!
173 Set a background SVG icon for the node.
174
175 @param file_base_name: base file name, including .svg
176 extension, of the svg file. Place the file in the folder
177 src/contrib/visualizer/resource.
178
179 @param width: scale to the specified width, in meters
180 @param height: scale to the specified height, in meters
181
182 @param align_x: horizontal alignment of the icon relative to
183 the node position, from 0 (icon fully to the left of the node)
184 to 1.0 (icon fully to the right of the node)
185
186 @param align_y: vertical alignment of the icon relative to the
187 node position, from 0 (icon fully to the top of the node) to
188 1.0 (icon fully to the bottom of the node)
189
190 @return a ValueError exception if invalid dimensions.
191
192 """
193 if width is None and height is None:
194 raise ValueError("either width or height must be given")
195 rsvg_handle = svgitem.rsvg_handle_factory(file_base_name)
196 x = self.canvas_item.props.center_x
197 y = self.canvas_item.props.center_y
198 self.svg_item = svgitem.SvgItem(x, y, rsvg_handle)
199 self.svg_item.props.parent = self.visualizer.canvas.get_root_item()
200 self.svg_item.props.pointer_events = GooCanvas.CanvasPointerEvents.NONE
201 self.svg_item.lower(None)
202 self.svg_item.props.visibility = GooCanvas.CanvasItemVisibility.VISIBLE_ABOVE_THRESHOLD
203 if width is not None:
204 self.svg_item.props.width = transform_distance_simulation_to_canvas(width)
205 if height is not None:
206 self.svg_item.props.height = transform_distance_simulation_to_canvas(height)
207
208 # threshold1 = 10.0/self.svg_item.props.height
209 # threshold2 = 10.0/self.svg_item.props.width
210 # self.svg_item.props.visibility_threshold = min(threshold1, threshold2)
211
212 self.svg_align_x = align_x
213 self.svg_align_y = align_y
214 self._update_svg_position(x, y)
215 self._update_appearance()
216
217 def set_label(self, label):
218 """!
219 Set a label for the node.
220
221 @param self: class object.
222 @param label: label to set
223
224 @return: an exception if invalid parameter.
225 """
226 assert isinstance(label, basestring)
227 self._label = label
228 self._update_appearance()
229
230 def _update_svg_position(self, x, y):
231 """!
232 Update svg position.
233
234 @param self: class object.
235 @param x: x position
236 @param y: y position
237 @return none
238 """
239 w = self.svg_item.width
240 h = self.svg_item.height
241 self.svg_item.set_properties(
242 x=(x - (1 - self.svg_align_x) * w), y=(y - (1 - self.svg_align_y) * h)
243 )
244
245 def tooltip_query(self, tooltip):
246 """!
247 Query tooltip.
248
249 @param self: class object.
250 @param tooltip: tooltip
251 @return none
252 """
253 self.visualizer.simulation.lock.acquire()
254 try:
255 ns3_node = ns.NodeList.GetNode(self.node_index)
256 ipv4 = ns.cppyy.gbl.getNodeIpv4(ns3_node)
257 ipv6 = ns.cppyy.gbl.getNodeIpv6(ns3_node)
258
259 name = "<b><u>Node %i</u></b>" % self.node_index
260 node_name = ns.Names.FindName(ns3_node)
261 if len(node_name) != 0:
262 name += " <b>(" + node_name + ")</b>"
263
264 lines = [name]
265 lines.append("")
266
267 self.emit("query-extra-tooltip-info", lines)
268
269 mob = ns.cppyy.gbl.hasMobilityModel(ns3_node)
270 if mob:
271 mobility_model_name = ns.cppyy.gbl.getMobilityModelName(ns3_node)
272 lines.append(
273 " <b>Mobility Model</b>: %s" % ns.cppyy.gbl.getMobilityModelName(ns3_node)
274 )
275
276 for devI in range(ns3_node.GetNDevices()):
277 lines.append("")
278 lines.append(" <u>NetDevice %i:</u>" % devI)
279 dev = ns3_node.GetDevice(devI)
280 name = ns.Names.FindName(dev)
281 if name:
282 lines.append(" <b>Name:</b> %s" % name)
283 devname = dev.GetInstanceTypeId().GetName()
284 lines.append(" <b>Type:</b> %s" % devname)
285
286 if ipv4 is not None:
287 ipv4_idx = ipv4.GetInterfaceForDevice(dev)
288 if ipv4_idx != -1:
289 addresses = [
290 "%s/%s"
291 % (
292 ipv4.GetAddress(ipv4_idx, i).GetLocal(),
293 ipv4.GetAddress(ipv4_idx, i).GetMask(),
294 )
295 for i in range(ipv4.GetNAddresses(ipv4_idx))
296 ]
297 lines.append(" <b>IPv4 Addresses:</b> %s" % "; ".join(addresses))
298
299 if ipv6 is not None:
300 ipv6_idx = ipv6.GetInterfaceForDevice(dev)
301 if ipv6_idx != -1:
302 addresses = [
303 "%s/%s"
304 % (
305 ipv6.GetAddress(ipv6_idx, i).GetAddress(),
306 ipv6.GetAddress(ipv6_idx, i).GetPrefix(),
307 )
308 for i in range(ipv6.GetNAddresses(ipv6_idx))
309 ]
310 lines.append(" <b>IPv6 Addresses:</b> %s" % "; ".join(addresses))
311
312 lines.append(" <b>MAC Address:</b> %s" % (dev.GetAddress(),))
313
314 tooltip.set_markup("\n".join(lines))
315 finally:
316 self.visualizer.simulation.lock.release()
317
318 def on_enter_notify_event(self, view, target, event):
319 """!
320 On Enter event handle.
321
322 @param self: class object.
323 @param view: view
324 @param target: target
325 @param event: event
326 @return none
327 """
328
329 ## highlighted property
331
332 def on_leave_notify_event(self, view, target, event):
333 """!
334 On Leave event handle.
335
336 @param self: class object.
337 @param view: view
338 @param target: target
339 @param event: event
340 @return none
341 """
342 self.highlightedhighlighted = False
343
344 def _set_selected(self, value):
345 """!
346 Set selected function.
347
348 @param self: class object.
349 @param value: selected value
350 @return none
351 """
352 self._selected = value
353 self._update_appearance()
354
355 def _get_selected(self):
356 """!
357 Get selected function.
358
359 @param self: class object.
360 @return selected status
361 """
362 return self._selected
363
364 selected = property(_get_selected, _set_selected)
365
366 def _set_highlighted(self, value):
367 """!
368 Set highlighted function.
369
370 @param self: class object.
371 @param value: selected value
372 @return none
373 """
374 self._highlighted = value
375 self._update_appearance()
376
378 """!
379 Get highlighted function.
380
381 @param self: class object.
382 @return highlighted status
383 """
384 return self._highlighted
385
386 highlighted = property(_get_highlighted, _set_highlighted)
387
388 def set_size(self, size):
389 """!
390 Set size function.
391
392 @param self: class object.
393 @param size: selected size
394 @return none
395 """
396 self._size = size
397 self._update_appearance()
398
400 """!
401 Update the node aspect to reflect the selected/highlighted state
402
403 @param self: class object.
404 @return none
405 """
406
407 size = transform_distance_simulation_to_canvas(self._size)
408 if self.svg_item is not None:
409 alpha = 0x80
410 else:
411 alpha = 0xFF
412 fill_color_rgba = (self._color & 0xFFFFFF00) | alpha
413 self.canvas_item.set_properties(
414 radius_x=size, radius_y=size, fill_color_rgba=fill_color_rgba
415 )
416 if self._selected:
417 line_width = size * 0.3
418 else:
419 line_width = size * 0.15
421 stroke_color = "yellow"
422 else:
423 stroke_color = "black"
424 self.canvas_item.set_properties(line_width=line_width, stroke_color=stroke_color)
425
426 if self._label is not None:
427 if self._label_canvas_item is None:
428 self._label_canvas_item = GooCanvas.CanvasText(
429 visibility_threshold=0.5,
430 font="Sans Serif 10",
431 fill_color_rgba=0x808080FF,
432 alignment=Pango.Alignment.CENTER,
433 anchor=GooCanvas.CanvasAnchorType.N,
434 parent=self.visualizer.canvas.get_root_item(),
435 pointer_events=GooCanvas.CanvasPointerEvents.NONE,
436 )
437 self._label_canvas_item.lower(None)
438
439 self._label_canvas_item.set_properties(
440 visibility=GooCanvas.CanvasItemVisibility.VISIBLE_ABOVE_THRESHOLD, text=self._label
441 )
442 self._update_position()
443
444 def set_position(self, x, y):
445 """!
446 Set position function.
447
448 @param self: class object.
449 @param x: x position
450 @param y: y position
451 @return none
452 """
453 self.canvas_item.set_property("center_x", x)
454 self.canvas_item.set_property("center_y", y)
455 if self.svg_item is not None:
456 self._update_svg_position(x, y)
457
458 for link in self.links:
459 link.update_points()
460
461 if self._label_canvas_item is not None:
462 self._label_canvas_item.set_properties(x=x, y=(y + self._size * 3))
463
464 # If the location of the point is now beyond the bounds of the
465 # canvas then those bounds now need to be increased
466 try:
467 bounds = self.visualizer.canvas.get_bounds()
468
469 (min_x, min_y, max_x, max_y) = bounds
470
471 min_x = min(x, min_x)
472 min_y = min(y, min_y)
473 max_x = max(x, max_x)
474 max_y = max(y, max_y)
475
476 new_bounds = (min_x, min_y, max_x, max_y)
477
478 if new_bounds != bounds:
479 self.visualizer.canvas.set_bounds(*new_bounds)
480 except TypeError:
481 # bug 2969: GooCanvas.Canvas.get_bounds() inconsistency
482 pass
483
484 def get_position(self):
485 """!
486 Get position function.
487
488 @param self: class object.
489 @return x and y position
490 """
491 return (
492 self.canvas_item.get_property("center_x"),
493 self.canvas_item.get_property("center_y"),
494 )
495
497 """!
498 Update position function.
499
500 @param self: class object.
501 @return none
502 """
503 x, y = self.get_position()
504 self.set_position(x, y)
505
506 def set_color(self, color):
507 """!
508 Set color function.
509
510 @param self: class object.
511 @param color: color to set.
512 @return none
513 """
514 if isinstance(color, str):
515 color = Gdk.color_parse(color)
516 color = (
517 ((color.red >> 8) << 24)
518 | ((color.green >> 8) << 16)
519 | ((color.blue >> 8) << 8)
520 | 0xFF
521 )
522 self._color = color
523 self._update_appearance()
524
525 def add_link(self, link):
526 """!
527 Add link function.
528
529 @param self: class object.
530 @param link: link to add.
531 @return none
532 """
533 assert isinstance(link, Link)
534 self.links.append(link)
535
536 def remove_link(self, link):
537 """!
538 Remove link function.
539
540 @param self: class object.
541 @param link: link to add.
542 @return none
543 """
544 assert isinstance(link, Link)
545 self.links.remove(link)
546
547 @property
548 def has_mobility(self):
549 """!
550 Has mobility function.
551
552 @param self: class object.
553 @return modility option
554 """
555 if self._has_mobility is None:
556 node = ns.NodeList.GetNode(self.node_index)
557 self._has_mobility = ns.cppyy.gbl.hasMobilityModel(node)
558 return self._has_mobility
559
560
561## Channel
563 ## @var channel
564 # channel
565 ## @var canvas_item
566 # canvas
567 ## @var links
568 # list of links
569 #
570 def __init__(self, channel):
571 """!
572 Initializer function.
573
574 @param self: class object.
575 @param channel: channel.
576 """
577 self.channel = channel
578 self.canvas_item = GooCanvas.CanvasEllipse(
579 radius_x=30,
580 radius_y=30,
581 fill_color="white",
582 stroke_color="grey",
583 line_width=2.0,
584 line_dash=GooCanvas.CanvasLineDash.newv([10.0, 10.0]),
585 visibility=GooCanvas.CanvasItemVisibility.VISIBLE,
586 )
587 self.canvas_item.pyviz_object = self
588 self.links = []
589
590 def set_position(self, x, y):
591 """!
592 Initializer function.
593
594 @param self: class object.
595 @param x: x position.
596 @param y: y position.
597 @return
598 """
599 self.canvas_item.set_property("center_x", x)
600 self.canvas_item.set_property("center_y", y)
601
602 for link in self.links:
603 link.update_points()
604
605 def get_position(self):
606 """!
607 Initializer function.
608
609 @param self: class object.
610 @return x / y position.
611 """
612 return (
613 self.canvas_item.get_property("center_x"),
614 self.canvas_item.get_property("center_y"),
615 )
616
617
618## WiredLink
620 ## @var node1
621 # first node
622 ## @var node2
623 # second node
624 ## @var canvas_item
625 # canvas
626 #
627 def __init__(self, node1, node2):
628 """!
629 Initializer function.
630
631 @param self: class object.
632 @param node1: class object.
633 @param node2: class object.
634 """
635 assert isinstance(node1, Node)
636 assert isinstance(node2, (Node, Channel))
637 self.node1 = node1
638 self.node2 = node2
639 self.canvas_item = GooCanvas.CanvasPath(line_width=1.0, stroke_color="black")
640 self.canvas_item.pyviz_object = self
641 self.node1.links.append(self)
642 self.node2.links.append(self)
643
644 def update_points(self):
645 """!
646 Update points function.
647
648 @param self: class object.
649 @return none
650 """
651 pos1_x, pos1_y = self.node1.get_position()
652 pos2_x, pos2_y = self.node2.get_position()
653 self.canvas_item.set_property("data", "M %r %r L %r %r" % (pos1_x, pos1_y, pos2_x, pos2_y))
654
655
656## SimulationThread
657class SimulationThread(threading.Thread):
658 ## @var viz
659 # Visualizer object
660 ## @var lock
661 # thread lock
662 ## @var go
663 # thread event
664 ## @var target_time
665 # in seconds
666 ## @var quit
667 # quit indicator
668 ## @var sim_helper
669 # helper function
670 ## @var pause_messages
671 # pause messages
672 def __init__(self, viz):
673 """!
674 Initializer function.
675
676 @param self: class object.
677 @param viz: class object.
678 """
679 super(SimulationThread, self).__init__()
680 assert isinstance(viz, Visualizer)
681 self.viz = viz # Visualizer object
682 self.lock = threading.Lock()
683 self.go = threading.Event()
684 self.go.clear()
685 self.target_time = 0 # in seconds
686 self.quit = False
687 self.sim_helper = ns.PyViz()
689
690 def set_nodes_of_interest(self, nodes):
691 """!
692 Set nodes of interest function.
693
694 @param self: class object.
695 @param nodes: class object.
696 @return
697 """
698 self.lock.acquire()
699 try:
700 self.sim_helper.SetNodesOfInterest(nodes)
701 finally:
702 self.lock.release()
703
704 def run(self):
705 """!
706 Initializer function.
707
708 @param self: class object.
709 @return none
710 """
711 while not self.quit:
712 # print "sim: Wait for go"
713 self.go.wait() # wait until the main (view) thread gives us the go signal
714 self.go.clear()
715 if self.quit:
716 break
717 # self.go.clear()
718 # print "sim: Acquire lock"
719 self.lock.acquire()
720 try:
721 if 0:
723 self.viz.play_button.set_sensitive(False)
724 break
725 # print "sim: Current time is %f; Run until: %f" % (ns3.Simulator.Now ().GetSeconds (), self.target_time)
726 # if ns3.Simulator.Now ().GetSeconds () > self.target_time:
727 # print "skipping, model is ahead of view!"
728 self.sim_helper.SimulatorRunUntil(ns.Seconds(self.target_time))
729 # print "sim: Run until ended at current time: ", ns3.Simulator.Now ().GetSeconds ()
730 self.pause_messages.extend(self.sim_helper.GetPauseMessages())
731 GLib.idle_add(self.viz.update_model, priority=PRIORITY_UPDATE_MODEL)
732 # print "sim: Run until: ", self.target_time, ": finished."
733 finally:
734 self.lock.release()
735 # print "sim: Release lock, loop."
736
737
738## ShowTransmissionsMode
740 ## @var ALL
741 # all
742 ## @var NONE
743 # none
744 ## @var SELECTED
745 # selected
746
747 ## enumeration
748 __slots__ = []
749
750
751ShowTransmissionsMode.ALL = ShowTransmissionsMode()
752ShowTransmissionsMode.NONE = ShowTransmissionsMode()
753ShowTransmissionsMode.SELECTED = ShowTransmissionsMode()
754
755
756## Visualizer
757class Visualizer(GObject.GObject):
758 ## @var INSTANCE
759 # all
760 INSTANCE = None
761
762 if _import_error is None:
763 __gsignals__ = {
764 # signal emitted whenever a right-click-on-node popup menu is being constructed
765 "populate-node-menu": (
766 GObject.SignalFlags.RUN_LAST,
767 None,
768 (
769 object,
770 Gtk.Menu,
771 ),
772 ),
773 # signal emitted after every simulation period (SAMPLE_PERIOD seconds of simulated time)
774 # the simulation lock is acquired while the signal is emitted
775 "simulation-periodic-update": (GObject.SignalFlags.RUN_LAST, None, ()),
776 # signal emitted right after the topology is scanned
777 "topology-scanned": (GObject.SignalFlags.RUN_LAST, None, ()),
778 # signal emitted when it's time to update the view objects
779 "update-view": (GObject.SignalFlags.RUN_LAST, None, ()),
780 }
781
782 def __init__(self):
783 """!
784 Initializer function.
785
786 @param self: class object.
787 @return none
788 """
789 assert Visualizer.INSTANCE is None
790 Visualizer.INSTANCE = self
791 super(Visualizer, self).__init__()
792 self.nodes = {} # node index -> Node
793 self.channels = {} # id(ns3.Channel) -> Channel
794 self.window = None # toplevel window
795 self.canvas = None # GooCanvas.Canvas
796 self.time_label = None # Gtk.Label
797 self.play_button = None # Gtk.ToggleButton
798 self.zoom = None # Gtk.Adjustment
799 self._scrolled_window = None # Gtk.ScrolledWindow
800
801 self.links_group = GooCanvas.CanvasGroup()
802 self.channels_group = GooCanvas.CanvasGroup()
803 self.nodes_group = GooCanvas.CanvasGroup()
804
805 self._update_timeout_id = None
806 self.simulation = SimulationThread(self)
807 self.selected_node = None # node currently selected
808 self.speed = 1.0
809 self.information_windows = []
810 self._transmission_arrows = []
811 self._last_transmissions = []
812 self._drop_arrows = []
813 self._last_drops = []
814 self._show_transmissions_mode = None
815 self.set_show_transmissions_mode(ShowTransmissionsMode.ALL)
816 self._panning_state = None
817 self.node_size_adjustment = None
818 self.transmissions_smoothing_adjustment = None
819 self.sample_period = SAMPLE_PERIOD
820 self.node_drag_state = None
821 self.follow_node = None
822 self.shell_window = None
823
824 self.create_gui()
825
826 for plugin in plugins:
827 plugin(self)
828
829 def set_show_transmissions_mode(self, mode):
830 """!
831 Set show transmission mode.
832
833 @param self: class object.
834 @param mode: mode to set.
835 @return none
836 """
837 assert isinstance(mode, ShowTransmissionsMode)
838 self._show_transmissions_mode = mode
839 if self._show_transmissions_mode == ShowTransmissionsMode.ALL:
840 self.simulation.set_nodes_of_interest(list(range(ns.NodeList.GetNNodes())))
841 elif self._show_transmissions_mode == ShowTransmissionsMode.NONE:
842 self.simulation.set_nodes_of_interest([])
843 elif self._show_transmissions_mode == ShowTransmissionsMode.SELECTED:
844 if self.selected_node is None:
845 self.simulation.set_nodes_of_interest([])
846 else:
847 self.simulation.set_nodes_of_interest([self.selected_node.node_index])
848
849 def _create_advanced_controls(self):
850 """!
851 Create advanced controls.
852
853 @param self: class object.
854 @return expander
855 """
856 expander = Gtk.Expander.new("Advanced")
857 expander.show()
858
859 main_vbox = GObject.new(Gtk.VBox, border_width=8, visible=True)
860 expander.add(main_vbox)
861
862 main_hbox1 = GObject.new(Gtk.HBox, border_width=8, visible=True)
863 main_vbox.pack_start(main_hbox1, True, True, 0)
864
865 show_transmissions_group = GObject.new(
866 Gtk.HeaderBar, title="Show transmissions", visible=True
867 )
868 main_hbox1.pack_start(show_transmissions_group, False, False, 8)
869
870 vbox = Gtk.VBox(homogeneous=True, spacing=4)
871 vbox.show()
872 show_transmissions_group.add(vbox)
873
874 all_nodes = Gtk.RadioButton.new(None)
875 all_nodes.set_label("All nodes")
876 all_nodes.set_active(True)
877 all_nodes.show()
878 vbox.add(all_nodes)
879
880 selected_node = Gtk.RadioButton.new_from_widget(all_nodes)
881 selected_node.show()
882 selected_node.set_label("Selected node")
883 selected_node.set_active(False)
884 vbox.add(selected_node)
885
886 no_node = Gtk.RadioButton.new_from_widget(all_nodes)
887 no_node.show()
888 no_node.set_label("Disabled")
889 no_node.set_active(False)
890 vbox.add(no_node)
891
892 def toggled(radio):
893 if radio.get_active():
894 self.set_show_transmissions_mode(ShowTransmissionsMode.ALL)
895
896 all_nodes.connect("toggled", toggled)
897
898 def toggled(radio):
899 if radio.get_active():
900 self.set_show_transmissions_mode(ShowTransmissionsMode.NONE)
901
902 no_node.connect("toggled", toggled)
903
904 def toggled(radio):
905 if radio.get_active():
906 self.set_show_transmissions_mode(ShowTransmissionsMode.SELECTED)
907
908 selected_node.connect("toggled", toggled)
909
910 # -- misc settings
911 misc_settings_group = GObject.new(Gtk.HeaderBar, title="Misc Settings", visible=True)
912 main_hbox1.pack_start(misc_settings_group, False, False, 8)
913 settings_hbox = GObject.new(Gtk.HBox, border_width=8, visible=True)
914 misc_settings_group.add(settings_hbox)
915
916 # --> node size
917 vbox = GObject.new(Gtk.VBox, border_width=0, visible=True)
918 scale = GObject.new(Gtk.HScale, visible=True, digits=2)
919 vbox.pack_start(scale, True, True, 0)
920 vbox.pack_start(GObject.new(Gtk.Label, label="Node Size", visible=True), True, True, 0)
921 settings_hbox.pack_start(vbox, False, False, 6)
922 self.node_size_adjustment = scale.get_adjustment()
923
924 def node_size_changed(adj):
925 for node in self.nodes.values():
926 node.set_size(adj.get_value())
927
928 self.node_size_adjustment.connect("value-changed", node_size_changed)
929 self.node_size_adjustment.set_lower(0.01)
930 self.node_size_adjustment.set_upper(20)
931 self.node_size_adjustment.set_step_increment(0.1)
932 self.node_size_adjustment.set_value(DEFAULT_NODE_SIZE)
933
934 # --> transmissions smooth factor
935 vbox = GObject.new(Gtk.VBox, border_width=0, visible=True)
936 scale = GObject.new(Gtk.HScale, visible=True, digits=1)
937 vbox.pack_start(scale, True, True, 0)
938 vbox.pack_start(
939 GObject.new(Gtk.Label, label="Tx. Smooth Factor (s)", visible=True), True, True, 0
940 )
941 settings_hbox.pack_start(vbox, False, False, 6)
942 self.transmissions_smoothing_adjustment = scale.get_adjustment()
943 adj = self.transmissions_smoothing_adjustment
944 adj.set_lower(0.1)
945 adj.set_upper(10)
946 adj.set_step_increment(0.1)
947 adj.set_value(DEFAULT_TRANSMISSIONS_MEMORY * 0.1)
948
949 return expander
950
951 ## PanningState class
952 class _PanningState(object):
953 ## @var __slots__
954 # internal variables
955 __slots__ = ["initial_mouse_pos", "initial_canvas_pos", "motion_signal"]
956
957 def _begin_panning(self, widget, event):
958 """!
959 Set show trnamission mode.
960
961 @param self: class object.
962 @param mode: mode to set.
963 @return none
964 """
965 display = self.canvas.get_window().get_display()
966 cursor = Gdk.Cursor.new_for_display(display, Gdk.CursorType.FLEUR)
967 self.canvas.get_window().set_cursor(cursor)
968 self._panning_state = self._PanningState()
969 pos = widget.get_window().get_device_position(event.device)
970 self._panning_state.initial_mouse_pos = (pos.x, pos.y)
971 x = self._scrolled_window.get_hadjustment().get_value()
972 y = self._scrolled_window.get_vadjustment().get_value()
973 self._panning_state.initial_canvas_pos = (x, y)
974 self._panning_state.motion_signal = self.canvas.connect(
975 "motion-notify-event", self._panning_motion
976 )
977
978 def _end_panning(self, event):
979 """!
980 End panning function.
981
982 @param self: class object.
983 @param event: active event.
984 @return none
985 """
986 if self._panning_state is None:
987 return
988 self.canvas.get_window().set_cursor(None)
989 self.canvas.disconnect(self._panning_state.motion_signal)
990 self._panning_state = None
991
992 def _panning_motion(self, widget, event):
993 """!
994 Panning motion function.
995
996 @param self: class object.
997 @param widget: widget.
998 @param event: event.
999 @return true if successful
1000 """
1001 assert self._panning_state is not None
1002 if event.is_hint:
1003 pos = widget.get_window().get_device_position(event.device)
1004 x, y = pos.x, pos.y
1005 else:
1006 x, y = event.x, event.y
1007
1008 hadj = self._scrolled_window.get_hadjustment()
1009 vadj = self._scrolled_window.get_vadjustment()
1010 mx0, my0 = self._panning_state.initial_mouse_pos
1011 cx0, cy0 = self._panning_state.initial_canvas_pos
1012
1013 dx = x - mx0
1014 dy = y - my0
1015 hadj.set_value(cx0 - dx)
1016 vadj.set_value(cy0 - dy)
1017 return True
1018
1019 def _canvas_button_press(self, widget, event):
1020 if event.button == 2:
1021 self._begin_panning(widget, event)
1022 return True
1023 return False
1024
1025 def _canvas_button_release(self, dummy_widget, event):
1026 if event.button == 2:
1027 self._end_panning(event)
1028 return True
1029 return False
1030
1031 def _canvas_scroll_event(self, dummy_widget, event):
1032 if event.direction == Gdk.ScrollDirection.UP:
1033 self.zoom.set_value(self.zoom.get_value() * 1.25)
1034 return True
1035 elif event.direction == Gdk.ScrollDirection.DOWN:
1036 self.zoom.set_value(self.zoom.get_value() / 1.25)
1037 return True
1038 return False
1039
1040 def get_hadjustment(self):
1041 return self._scrolled_window.get_hadjustment()
1042
1043 def get_vadjustment(self):
1044 return self._scrolled_window.get_vadjustment()
1045
1046 def create_gui(self):
1047 self.window = Gtk.Window()
1048 vbox = Gtk.VBox()
1049 vbox.show()
1050 self.window.add(vbox)
1051
1052 # canvas
1053 self.canvas = GooCanvas.Canvas()
1054 self.canvas.connect_after("button-press-event", self._canvas_button_press)
1055 self.canvas.connect_after("button-release-event", self._canvas_button_release)
1056 self.canvas.connect("scroll-event", self._canvas_scroll_event)
1057 self.canvas.props.has_tooltip = True
1058 self.canvas.connect("query-tooltip", self._canvas_tooltip_cb)
1059 self.canvas.show()
1060 sw = Gtk.ScrolledWindow()
1061 sw.show()
1062 self._scrolled_window = sw
1063 sw.add(self.canvas)
1064 vbox.pack_start(sw, True, True, 4)
1065 self.canvas.set_size_request(600, 450)
1066 self.canvas.set_bounds(-10000, -10000, 10000, 10000)
1067 self.canvas.scroll_to(0, 0)
1068
1069 self.canvas.get_root_item().add_child(self.links_group, -1)
1070 self.links_group.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1071
1072 self.canvas.get_root_item().add_child(self.channels_group, -1)
1073 self.channels_group.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1074 self.channels_group.raise_(self.links_group)
1075
1076 self.canvas.get_root_item().add_child(self.nodes_group, -1)
1077 self.nodes_group.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1078 self.nodes_group.raise_(self.channels_group)
1079
1080 self.hud = hud.Axes(self)
1081
1082 hbox = Gtk.HBox()
1083 hbox.show()
1084 vbox.pack_start(hbox, False, False, 4)
1085
1086 # zoom
1087 zoom_adj = Gtk.Adjustment(
1088 value=1.0,
1089 lower=0.01,
1090 upper=10.0,
1091 step_increment=0.02,
1092 page_increment=1.0,
1093 page_size=1.0,
1094 )
1095 self.zoom = zoom_adj
1096
1097 def _zoom_changed(adj):
1098 self.canvas.set_scale(adj.get_value())
1099
1100 zoom_adj.connect("value-changed", _zoom_changed)
1101 zoom = Gtk.SpinButton.new(zoom_adj, 0.1, 1)
1102 zoom.set_digits(3)
1103 zoom.show()
1104 hbox.pack_start(GObject.new(Gtk.Label, label=" Zoom:", visible=True), False, False, 4)
1105 hbox.pack_start(zoom, False, False, 4)
1106 _zoom_changed(zoom_adj)
1107
1108 # speed
1109 speed_adj = Gtk.Adjustment(
1110 value=1.0, lower=0.01, upper=10.0, step_increment=0.02, page_increment=1.0, page_size=0
1111 )
1112
1113 def _speed_changed(adj):
1114 self.speed = adj.get_value()
1115 self.sample_period = SAMPLE_PERIOD * adj.get_value()
1116 self._start_update_timer()
1117
1118 speed_adj.connect("value-changed", _speed_changed)
1119 speed = Gtk.SpinButton.new(speed_adj, 1, 0)
1120 speed.set_digits(3)
1121 speed.show()
1122 hbox.pack_start(GObject.new(Gtk.Label, label=" Speed:", visible=True), False, False, 4)
1123 hbox.pack_start(speed, False, False, 4)
1124 _speed_changed(speed_adj)
1125
1126 # Current time
1127 self.time_label = GObject.new(Gtk.Label, label=" Speed:", visible=True)
1128 self.time_label.set_width_chars(20)
1129 hbox.pack_start(self.time_label, False, False, 4)
1130
1131 # Screenshot button
1132 screenshot_button = GObject.new(
1133 Gtk.Button,
1134 label="Snapshot",
1135 relief=Gtk.ReliefStyle.NONE,
1136 focus_on_click=False,
1137 visible=True,
1138 )
1139 hbox.pack_start(screenshot_button, False, False, 4)
1140
1141 def load_button_icon(button, icon_name):
1142 if not Gtk.IconTheme.get_default().has_icon(icon_name):
1143 print(f"Could not load icon {icon_name}", file=sys.stderr)
1144 return
1145 image = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.BUTTON)
1146 button.set_image(image)
1147 button.props.always_show_image = True
1148
1149 load_button_icon(screenshot_button, "applets-screenshooter")
1150 screenshot_button.connect("clicked", self._take_screenshot)
1151
1152 # Shell button
1153 if ipython_view is not None:
1154 shell_button = GObject.new(
1155 Gtk.Button,
1156 label="Shell",
1157 relief=Gtk.ReliefStyle.NONE,
1158 focus_on_click=False,
1159 visible=True,
1160 )
1161 hbox.pack_start(shell_button, False, False, 4)
1162 load_button_icon(shell_button, "gnome-terminal")
1163 shell_button.connect("clicked", self._start_shell)
1164
1165 # Play button
1166 self.play_button = GObject.new(
1167 Gtk.ToggleButton,
1168 label="Simulate (F3)",
1169 relief=Gtk.ReliefStyle.NONE,
1170 focus_on_click=False,
1171 visible=True,
1172 )
1173 load_button_icon(self.play_button, "media-playback-start")
1174 accel_group = Gtk.AccelGroup()
1175 self.window.add_accel_group(accel_group)
1176 self.play_button.add_accelerator(
1177 "clicked", accel_group, Gdk.KEY_F3, 0, Gtk.AccelFlags.VISIBLE
1178 )
1179 self.play_button.connect("toggled", self._on_play_button_toggled)
1180 hbox.pack_start(self.play_button, False, False, 4)
1181
1182 self.canvas.get_root_item().connect("button-press-event", self.on_root_button_press_event)
1183
1184 vbox.pack_start(self._create_advanced_controls(), False, False, 4)
1185
1186 display = Gdk.Display.get_default()
1187 try:
1188 monitor = display.get_primary_monitor()
1189 geometry = monitor.get_geometry()
1190 scale_factor = monitor.get_scale_factor()
1191 except AttributeError:
1192 screen = display.get_default_screen()
1193 monitor_id = screen.get_primary_monitor()
1194 geometry = screen.get_monitor_geometry(monitor_id)
1195 scale_factor = screen.get_monitor_scale_factor(monitor_id)
1196 width = scale_factor * geometry.width
1197 height = scale_factor * geometry.height
1198 self.window.set_default_size(width * 2 / 3, height * 2 / 3)
1199 self.window.show()
1200
1201 def scan_topology(self):
1202 print("scanning topology: %i nodes..." % (ns.NodeList.GetNNodes(),))
1203 graph = pygraphviz.AGraph()
1204 seen_nodes = 0
1205 for nodeI in range(ns.NodeList.GetNNodes()):
1206 seen_nodes += 1
1207 if seen_nodes == 100:
1208 print(
1209 "scan topology... %i nodes visited (%.1f%%)"
1210 % (nodeI, 100 * nodeI / ns.NodeList.GetNNodes())
1211 )
1212 seen_nodes = 0
1213 node = ns.NodeList.GetNode(nodeI)
1214 node_name = "Node %i" % nodeI
1215 node_view = self.get_node(nodeI)
1216
1217 mobility = ns.cppyy.gbl.hasMobilityModel(node)
1218 if mobility:
1219 node_view.set_color("red")
1220 pos = ns.cppyy.gbl.getNodePosition(node)
1221 node_view.set_position(*transform_point_simulation_to_canvas(pos.x, pos.y))
1222 # print "node has mobility position -> ", "%f,%f" % (pos.x, pos.y)
1223 else:
1224 graph.add_node(node_name)
1225
1226 for devI in range(node.GetNDevices()):
1227 device = node.GetDevice(devI)
1228 device_traits = lookup_netdevice_traits(type(device.__deref__()))
1229 if device_traits.is_wireless:
1230 continue
1231 if device_traits.is_virtual:
1232 continue
1233 channel = device.GetChannel()
1234 if channel.GetNDevices() > 2:
1235 if REPRESENT_CHANNELS_AS_NODES:
1236 # represent channels as white nodes
1237 if mobility is None:
1238 channel_name = "Channel %s" % id(channel)
1239 graph.add_edge(node_name, channel_name)
1240 self.get_channel(channel)
1241 self.create_link(self.get_node(nodeI), self.get_channel(channel))
1242 else:
1243 # don't represent channels, just add links between nodes in the same channel
1244 for otherDevI in range(channel.GetNDevices()):
1245 otherDev = channel.GetDevice(otherDevI)
1246 otherNode = otherDev.GetNode()
1247 otherNodeView = self.get_node(otherNode.GetId())
1248 if otherNode is not node:
1249 if mobility is None and not otherNodeView.has_mobility:
1250 other_node_name = "Node %i" % otherNode.GetId()
1251 graph.add_edge(node_name, other_node_name)
1252 self.create_link(self.get_node(nodeI), otherNodeView)
1253 else:
1254 for otherDevI in range(channel.GetNDevices()):
1255 otherDev = channel.GetDevice(otherDevI)
1256 otherNode = otherDev.GetNode()
1257 otherNodeView = self.get_node(otherNode.GetId())
1258 if otherNode is not node:
1259 if mobility is None and not otherNodeView.has_mobility:
1260 other_node_name = "Node %i" % otherNode.GetId()
1261 graph.add_edge(node_name, other_node_name)
1262 self.create_link(self.get_node(nodeI), otherNodeView)
1263
1264 print("scanning topology: calling graphviz layout")
1265 graph.layout(LAYOUT_ALGORITHM)
1266 for node in graph.iternodes():
1267 # print node, "=>", node.attr['pos']
1268 node_type, node_id = node.split(" ")
1269 pos_x, pos_y = [float(s) for s in node.attr["pos"].split(",")]
1270 if node_type == "Node":
1271 obj = self.nodes[int(node_id)]
1272 elif node_type == "Channel":
1273 obj = self.channels[int(node_id)]
1274 obj.set_position(pos_x, pos_y)
1275
1276 print("scanning topology: all done.")
1277 self.emit("topology-scanned")
1278
1279 def get_node(self, index):
1280 try:
1281 return self.nodes[index]
1282 except KeyError:
1283 node = Node(self, index)
1284 self.nodes[index] = node
1285 self.nodes_group.add_child(node.canvas_item, -1)
1286 node.canvas_item.connect("button-press-event", self.on_node_button_press_event, node)
1287 node.canvas_item.connect(
1288 "button-release-event", self.on_node_button_release_event, node
1289 )
1290 return node
1291
1292 def get_channel(self, ns3_channel):
1293 try:
1294 return self.channels[id(ns3_channel)]
1295 except KeyError:
1296 channel = Channel(ns3_channel)
1297 self.channels[id(ns3_channel)] = channel
1298 self.channels_group.add_child(channel.canvas_item, -1)
1299 return channel
1300
1301 def create_link(self, node, node_or_channel):
1302 link = WiredLink(node, node_or_channel)
1303 self.links_group.add_child(link.canvas_item, -1)
1304 link.canvas_item.lower(None)
1305
1306 def update_view(self):
1307 # print "update_view"
1308
1309 self.time_label.set_text("Time: %f s" % ns.Simulator.Now().GetSeconds())
1310
1311 self._update_node_positions()
1312
1313 # Update information
1314 for info_win in self.information_windows:
1315 info_win.update()
1316
1317 self._update_transmissions_view()
1318 self._update_drops_view()
1319
1320 self.emit("update-view")
1321
1322 def _update_node_positions(self):
1323 for node in self.nodes.values():
1324 if node.has_mobility:
1325 ns3_node = ns.NodeList.GetNode(node.node_index)
1326 mobility = ns.cppyy.gbl.hasMobilityModel(ns3_node)
1327 if mobility:
1328 pos = ns.cppyy.gbl.getNodePosition(ns3_node)
1329 x, y = transform_point_simulation_to_canvas(pos.x, pos.y)
1330 node.set_position(x, y)
1331 if node is self.follow_node:
1332 hadj = self._scrolled_window.get_hadjustment()
1333 vadj = self._scrolled_window.get_vadjustment()
1334 px, py = self.canvas.convert_to_pixels(x, y)
1335 hadj.set_value(px - hadj.get_page_size() / 2)
1336 vadj.set_value(py - vadj.get_page_size() / 2)
1337
1338 def center_on_node(self, node):
1339 if isinstance(node, ns.Node):
1340 node = self.nodes[node.GetId()]
1341 elif isinstance(node, int):
1342 node = self.nodes[node]
1343 elif isinstance(node, Node):
1344 pass
1345 else:
1346 raise TypeError("expected int, viz.Node or ns.Node, not %r" % node)
1347
1348 x, y = node.get_position()
1349 hadj = self._scrolled_window.get_hadjustment()
1350 vadj = self._scrolled_window.get_vadjustment()
1351 px, py = self.canvas.convert_to_pixels(x, y)
1352 hadj.set_value(px - hadj.get_page_size() / 2)
1353 vadj.set_value(py - vadj.get_page_size() / 2)
1354
1355 def update_model(self):
1356 self.simulation.lock.acquire()
1357 try:
1358 self.emit("simulation-periodic-update")
1359 finally:
1360 self.simulation.lock.release()
1361
1362 def do_simulation_periodic_update(self):
1363 smooth_factor = int(self.transmissions_smoothing_adjustment.get_value() * 10)
1364
1365 transmissions = self.simulation.sim_helper.GetTransmissionSamples()
1366 self._last_transmissions.append(transmissions)
1367 while len(self._last_transmissions) > smooth_factor:
1368 self._last_transmissions.pop(0)
1369
1370 drops = self.simulation.sim_helper.GetPacketDropSamples()
1371 self._last_drops.append(drops)
1372 while len(self._last_drops) > smooth_factor:
1373 self._last_drops.pop(0)
1374
1375 def _get_label_over_line_position(self, pos1_x, pos1_y, pos2_x, pos2_y):
1376 hadj = self._scrolled_window.get_hadjustment()
1377 vadj = self._scrolled_window.get_vadjustment()
1378 bounds_x1, bounds_y1 = self.canvas.convert_from_pixels(hadj.get_value(), vadj.get_value())
1379 bounds_x2, bounds_y2 = self.canvas.convert_from_pixels(
1380 hadj.get_value() + hadj.get_page_size(), vadj.get_value() + vadj.get_page_size()
1381 )
1382 ns.PyViz.LineClipping(
1383 bounds_x1, bounds_y1, bounds_x2, bounds_y2, pos1_x, pos1_y, pos2_x, pos2_y
1384 )
1385 return (pos1_x.value + pos2_x.value) / 2, (pos1_y.value + pos2_y.value) / 2
1386
1387 def _update_transmissions_view(self):
1388 transmissions_average = {}
1389 for transmission_set in self._last_transmissions:
1390 for transmission in transmission_set:
1391 key = (transmission.transmitter.GetId(), transmission.receiver.GetId())
1392 rx_bytes, count = transmissions_average.get(key, (0, 0))
1393 rx_bytes += transmission.bytes
1394 count += 1
1395 transmissions_average[key] = rx_bytes, count
1396
1397 old_arrows = self._transmission_arrows
1398 for arrow, label in old_arrows:
1399 arrow.set_property("visibility", GooCanvas.CanvasItemVisibility.HIDDEN)
1400 label.set_property("visibility", GooCanvas.CanvasItemVisibility.HIDDEN)
1401 new_arrows = []
1402
1403 k = self.node_size_adjustment.get_value() / 5
1404
1405 for (transmitter_id, receiver_id), (rx_bytes, rx_count) in transmissions_average.items():
1406 transmitter = self.get_node(transmitter_id)
1407 receiver = self.get_node(receiver_id)
1408 try:
1409 arrow, label = old_arrows.pop()
1410 except IndexError:
1411 arrow = GooCanvas.CanvasPolyline(
1412 line_width=2.0,
1413 stroke_color_rgba=0x00C000C0,
1414 close_path=False,
1415 end_arrow=True,
1416 pointer_events=GooCanvas.CanvasPointerEvents.NONE,
1417 )
1418 arrow.set_property("parent", self.canvas.get_root_item())
1419 arrow.raise_(None)
1420
1421 label = GooCanvas.CanvasText(
1422 parent=self.canvas.get_root_item(),
1423 pointer_events=GooCanvas.CanvasPointerEvents.NONE,
1424 )
1425 label.raise_(None)
1426
1427 arrow.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1428 line_width = max(0.1, math.log(float(rx_bytes) / rx_count / self.sample_period) * k)
1429 arrow.set_property("line-width", line_width)
1430
1431 pos1_x, pos1_y = transmitter.get_position()
1432 pos2_x, pos2_y = receiver.get_position()
1433 points = GooCanvas.CanvasPoints.new(2)
1434 points.set_point(0, pos1_x, pos1_y)
1435 points.set_point(1, pos2_x, pos2_y)
1436 arrow.set_property("points", points)
1437
1438 kbps = float(rx_bytes * 8) / 1e3 / rx_count / self.sample_period
1439 label.set_properties(
1440 visibility=GooCanvas.CanvasItemVisibility.VISIBLE_ABOVE_THRESHOLD,
1441 visibility_threshold=0.5,
1442 font=("Sans Serif %f" % int(1 + BITRATE_FONT_SIZE * k)),
1443 )
1444 angle = math.atan2((pos2_y - pos1_y), (pos2_x - pos1_x))
1445 if -PI_OVER_2 <= angle <= PI_OVER_2:
1446 label.set_properties(
1447 text=("%.2f kbit/s →" % (kbps,)),
1448 alignment=Pango.Alignment.CENTER,
1449 anchor=GooCanvas.CanvasAnchorType.S,
1450 x=0,
1451 y=-line_width / 2,
1452 )
1453 else:
1454 label.set_properties(
1455 text=("← %.2f kbit/s" % (kbps,)),
1456 alignment=Pango.Alignment.CENTER,
1457 anchor=GooCanvas.CanvasAnchorType.N,
1458 x=0,
1459 y=line_width / 2,
1460 )
1461 M = cairo.Matrix()
1462 lx, ly = self._get_label_over_line_position(
1463 c_double(pos1_x), c_double(pos1_y), c_double(pos2_x), c_double(pos2_y)
1464 )
1465 M.translate(lx, ly)
1466 M.rotate(angle)
1467 try:
1468 label.set_transform(M)
1469 except KeyError:
1470 # https://gitlab.gnome.org/GNOME/pygobject/issues/16
1471 warnings.warn(
1472 "PyGobject bug causing label position error; "
1473 "should be fixed in PyGObject >= 3.29.1"
1474 )
1475 label.set_properties(x=(lx + label.props.x), y=(ly + label.props.y))
1476
1477 new_arrows.append((arrow, label))
1478
1479 self._transmission_arrows = new_arrows + old_arrows
1480
1481 def _update_drops_view(self):
1482 drops_average = {}
1483 for drop_set in self._last_drops:
1484 for drop in drop_set:
1485 key = drop.transmitter.GetId()
1486 drop_bytes, count = drops_average.get(key, (0, 0))
1487 drop_bytes += drop.bytes
1488 count += 1
1489 drops_average[key] = drop_bytes, count
1490
1491 old_arrows = self._drop_arrows
1492 for arrow, label in old_arrows:
1493 arrow.set_property("visibility", GooCanvas.CanvasItemVisibility.HIDDEN)
1494 label.set_property("visibility", GooCanvas.CanvasItemVisibility.HIDDEN)
1495 new_arrows = []
1496
1497 # get the coordinates for the edge of screen
1498 vadjustment = self._scrolled_window.get_vadjustment()
1499 bottom_y = vadjustment.get_value() + vadjustment.get_page_size()
1500 dummy, edge_y = self.canvas.convert_from_pixels(0, bottom_y)
1501
1502 k = self.node_size_adjustment.get_value() / 5
1503
1504 for transmitter_id, (drop_bytes, drop_count) in drops_average.items():
1505 transmitter = self.get_node(transmitter_id)
1506 try:
1507 arrow, label = old_arrows.pop()
1508 except IndexError:
1509 arrow = GooCanvas.CanvasPolyline(
1510 line_width=2.0,
1511 stroke_color_rgba=0xC00000C0,
1512 close_path=False,
1513 end_arrow=True,
1514 pointer_events=GooCanvas.CanvasPointerEvents.NONE,
1515 )
1516 arrow.set_property("parent", self.canvas.get_root_item())
1517 arrow.raise_(None)
1518
1519 label = GooCanvas.CanvasText(
1520 pointer_events=GooCanvas.CanvasPointerEvents.NONE
1521 ) # , fill_color_rgba=0x00C000C0)
1522 label.set_property("parent", self.canvas.get_root_item())
1523 label.raise_(None)
1524
1525 arrow.set_property("visibility", GooCanvas.CanvasItemVisibility.VISIBLE)
1526 arrow.set_property(
1527 "line-width",
1528 max(0.1, math.log(float(drop_bytes) / drop_count / self.sample_period) * k),
1529 )
1530 pos1_x, pos1_y = transmitter.get_position()
1531 pos2_x, pos2_y = pos1_x, edge_y
1532 points = GooCanvas.CanvasPoints.new(2)
1533 points.set_point(0, pos1_x, pos1_y)
1534 points.set_point(1, pos2_x, pos2_y)
1535 arrow.set_property("points", points)
1536
1537 label.set_properties(
1538 visibility=GooCanvas.CanvasItemVisibility.VISIBLE_ABOVE_THRESHOLD,
1539 visibility_threshold=0.5,
1540 font=("Sans Serif %i" % int(1 + BITRATE_FONT_SIZE * k)),
1541 text=(
1542 "%.2f kbit/s" % (float(drop_bytes * 8) / 1e3 / drop_count / self.sample_period,)
1543 ),
1544 alignment=Pango.Alignment.CENTER,
1545 x=(pos1_x + pos2_x) / 2,
1546 y=(pos1_y + pos2_y) / 2,
1547 )
1548
1549 new_arrows.append((arrow, label))
1550
1551 self._drop_arrows = new_arrows + old_arrows
1552
1553 def update_view_timeout(self):
1554 # print "view: update_view_timeout called at real time ", time.time()
1555
1556 # while the simulator is busy, run the gtk event loop
1557 while not self.simulation.lock.acquire(False):
1558 while Gtk.events_pending():
1559 Gtk.main_iteration()
1560 pause_messages = self.simulation.pause_messages
1561 self.simulation.pause_messages = []
1562 try:
1563 self.update_view()
1564 self.simulation.target_time = ns.Simulator.Now().GetSeconds() + self.sample_period
1565 # print "view: target time set to %f" % self.simulation.target_time
1566 finally:
1567 self.simulation.lock.release()
1568
1569 if pause_messages:
1570 # print pause_messages
1571 dialog = Gtk.MessageDialog(
1572 parent=self.window,
1573 flags=0,
1574 type=Gtk.MessageType.WARNING,
1575 buttons=Gtk.ButtonsType.OK,
1576 message_format="\n".join(pause_messages),
1577 )
1578 dialog.connect("response", lambda d, r: d.destroy())
1579 dialog.show()
1580 self.play_button.set_active(False)
1581
1582 # if we're paused, stop the update timer
1583 if not self.play_button.get_active():
1584 self._update_timeout_id = None
1585 return False
1586
1587 # print "view: self.simulation.go.set()"
1588 self.simulation.go.set()
1589 # print "view: done."
1590 return True
1591
1592 def _start_update_timer(self):
1593 if self._update_timeout_id is not None:
1594 GLib.source_remove(self._update_timeout_id)
1595 # print "start_update_timer"
1596 self._update_timeout_id = GLib.timeout_add(
1597 int(SAMPLE_PERIOD / min(self.speed, 1) * 1e3),
1598 self.update_view_timeout,
1599 priority=PRIORITY_UPDATE_VIEW,
1600 )
1601
1602 def _on_play_button_toggled(self, button):
1603 if button.get_active():
1604 self._start_update_timer()
1605 else:
1606 if self._update_timeout_id is not None:
1607 GLib.source_remove(self._update_timeout_id)
1608
1609 def _quit(self, *dummy_args):
1610 if self._update_timeout_id is not None:
1611 GLib.source_remove(self._update_timeout_id)
1612 self._update_timeout_id = None
1613 self.simulation.quit = True
1614 self.simulation.go.set()
1615 self.simulation.join()
1616 Gtk.main_quit()
1617
1618 def _monkey_patch_ipython(self):
1619 # The user may want to access the NS 3 simulation state, but
1620 # NS 3 is not thread safe, so it could cause serious problems.
1621 # To work around this, monkey-patch IPython to automatically
1622 # acquire and release the simulation lock around each code
1623 # that is executed.
1624
1625 original_runcode = self.ipython.runcode
1626
1627 def runcode(ip, *args):
1628 # print "lock"
1629 self.simulation.lock.acquire()
1630 try:
1631 return original_runcode(*args)
1632 finally:
1633 # print "unlock"
1634 self.simulation.lock.release()
1635
1636 import types
1637
1638 self.ipython.runcode = types.MethodType(runcode, self.ipython)
1639
1640 def autoscale_view(self):
1641 if not self.nodes:
1642 return
1643 self._update_node_positions()
1644 positions = [node.get_position() for node in self.nodes.values()]
1645 min_x, min_y = min(x for (x, y) in positions), min(y for (x, y) in positions)
1646 max_x, max_y = max(x for (x, y) in positions), max(y for (x, y) in positions)
1647 min_x_px, min_y_px = self.canvas.convert_to_pixels(min_x, min_y)
1648 max_x_px, max_y_px = self.canvas.convert_to_pixels(max_x, max_y)
1649 dx = max_x - min_x
1650 dy = max_y - min_y
1651 dx_px = max_x_px - min_x_px
1652 dy_px = max_y_px - min_y_px
1653 hadj = self._scrolled_window.get_hadjustment()
1654 vadj = self._scrolled_window.get_vadjustment()
1655 new_dx, new_dy = 1.5 * dx_px, 1.5 * dy_px
1656
1657 if new_dx == 0 or new_dy == 0:
1658 return
1659
1660 self.zoom.set_value(min(hadj.get_page_size() / new_dx, vadj.get_page_size() / new_dy))
1661
1662 x1, y1 = self.canvas.convert_from_pixels(hadj.get_value(), vadj.get_value())
1663 x2, y2 = self.canvas.convert_from_pixels(
1664 (hadj.get_value() + hadj.get_page_size()), (vadj.get_value() + vadj.get_page_size())
1665 )
1666 width = x2 - x1
1667 height = y2 - y1
1668 center_x = (min_x + max_x) / 2
1669 center_y = (min_y + max_y) / 2
1670
1671 self.canvas.scroll_to(center_x - width / 2, center_y - height / 2)
1672
1673 return False
1674
1675 def start(self):
1676 self.scan_topology()
1677 self.window.connect("delete-event", self._quit)
1678 # self._start_update_timer()
1679 GLib.timeout_add(200, self.autoscale_view)
1680 self.simulation.start()
1681
1682 try:
1683 __IPYTHON__
1684 except NameError:
1685 pass
1686 else:
1687 self._monkey_patch_ipython()
1688
1689 Gtk.main()
1690
1691 def on_root_button_press_event(self, view, target, event):
1692 if event.button == 1:
1693 self.select_node(None)
1694 return True
1695
1696 def on_node_button_press_event(self, view, target, event, node):
1697 button = event.button
1698 if button == 1:
1699 self.select_node(node)
1700 return True
1701 elif button == 3:
1702 self.popup_node_menu(node, event)
1703 return True
1704 elif button == 2:
1705 self.begin_node_drag(node, event)
1706 return True
1707 return False
1708
1709 def on_node_button_release_event(self, view, target, event, node):
1710 if event.button == 2:
1711 self.end_node_drag(node)
1712 return True
1713 return False
1714
1715 class NodeDragState(object):
1716 def __init__(self, canvas_x0, canvas_y0, sim_x0, sim_y0):
1717 self.canvas_x0 = canvas_x0
1718 self.canvas_y0 = canvas_y0
1719 self.sim_x0 = sim_x0
1720 self.sim_y0 = sim_y0
1721 self.motion_signal = None
1722
1723 def begin_node_drag(self, node, event):
1724 self.simulation.lock.acquire()
1725 try:
1726 ns3_node = ns.NodeList.GetNode(node.node_index)
1727 mob = ns.cppyy.gbl.hasMobilityModel(ns3_node)
1728 if not mob:
1729 return
1730 if self.node_drag_state is not None:
1731 return
1732 pos = ns.cppyy.gbl.getNodePosition(ns3_node)
1733 finally:
1734 self.simulation.lock.release()
1735 devpos = self.canvas.get_window().get_device_position(event.device)
1736 x0, y0 = self.canvas.convert_from_pixels(devpos.x, devpos.y)
1737 self.node_drag_state = self.NodeDragState(x0, y0, pos.x, pos.y)
1738 self.node_drag_state.motion_signal = node.canvas_item.connect(
1739 "motion-notify-event", self.node_drag_motion, node
1740 )
1741
1742 def node_drag_motion(self, item, targe_item, event, node):
1743 self.simulation.lock.acquire()
1744 try:
1745 ns3_node = ns.NodeList.GetNode(node.node_index)
1746 mob = ns.cppyy.gbl.hasMobilityModel(ns3_node)
1747 if not mob:
1748 return False
1749 if self.node_drag_state is None:
1750 return False
1751 devpos = self.canvas.get_window().get_device_position(event.device)
1752 canvas_x, canvas_y = self.canvas.convert_from_pixels(devpos.x, devpos.y)
1753 dx = canvas_x - self.node_drag_state.canvas_x0
1754 dy = canvas_y - self.node_drag_state.canvas_y0
1755 pos = mob.GetPosition()
1756 pos.x = self.node_drag_state.sim_x0 + transform_distance_canvas_to_simulation(dx)
1757 pos.y = self.node_drag_state.sim_y0 + transform_distance_canvas_to_simulation(dy)
1758 # print "SetPosition(%G, %G)" % (pos.x, pos.y)
1759 mob.SetPosition(pos)
1760 node.set_position(*transform_point_simulation_to_canvas(pos.x, pos.y))
1761 finally:
1762 self.simulation.lock.release()
1763 return True
1764
1765 def end_node_drag(self, node):
1766 if self.node_drag_state is None:
1767 return
1768 node.canvas_item.disconnect(self.node_drag_state.motion_signal)
1769 self.node_drag_state = None
1770
1771 def popup_node_menu(self, node, event):
1772 menu = Gtk.Menu()
1773 self.emit("populate-node-menu", node, menu)
1774 menu.popup_at_pointer(event)
1775
1776 def _update_ipython_selected_node(self):
1777 # If we are running under ipython -gthread, make this new
1778 # selected node available as a global 'selected_node'
1779 # variable.
1780 try:
1781 __IPYTHON__
1782 except NameError:
1783 pass
1784 else:
1785 if self.selected_node is None:
1786 ns3_node = None
1787 else:
1788 self.simulation.lock.acquire()
1789 try:
1790 ns3_node = ns.NodeList.GetNode(self.selected_node.node_index)
1791 finally:
1792 self.simulation.lock.release()
1793 self.ipython.updateNamespace({"selected_node": ns3_node})
1794
1795 def select_node(self, node):
1796 if isinstance(node, ns.Node):
1797 node = self.nodes[node.GetId()]
1798 elif isinstance(node, int):
1799 node = self.nodes[node]
1800 elif isinstance(node, Node):
1801 pass
1802 elif node is None:
1803 pass
1804 else:
1805 raise TypeError("expected None, int, viz.Node or ns.Node, not %r" % node)
1806
1807 if node is self.selected_node:
1808 return
1809
1810 if self.selected_node is not None:
1811 self.selected_node.selected = False
1812 self.selected_node = node
1813 if self.selected_node is not None:
1814 self.selected_node.selected = True
1815
1816 if self._show_transmissions_mode == ShowTransmissionsMode.SELECTED:
1817 if self.selected_node is None:
1818 self.simulation.set_nodes_of_interest([])
1819 else:
1820 self.simulation.set_nodes_of_interest([self.selected_node.node_index])
1821
1822 self._update_ipython_selected_node()
1823
1824 def add_information_window(self, info_win):
1825 self.information_windows.append(info_win)
1826 self.simulation.lock.acquire()
1827 try:
1828 info_win.update()
1829 finally:
1830 self.simulation.lock.release()
1831
1832 def remove_information_window(self, info_win):
1833 self.information_windows.remove(info_win)
1834
1835 def _canvas_tooltip_cb(self, canvas, x, y, keyboard_mode, tooltip):
1836 # print "tooltip query: ", x, y
1837 hadj = self._scrolled_window.get_hadjustment()
1838 vadj = self._scrolled_window.get_vadjustment()
1839 x, y = self.canvas.convert_from_pixels(hadj.get_value() + x, vadj.get_value() + y)
1840 item = self.canvas.get_item_at(x, y, True)
1841 # print "items at (%f, %f): %r | keyboard_mode=%r" % (x, y, item, keyboard_mode)
1842 if not item:
1843 return False
1844 while item is not None:
1845 obj = getattr(item, "pyviz_object", None)
1846 if obj is not None:
1847 obj.tooltip_query(tooltip)
1848 return True
1849 item = item.props.parent
1850 return False
1851
1852 def _get_export_file_name(self):
1853 sel = Gtk.FileChooserNative.new(
1854 "Save...", self.canvas.get_toplevel(), Gtk.FileChooserAction.SAVE, "_Save", "_Cancel"
1855 )
1856 sel.set_local_only(True)
1857 sel.set_do_overwrite_confirmation(True)
1858 sel.set_current_name("Unnamed.pdf")
1859
1860 filter = Gtk.FileFilter()
1861 filter.set_name("Embedded PostScript")
1862 filter.add_mime_type("image/x-eps")
1863 sel.add_filter(filter)
1864
1865 filter = Gtk.FileFilter()
1866 filter.set_name("Portable Document Graphics")
1867 filter.add_mime_type("application/pdf")
1868 sel.add_filter(filter)
1869
1870 filter = Gtk.FileFilter()
1871 filter.set_name("Scalable Vector Graphics")
1872 filter.add_mime_type("image/svg+xml")
1873 sel.add_filter(filter)
1874
1875 resp = sel.run()
1876 if resp != Gtk.ResponseType.ACCEPT:
1877 sel.destroy()
1878 return None
1879
1880 file_name = sel.get_filename()
1881 sel.destroy()
1882 return file_name
1883
1884 def _take_screenshot(self, dummy_button):
1885 # print "Cheese!"
1886 file_name = self._get_export_file_name()
1887 if file_name is None:
1888 return
1889
1890 # figure out the correct bounding box for what is visible on screen
1891 x1 = self._scrolled_window.get_hadjustment().get_value()
1892 y1 = self._scrolled_window.get_vadjustment().get_value()
1893 x2 = x1 + self._scrolled_window.get_hadjustment().get_page_size()
1894 y2 = y1 + self._scrolled_window.get_vadjustment().get_page_size()
1895 bounds = GooCanvas.CanvasBounds()
1896 bounds.x1, bounds.y1 = self.canvas.convert_from_pixels(x1, y1)
1897 bounds.x2, bounds.y2 = self.canvas.convert_from_pixels(x2, y2)
1898 dest_width = bounds.x2 - bounds.x1
1899 dest_height = bounds.y2 - bounds.y1
1900 # print bounds.x1, bounds.y1, " -> ", bounds.x2, bounds.y2
1901
1902 dummy, extension = os.path.splitext(file_name)
1903 extension = extension.lower()
1904 if extension == ".eps":
1905 surface = cairo.PSSurface(file_name, dest_width, dest_height)
1906 elif extension == ".pdf":
1907 surface = cairo.PDFSurface(file_name, dest_width, dest_height)
1908 elif extension == ".svg":
1909 surface = cairo.SVGSurface(file_name, dest_width, dest_height)
1910 else:
1911 dialog = Gtk.MessageDialog(
1912 parent=self.canvas.get_toplevel(),
1913 flags=Gtk.DialogFlags.DESTROY_WITH_PARENT,
1914 type=Gtk.MessageType.ERROR,
1915 buttons=Gtk.ButtonsType.OK,
1916 message_format="Unknown extension '%s' (valid extensions are '.eps', '.svg', and '.pdf')"
1917 % (extension,),
1918 )
1919 dialog.run()
1920 dialog.destroy()
1921 return
1922
1923 # draw the canvas to a printing context
1924 cr = cairo.Context(surface)
1925 cr.translate(-bounds.x1, -bounds.y1)
1926 self.canvas.render(cr, bounds, self.zoom.get_value())
1927 cr.show_page()
1928 surface.finish()
1929
1930 def set_follow_node(self, node):
1931 if isinstance(node, ns.Node):
1932 node = self.nodes[node.GetId()]
1933 self.follow_node = node
1934
1935 def _start_shell(self, dummy_button):
1936 if self.shell_window is not None:
1937 self.shell_window.present()
1938 return
1939
1940 self.shell_window = Gtk.Window()
1941 self.shell_window.set_size_request(750, 550)
1942 self.shell_window.set_resizable(True)
1943 scrolled_window = Gtk.ScrolledWindow()
1944 scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
1945 self.ipython = ipython_view.IPythonView()
1946 self.ipython.modify_font(Pango.FontDescription(SHELL_FONT))
1947 self.ipython.set_wrap_mode(Gtk.WrapMode.CHAR)
1948 self.ipython.show()
1949 scrolled_window.add(self.ipython)
1950 scrolled_window.show()
1951 self.shell_window.add(scrolled_window)
1952 self.shell_window.show()
1953 self.shell_window.connect("destroy", self._on_shell_window_destroy)
1954
1955 self._update_ipython_selected_node()
1956 self.ipython.updateNamespace({"viz": self})
1957
1958 def _on_shell_window_destroy(self, window):
1959 self.shell_window = None
1960
1961
1962initialization_hooks = []
1963
1964
1965def add_initialization_hook(hook, *args):
1966 """
1967 Adds a callback to be called after
1968 the visualizer is initialized, like this::
1969 initialization_hook(visualizer, *args)
1970 """
1971 global initialization_hooks
1972 initialization_hooks.append((hook, args))
1973
1974
1975def set_bounds(x1, y1, x2, y2):
1976 assert x2 > x1
1977 assert y2 > y1
1978
1979 def hook(viz):
1980 cx1, cy1 = transform_point_simulation_to_canvas(x1, y1)
1981 cx2, cy2 = transform_point_simulation_to_canvas(x2, y2)
1982 viz.canvas.set_bounds(cx1, cy1, cx2, cy2)
1983
1984 add_initialization_hook(hook)
1985
1986
1987def start():
1988 assert Visualizer.INSTANCE is None
1989 if _import_error is not None:
1990 import sys
1991
1992 print("No visualization support (%s)." % (str(_import_error),), file=sys.stderr)
1993 ns.Simulator.Run()
1994 return
1995 load_plugins()
1996 viz = Visualizer()
1997 for hook, args in initialization_hooks:
1998 GLib.idle_add(hook, viz, *args)
1999 ns.Packet.EnablePrinting()
2000 viz.start()
static bool IsFinished()
Check if the simulation should finish.
Definition: simulator.cc:171
PyVizObject class.
Definition: base.py:10
def set_position(self, x, y)
Initializer function.
Definition: core.py:590
def __init__(self, channel)
Initializer function.
Definition: core.py:570
def get_position(self)
Initializer function.
Definition: core.py:605
links
list of links
Definition: core.py:588
Node class.
Definition: core.py:95
svg_align_y
svg align Y
Definition: core.py:165
visualizer
visualier object
Definition: core.py:150
def set_label(self, label)
Set a label for the node.
Definition: core.py:217
def add_link(self, link)
Add link function.
Definition: core.py:525
def set_svg_icon(self, file_base_name, width=None, height=None, align_x=0.5, align_y=0.5)
Set a background SVG icon for the node.
Definition: core.py:171
def get_position(self)
Get position function.
Definition: core.py:484
_highlighted
is highlighted
Definition: core.py:157
def _set_selected(self, value)
Set selected function.
Definition: core.py:344
_label_canvas_item
label canvas
Definition: core.py:167
property highlighted
highlighted property
Definition: core.py:386
def remove_link(self, link)
Remove link function.
Definition: core.py:536
def on_leave_notify_event(self, view, target, event)
On Leave event handle.
Definition: core.py:332
svg_item
svg item
Definition: core.py:163
def _update_svg_position(self, x, y)
Update svg position.
Definition: core.py:230
def __init__(self, visualizer, node_index)
Initialize function.
Definition: core.py:142
_has_mobility
has mobility model
Definition: core.py:155
def _get_selected(self)
Get selected function.
Definition: core.py:355
def on_enter_notify_event(self, view, target, event)
On Enter event handle.
Definition: core.py:318
def set_color(self, color)
Set color function.
Definition: core.py:506
def _get_highlighted(self)
Get highlighted function.
Definition: core.py:377
_selected
is selected
Definition: core.py:156
def _update_position(self)
Update position function.
Definition: core.py:496
def has_mobility(self)
Has mobility function.
Definition: core.py:548
svg_align_x
svg align X
Definition: core.py:164
def set_position(self, x, y)
Set position function.
Definition: core.py:444
def tooltip_query(self, tooltip)
Query tooltip.
Definition: core.py:245
def set_size(self, size)
Set size function.
Definition: core.py:388
canvas_item
canvas item
Definition: core.py:152
def _update_appearance(self)
Update the node aspect to reflect the selected/highlighted state.
Definition: core.py:399
def _set_highlighted(self, value)
Set highlighted function.
Definition: core.py:366
node_index
node index
Definition: core.py:151
ShowTransmissionsMode.
Definition: core.py:739
SimulationThread.
Definition: core.py:657
pause_messages
pause messages
Definition: core.py:688
def run(self)
Initializer function.
Definition: core.py:704
def set_nodes_of_interest(self, nodes)
Set nodes of interest function.
Definition: core.py:690
def __init__(self, viz)
Initializer function.
Definition: core.py:672
sim_helper
helper function
Definition: core.py:687
viz
Visualizer object.
Definition: core.py:681
Axes class.
Definition: hud.py:9
SvgItem class.
Definition: svgitem.py:9
def start()
Definition: core.py:1987
#define list