Add MinindnAdhoc and ability to configure mobility in configuration file

Change-Id: I7feec7616ed7cf9f0ebb51b4e8c704201931abc9
diff --git a/examples/wifi/adhoc.py b/examples/wifi/adhoc.py
new file mode 100644
index 0000000..b9be9c2
--- /dev/null
+++ b/examples/wifi/adhoc.py
@@ -0,0 +1,70 @@
+# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
+#
+# Copyright (C) 2015-2021, The University of Memphis,
+#                          Arizona Board of Regents,
+#                          Regents of the University of California.
+#
+# This file is part of Mini-NDN.
+# See AUTHORS.md for a complete list of Mini-NDN authors and contributors.
+#
+# Mini-NDN is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Mini-NDN is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mini-NDN, e.g., in COPYING.md file.
+# If not, see <http://www.gnu.org/licenses/>.
+
+from mininet.log import setLogLevel, info
+from minindn.wifi.minindnwifi import MinindnAdhoc
+from minindn.util import MiniNDNWifiCLI
+from minindn.apps.app_manager import AppManager
+from minindn.apps.nfd import Nfd
+from minindn.helpers.nfdc import Nfdc
+from minindn.helpers.ndnping import NDNPing
+from time import sleep
+# This experiment uses the singleap topology and is intended to be a basic
+# test case where we see if two nodes can send interests to each other.
+def runExperiment():
+    setLogLevel('info')
+
+    info("Starting network\n")
+    ndnwifi = MinindnAdhoc()
+    a = ndnwifi.net["sta1"]
+    b = ndnwifi.net["sta2"]
+
+    ndnwifi.start()
+
+
+    info("Starting NFD\n")
+    AppManager(ndnwifi, ndnwifi.net.stations, Nfd)
+
+    info("Starting pingserver...\n")
+    NDNPing.startPingServer(b, "/example")
+
+    # multicast face for wireless communication
+    multicastFaceId = Nfdc.getFaceId(a, "[01:00:5e:00:17:aa]", None, "ether", 6363)
+    # print(multicastFaceId)
+    Nfdc.registerRoute(a, "/example", multicastFaceId, 100)
+
+    info("Starting ping...\n")
+    NDNPing.ping(a, "/example", nPings=10)
+
+    sleep(10)
+
+    # Start the CLI
+    MiniNDNWifiCLI(ndnwifi.net)
+    ndnwifi.net.stop()
+    ndnwifi.cleanUp()
+
+if __name__ == '__main__':
+    try:
+        runExperiment()
+    except Exception as e:
+        MinindnAdhoc.handleException()
\ No newline at end of file
diff --git a/minindn/wifi/minindnwifi.py b/minindn/wifi/minindnwifi.py
index ba3c434..bf290b8 100644
--- a/minindn/wifi/minindnwifi.py
+++ b/minindn/wifi/minindnwifi.py
@@ -32,10 +32,13 @@
 from mn_wifi.topo import Topo as Topo_WiFi
 from mn_wifi.net import Mininet_wifi
 from mn_wifi.link import WirelessLink
+from mn_wifi.link import wmediumd, adhoc
 
 from minindn.minindn import Minindn
 from minindn.helpers.nfdc import Nfdc
 
