apps: Add GPSD app.
Once enabled, running a GPSD client (for example, cgps) on the node can obtain the current position and motion information of the node.
Change-Id: Ib88424cc7fe07898de5b37c1699e170bf89e58a5
diff --git a/examples/wifi/adhoc.py b/examples/wifi/adhoc.py
index b9be9c2..de5918e 100644
--- a/examples/wifi/adhoc.py
+++ b/examples/wifi/adhoc.py
@@ -1,6 +1,6 @@
# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
#
-# Copyright (C) 2015-2021, The University of Memphis,
+# Copyright (C) 2015-2025, The University of Memphis,
# Arizona Board of Regents,
# Regents of the University of California.
#
@@ -21,6 +21,7 @@
# along with Mini-NDN, e.g., in COPYING.md file.
# If not, see <http://www.gnu.org/licenses/>.
+from time import sleep
from mininet.log import setLogLevel, info
from minindn.wifi.minindnwifi import MinindnAdhoc
from minindn.util import MiniNDNWifiCLI
@@ -28,8 +29,8 @@
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
+
+# This experiment uses the topology defined in adhoc-topology.conf 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')
@@ -60,7 +61,7 @@
# Start the CLI
MiniNDNWifiCLI(ndnwifi.net)
- ndnwifi.net.stop()
+ ndnwifi.stop()
ndnwifi.cleanUp()
if __name__ == '__main__':
diff --git a/examples/wifi/adhoc_gpsd.py b/examples/wifi/adhoc_gpsd.py
new file mode 100644
index 0000000..4d52c83
--- /dev/null
+++ b/examples/wifi/adhoc_gpsd.py
@@ -0,0 +1,67 @@
+# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
+#
+# Copyright (C) 2015-2025, 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 time import sleep
+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.apps.gpsd import Gpsd
+# This experiment uses the topology defined in adhoc-topology.conf and
+# is intended to test whether app Gpsd can work with Mini-NDN.
+def runExperiment():
+ setLogLevel('info')
+
+ info("Starting network\n")
+ ndnwifi = MinindnAdhoc()
+ a = ndnwifi.net["sta1"]
+ b = ndnwifi.net["sta2"]
+
+ ndnwifi.start()
+
+ info("Starting gpsd\n")
+
+ AppManager(ndnwifi, ndnwifi.net.stations, Gpsd, lat=35.11908, lon=-89.93778, altitude=200, update_interval=0.2)
+
+ info("Starting NFD\n")
+ AppManager(ndnwifi, ndnwifi.net.stations, Nfd)
+
+ sleep(1)
+
+ # run cgps in the Xterm terminal to check gps info
+ for node in [a,b]:
+ node.cmd(f"xterm -T '{node.name}' -e 'cgps' &")
+
+ # Start the CLI
+ MiniNDNWifiCLI(ndnwifi.net)
+
+ # Stop the network and clean up
+ ndnwifi.stop()
+ ndnwifi.cleanUp()
+
+if __name__ == '__main__':
+ try:
+ runExperiment()
+ except Exception as e:
+ MinindnAdhoc.handleException()
diff --git a/minindn/apps/gpsd.py b/minindn/apps/gpsd.py
new file mode 100644
index 0000000..7ec79d0
--- /dev/null
+++ b/minindn/apps/gpsd.py
@@ -0,0 +1,237 @@
+# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
+#
+# Copyright (C) 2015-2025, 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/>.
+
+import threading
+import math
+import time
+import datetime
+from typing import Optional, Tuple
+
+from minindn.apps.application import Application
+
+
+class Gpsd(Application):
+ """
+ A class that simulates a GPS device and provides GPS data in NMEA format.
+ This class can generate NMEA GGA and VTG sentences and feeds the GPS data to a GPSD server.
+
+ To use this class, you need to install gpsd and netcat (nc) for communication.
+ """
+
+ def __init__(self, node, lat: float = 0.0, lon: float = 0.0, altitude: float = 0.0, update_interval: float = 0.2) -> None:
+ """
+ Initialize the GPSd application for a node.
+
+ :param node: The node for which the GPS data will be provided.
+ :param lat: Latitude of the point (0, 0, 0).
+ :param lon: Longitude of the point (0, 0, 0).
+ :param altitude: Altitude of the point (0, 0, 0).
+ :param update_interval: The time interval in seconds to send gps data to gpsd, and it should be more than 0.2
+ """
+ Application.__init__(self, node)
+ self.lat = lat
+ self.lon = lon
+ self.altitude = altitude
+ self.update_interval = update_interval
+ self.stop_event = threading.Event()
+ self.location_thread = None
+
+
+ def calculate_coordinates(self, offset_lat: float, offset_lon: float, altitude_offset: float) -> Tuple[float, float, float]:
+ """
+ Calculate the coordinates of the target point.
+
+ :param offset_lat: Latitude offset (unit: meters)
+ :param offset_lon: Longitude offset (unit: meters)
+ :param altitude_offset: Altitude offset (unit: meters)
+ :return: New GPS coordinates (latitude, longitude, altitude)
+ """
+ # Each degree of latitude is approximately 111 kilometers
+ lat_offset_deg = offset_lat / 111000.0 # Convert to degrees
+ # Distance per degree of longitude changes based on latitude
+ lon_offset_deg = offset_lon / (111000.0 * math.cos(math.radians(self.lat))) # Convert to degrees
+ # Calculate the latitude, longitude, and altitude of the target point
+ lat2 = self.lat + lat_offset_deg
+ lon2 = self.lon + lon_offset_deg
+ alt2 = self.altitude + altitude_offset
+
+ return lat2, lon2, alt2
+
+ @staticmethod
+ def nmea_checksum(sentence: str) -> str:
+ """
+ Calculate the checksum for an NMEA sentence.
+ :param sentence: The NMEA sentence to calculate the checksum for.
+ :return: The checksum for the sentence.
+ """
+ checksum = 0
+ for char in sentence:
+ checksum ^= ord(char)
+ return f"{checksum:02X}"
+
+ @staticmethod
+ def generate_vtg_sentence(vx: float, vy: float) -> str:
+ """
+ Generate a NMEA VTG sentence based on a 2D velocity vector.
+
+ Parameters:
+ vx (float): Velocity in the x-direction (eastward, in m/s)
+ vy (float): Velocity in the y-direction (northward, in m/s)
+
+ Returns:
+ str: The corresponding NMEA VTG sentence
+ """
+ # Calculate ground speed (horizontal speed)
+ ground_speed = math.sqrt(vx**2 + vy**2) # Ground speed in m/s
+ ground_speed_knots = ground_speed * 1.94384 # Convert speed to knots
+ ground_speed_kmh = ground_speed * 3.6 # Convert speed to km/h
+ # Calculate heading angle (relative to true north, clockwise)
+ angle = math.atan2(vx, vy) # atan2(y, x)
+ true_course = (math.degrees(angle) + 360) % 360 # Normalize angle to 0° - 360°
+ # Create the VTG NMEA sentence
+ nmea_sentence = f"GPVTG,{true_course:.1f},T,,M,{ground_speed_knots:.2f},N,{ground_speed_kmh:.2f},K"
+
+ nmea_sentence_with_checksum = f"${nmea_sentence}*{Gpsd.nmea_checksum(nmea_sentence)}"
+
+ return nmea_sentence_with_checksum
+
+ @staticmethod
+ def generate_gga_sentence(lat: float, lon: float, altitude: float, utc_time: Optional[str] = None) -> str:
+ """
+ Convert latitude, longitude, and altitude into NMEA format data.
+
+ Parameters:
+ lat (float): Latitude (unit: degrees)
+ lon (float): Longitude (unit: degrees)
+ altitude (float): Altitude (unit: meters)
+ utc_time (str): UTC Time in 'hhmmss.sss' format, optional
+
+ Returns:
+ str: Formatted NMEA data sentence
+ """
+ lat_direction = 'N' if lat >= 0 else 'S'
+ lon_direction = 'E' if lon >= 0 else 'W'
+
+ lat_deg = int(abs(lat))
+ lat_min = (abs(lat) - lat_deg) * 60
+ lon_deg = int(abs(lon))
+ lon_min = (abs(lon) - lon_deg) * 60
+
+ if utc_time is None:
+ utc_time = datetime.datetime.now(datetime.timezone.utc).strftime("%H%M%S.%f")[:-3]# Generate UTC time
+
+ nmea_sentence = f"GPGGA,{utc_time},{lat_deg:02d}{lat_min:07.4f},{lat_direction},{lon_deg:03d}{lon_min:07.4f},{lon_direction},1,12,1.0,{altitude:.1f},M,0.0,M,,"
+
+ nmea_sentence = f"${nmea_sentence}*{Gpsd.nmea_checksum(nmea_sentence)}"
+
+ return nmea_sentence
+
+ @staticmethod
+ def generate_rmc_sentence(lat: float, lon: float, vx: float, vy: float, utc_time: Optional[str] = None, date: Optional[str] = None) -> str:
+ """
+ Generate an NMEA GPRMC sentence using vx and vy (speed components along X and Y axes).
+
+ Parameters:
+ lat (float): Latitude (degrees)
+ lon (float): Longitude (degrees)
+ vx (float): Speed along the X-axis (knots)
+ vy (float): Speed along the Y-axis (knots)
+ utc_time (str, optional): UTC time in 'hhmmss.sss' format
+ date (str, optional): UTC date in 'ddmmyy' format
+
+ Returns:
+ str: Formatted NMEA GPRMC sentence
+ """
+ # Calculate speed and course
+ speed = math.sqrt(vx**2 + vy**2) # Speed (ground speed)
+ course = math.degrees(math.atan2(vy, vx)) # Course (direction in degrees)
+
+ # Normalize course to be within 0 to 360 degrees
+ if course < 0:
+ course += 360
+
+ # Convert latitude and longitude to NMEA format
+ lat_direction = 'N' if lat >= 0 else 'S'
+ lon_direction = 'E' if lon >= 0 else 'W'
+
+ lat_deg = int(abs(lat))
+ lat_min = (abs(lat) - lat_deg) * 60
+ lon_deg = int(abs(lon))
+ lon_min = (abs(lon) - lon_deg) * 60
+
+ # Use current utc_time and date if not provided
+ if utc_time is None or date is None:
+ now = datetime.datetime.now(datetime.timezone.utc)
+ if utc_time is None:
+ utc_time = now.strftime("%H%M%S.%f")[:-3] # hhmmss.sss
+ if date is None:
+ date = now.strftime("%d%m%y") # ddmmyy
+
+ # Construct NMEA sentence
+ status = "A" # A = Active, V = Void (No fix)
+ nmea_sentence = f"GPRMC,{utc_time},{status},{lat_deg:02d}{lat_min:07.4f},{lat_direction},{lon_deg:03d}{lon_min:07.4f},{lon_direction},{speed:.1f},{course:.1f},{date},,,A"
+
+ nmea_sentence = f"${nmea_sentence}*{Gpsd.nmea_checksum(nmea_sentence)}"
+
+ return nmea_sentence
+
+ def __feedGPStoGPSD(self, node) -> None:
+ """
+ Continuously feed GPS data to the GPSD server.
+ """
+ current_position = node.position
+ current_time_seconds = time.monotonic()
+ while not self.stop_event.is_set():
+ time.sleep(self.update_interval)
+ lat, lon, altitude = self.calculate_coordinates(node.position[0], node.position[1], node.position[2])
+ gga_sentence = self.generate_gga_sentence(lat, lon, altitude)
+
+ tmp_position = node.position
+ tmp_time_seconds = time.monotonic()
+ vx = (tmp_position[0] - current_position[0]) / (tmp_time_seconds - current_time_seconds)
+ vy = (tmp_position[1] - current_position[1]) / (tmp_time_seconds - current_time_seconds)
+ current_position = tmp_position
+ current_time_seconds = tmp_time_seconds
+ rmc_sentence = self.generate_rmc_sentence(lat, lon, vx, vy)
+ vtg_sentence = self.generate_vtg_sentence(vx, vy)
+
+ cmd = f"echo '{gga_sentence}\n{rmc_sentence}\n{vtg_sentence}\n' | nc -u -w 1 127.0.0.1 7150"
+ process = node.popen(cmd, shell=True)
+
+ def start(self) -> None:
+ """
+ Start a thread to periodically send GPS data for the node.
+ """
+ Application.start(self, command="gpsd -n udp://127.0.0.1:7150")
+
+ self.location_thread = threading.Thread(target=self.__feedGPStoGPSD, args=(self.node,))
+ self.location_thread.start()
+
+ def stop(self) -> None:
+ """
+ Stop all the __feedGPStoGPSD threads
+ """
+ if not self.stop_event.is_set():
+ self.stop_event.set()
+ self.location_thread.join()
+ Application.stop(self)
\ No newline at end of file
diff --git a/minindn/wifi/minindnwifi.py b/minindn/wifi/minindnwifi.py
index 8eb9b2c..6a6ad88 100644
--- a/minindn/wifi/minindnwifi.py
+++ b/minindn/wifi/minindnwifi.py
@@ -411,7 +411,7 @@
continue
key = param.split('=')[0]
value = param.split('=')[1]
- if key in ['range']:
+ if key in ['range', 'min_v', 'max_v']:
value = int(value)
params[key] = value
# ip6 address for each station using id
diff --git a/topologies/wifi/adhoc-topology.conf b/topologies/wifi/adhoc-topology.conf
index 2d220aa..80fed46 100644
--- a/topologies/wifi/adhoc-topology.conf
+++ b/topologies/wifi/adhoc-topology.conf
@@ -2,7 +2,7 @@
# 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
+sta2: position=50,0,50 range=116 min_x=0 max_x=50 min_y=0 max_y=50 min_v=5 max_v=5
# loss is optional, default is 0; and it won't work with mobility
# https://github.com/intrig-unicamp/mininet-wifi/issues/53
diff --git a/util/pkgdep/debian-like.sh b/util/pkgdep/debian-like.sh
index 52fb9a5..fbab0ce 100644
--- a/util/pkgdep/debian-like.sh
+++ b/util/pkgdep/debian-like.sh
@@ -1,6 +1,6 @@
# -*- Mode:bash; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
#
-# Copyright (C) 2015-2021, The University of Memphis,
+# Copyright (C) 2015-2025, The University of Memphis,
# Arizona Board of Regents,
# Regents of the University of California.
#
@@ -26,6 +26,7 @@
build-essential
ca-certificates
git
+ gpsd
libboost-atomic-dev
libboost-chrono-dev
libboost-date-time-dev
diff --git a/util/pkgdep/fedora.sh b/util/pkgdep/fedora.sh
index a6876e8..5a2056e 100644
--- a/util/pkgdep/fedora.sh
+++ b/util/pkgdep/fedora.sh
@@ -1,6 +1,6 @@
# -*- Mode:bash; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
#
-# Copyright (C) 2015-2022, The University of Memphis,
+# Copyright (C) 2015-2025, The University of Memphis,
# Arizona Board of Regents,
# Regents of the University of California.
#
@@ -33,6 +33,7 @@
boost-devel
ca-certificates
gcc-c++
+ gpsd
libpcap-devel
openssl-devel
python3-pip