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