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