+import ast
+
 class MinindnWifi(Minindn):
     """ Class for handling default args, Mininet-wifi object and home directories """
     def __init__(self, parser=argparse.ArgumentParser(), topo=None, topoFile=None, noTopo=False,
@@ -82,6 +85,10 @@
         else:
             self.net = Mininet_wifi(ifb=self.args.ifb, link=link, **mininetParams)
 
+        # process mobility if specified
+        if not noTopo:
+            self.processMobility(self.topoFile)
+
         # Prevents crashes running mixed topos
         nodes = self.net.stations + self.net.hosts + self.net.cars
         self.initParams(nodes)
@@ -93,7 +100,7 @@
                 Minindn.ndnSecurityDisabled = '/dummy/KEY/-%9C%28r%B8%AA%3B%60' in output.decode("utf-8")
                 info('Dummy key chain patch is installed in ndn-cxx. Security will be disabled.\n')
             else:
-                debug(error)
+                debug(error + "\n")
         except:
             pass
 
@@ -116,24 +123,31 @@
         parser.add_argument('--mobility',action='store_true',dest='mobility',default=False,
                             help='Enable custom mobility for topology (defined in topology file)')
 
-        parser.add_argument('--model-mob',action='store_true',dest='modelMob',default=False,
-                            help='Enable model mobility for topology (defined in topology file)')
-
         parser.add_argument('--ifb',action='store_true',dest='ifb',default=False,
                             help='Simulate delay on receiver-side by use of virtual IFB devices (see docs)')
 
         return parser
 
     @staticmethod
+    def convert_params(params):
+        converted_params = {}
+        for key, value in params.items():
+            try:
+                converted_params[key] = ast.literal_eval(value)
+            except (ValueError, SyntaxError):
+                converted_params[key] = value
+        return converted_params
+
+    @staticmethod
     def processTopo(topoFile):
         config = configparser.ConfigParser(delimiters=' ')
         config.read(topoFile)
         topo = Topo_WiFi()
 
         items = config.items('stations')
-        debug("Stations")
+        debug("Stations\n")
         for item in items:
-            debug(item[0].split(':'))
+            debug(str(item[0].split(':'))+"\n")
             name = item[0].split(':')[0]
             params = {}
             for param in item[1].split(' '):
@@ -148,21 +162,21 @@
             topo.addStation(name, **params)
 
         try:
-            debug("Switches")
+            debug("Switches\n")
             items = config.items('switches')
             for item in items:
-                debug(item[0].split(':'))
+                debug(str(item[0].split(':'))+"\n")
                 name = item[0].split(':')[0]
                 topo.addSwitch(name)
         except configparser.NoSectionError:
-            debug("Switches are optional")
+            debug("Switches are optional\n")
             pass
 
         try:
             debug("APs")
             items = config.items('accessPoints')
             for item in items:
-                debug(item[0].split(':'))
+                debug(str(item[0].split(':'))+"\n")
                 name = item[0].split(':')[0]
                 ap_params = {}
                 for param in item[1].split(' '):
@@ -175,14 +189,14 @@
                     ap_params[key] = value
                 topo.addAccessPoint(name, **ap_params)
         except configparser.NoSectionError:
-            debug("APs are optional")
+            debug("APs are optional\n")
             pass
 
         items = config.items('links')
         debug("Links")
         for item in items:
             link = item[0].split(':')
-            debug(link)
+            debug(str(link) + "\n")
             params = {}
             for param in item[1].split(' '):
                 if param == "_":
@@ -200,10 +214,10 @@
         faces = {}
         try:
             items = config.items('faces')
-            debug("Faces")
+            debug("Faces\n")
             for item in items:
                 face_a, face_b = item[0].split(':')
-                debug(item)
+                debug(str(item)+"\n")
                 cost = -1
                 for param in item[1].split(' '):
                     if param.split("=")[0] == 'cost':
@@ -214,11 +228,51 @@
                 else:
                     faces[face_a].append(face_info)
         except configparser.NoSectionError:
-            debug("Faces section is optional")
+            debug("Faces section is optional\n")
             pass
 
         return (topo, faces)
 
+    def processMobility(self, topoFile):
+        config = configparser.ConfigParser(delimiters=' ')
+        config.read(topoFile)
+
+        try:
+            debug("Mobility\n")
+            items = config.items('mobility')
+            if len(items) == 0:
+                return
+            params = {}
+            for param in items[0][1].split(' '):
+                if param == "_":
+                    continue
+                key = param.split('=')[0]
+                value = param.split('=')[1]
+                params[key] = value
+            params = self.convert_params(params)
+            self.startMobilityModel(**params)
+
+            items = config.items('stations')
+            debug("Start Mobility\n")
+            for item in items:
+                debug(str(item[0].split(':'))+"\n")
+                name = item[0].split(':')[0]
+                params = {}
+                for param in item[1].split(' '):
+                    if param == "_":
+                        continue
+                    key = param.split('=')[0]
+                    value = param.split('=')[1]
+                    if key in ['range']:
+                        value = int(value)
+                    params[key] = value
+                # by default, nodes are moving
+                if "moving" not in params or params["moving"] == "True":
+                    self.net.mobility(self.net[name],'start', time=0, **params)
+
+        except configparser.NoSectionError:
+            debug("Mobility section is optional\n")
+
     def startMobility(self, max_x=1000, max_y=1000, **kwargs):
         """ Method to run a basic mobility setup on your net"""
         self.net.plotGraph(max_x=max_x, max_y=max_y)
@@ -304,4 +358,141 @@
                     created_faces[nodeB].append(nodeALink)
         for station_name in batch_faces.keys():
             self.nfdcBatchProcessing(self.net[station_name], batch_faces[station_name])
-        return created_faces
\ No newline at end of file
+        return created_faces
+
+class MinindnAdhoc(MinindnWifi):
+    """
+    Class for ad hoc network of Mininet-wifi
+    Topology example: topologies/wifi/adhoc-topology.conf
+    The link type is wmediumd by default
+    """
+    def __init__(self, parser=argparse.ArgumentParser(), topo=None, topoFile=None, noTopo=False,
+                 link=wmediumd, workDir=None, **mininetParams):
+        # call parent constructor
+        super().__init__(parser, topo, topoFile, noTopo, link, workDir, **mininetParams)
+        if not self.topoFile:
+            info("Without topoFile, ad hoc links are not added to the network, and you need to"
+                 " add them manually. The example topology file can be found in"
+                 " topologies/wifi/adhoc-topology.conf\n")
+        else:
+            self.addAdhocLinks()
+
+    @staticmethod
+    def parseArgs(parent):
+        parser = argparse.ArgumentParser(prog='minindn-adhoc', parents=[parent], add_help=False)
+
+        # nargs='?' required here since optional argument
+        parser.add_argument('topoFile', nargs='?', default='/usr/local/etc/mini-ndn/adhoc-topology.conf',
+                            help='If no template_file is given, topologies/wifi/adhoc-topology.conf will be used.')
+
+        parser.add_argument('--work-dir', action='store', dest='workDir', default='/tmp/minindn',
+                            help='Specify the working directory; default is /tmp/minindn')
+
+        parser.add_argument('--result-dir', action='store', dest='resultDir', default=None,
+                            help='Specify the full path destination folder where experiment results will be moved')
+
+        parser.add_argument('--mobility',action='store_true',dest='mobility',default=False,
+                            help='Enable custom mobility for topology (defined in topology file)')
+
+        parser.add_argument('--ifb',action='store_true',dest='ifb',default=False,
+                            help='Simulate delay on receiver-side by use of virtual IFB devices (see docs)')
+
+        return parser
+
+    @staticmethod
+    def processTopo(topoFile):
+        config = configparser.ConfigParser(delimiters=' ')
+        config.read(topoFile)
+        topo = Topo_WiFi()
+
+        items = config.items('stations')
+        debug("Stations\n")
+        id = 0
+        for item in items:
+            debug(str(item[0].split(':'))+"\n")
+            name = item[0].split(':')[0]
+            params = {}
+            for param in item[1].split(' '):
+                    if param == "_":
+                        continue
+                    key = param.split('=')[0]
+                    value = param.split('=')[1]
+                    if key in ['range']:
+                        value = int(value)
+                    params[key] = value
+            # ip6 address for each station using id
+            if 'ip6' not in params:
+                params['ip6'] = 'fe80::{}'.format(id)
+            topo.addStation(name, **params)
+            id += 1
+
+        faces = {}
+        try:
+            items = config.items('faces')
+            debug("Faces\n")
+            for item in items:
+                face_a, face_b = item[0].split(':')
+                debug(str(item)+"\n")
+                cost = -1
+                for param in item[1].split(' '):
+                    if param.split("=")[0] == 'cost':
+                        cost = param.split("=")[1]
+                face_info = (face_b, int(cost))
+                if face_a not in faces:
+                    faces[face_a] = [face_info]
+                else:
+                    faces[face_a].append(face_info)
+        except configparser.NoSectionError:
+            debug("Faces section is optional\n")
+            pass
+
+        return (topo, faces)
+
+    """Add adhoc links to the network"""
+    # In the topo.py, all the links require two stations, but in adhoc topology, we need to add links for all the nodes.
+    def addAdhocLinks(self):
+        config = configparser.ConfigParser(delimiters=' ')
+        config.read(self.topoFile)
+
+        # read adhoc network parameters
+        # return if adhoc network is not defined
+        if 'adhocNetwork' not in config.sections():
+            info("Adhoc network is not defined in the topology file\n")
+            return
+
+        adhoc_params = config.items('adhocNetwork')
+        params = {}
+        for param in adhoc_params[0][1].split(' '):
+            if param == "_":
+                continue
+            key = param.split('=')[0]
+            value = param.split('=')[1]
+            if key in ['range']:
+                value = int(value)
+            params[key] = value
+        params = self.convert_params(params)
+
+        # return if ssid, mode, channel not defined in params
+        if 'ssid' not in params or 'mode' not in params or 'channel' not in params:
+            info("ssid, mode, channel not defined in adhoc network parameters\n")
+            return
+        networkParams = params
+
+        # add adhoc links
+        debug("Links\n")
+        items = config.items('stations')
+        for item in items:
+            debug(str(item[0].split(':'))+"\n")
+            name = item[0].split(':')[0]
+            params = {}
+            for param in item[1].split(' '):
+                    if param == "_":
+                        continue
+                    key = param.split('=')[0]
+                    value = param.split('=')[1]
+                    params[key] = value
+            params = self.convert_params(params)
+            # replace | with space in bitrates because space is not allowed in the configuration file
+            if 'bitrates' in params:
+                params['bitrates'] = params['bitrates'].replace("|", " ")
+            self.net.addLink(name, cls=adhoc, intf='{}-wlan0'.format(name), **networkParams, **params)
diff --git a/topologies/wifi/adhoc-topology.conf b/topologies/wifi/adhoc-topology.conf
new file mode 100644
index 0000000..2d220aa
--- /dev/null
+++ b/topologies/wifi/adhoc-topology.conf
@@ -0,0 +1,14 @@
+# Don't use ht_cap because it is not fully supported
+# For bitrates, replace space with "|" in bitrates because space is not allowed in the configuration file :"legacy-2.4 1" -> "legacy-2.4|1"
+[stations]
+sta1: position=0,0,0 range=116 min_x=-50 max_x=0 min_y=-50 max_y=0 bitrates=legacy-2.4|1 moving=false
+sta2: position=50,0,50 range=116 min_x=0 max_x=50 min_y=0 max_y=50
+
+# loss is optional, default is 0; and it won't work with mobility
+# https://github.com/intrig-unicamp/mininet-wifi/issues/53
+[adhocNetwork]
+adhoc: ssid=adhocNet mode=g channel=5
+
+[mobility]
+# Spaces are not allowed in the parameters.
+mobilityModel: time=0 model=RandomDirection min_x=-100 max_x=100 min_y=-100 max_y=100 seed=20
\ No newline at end of file