blob: 7626de1c04454316ef56213a70651b49544823fa [file] [log] [blame]
matianxing1992b95d1942025-01-28 15:04:02 -06001# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
2#
3# Copyright (C) 2015-2025, The University of Memphis,
4# Arizona Board of Regents,
5# Regents of the University of California.
6#
7# This file is part of Mini-NDN.
8# See AUTHORS.md for a complete list of Mini-NDN authors and contributors.
9#
10# Mini-NDN is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# Mini-NDN is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with Mini-NDN, e.g., in COPYING.md file.
22# If not, see <http://www.gnu.org/licenses/>.
23
24import threading
25import math
26import time
27import datetime
awlane749b07a2025-02-05 11:11:05 -060028from subprocess import TimeoutExpired, DEVNULL
matianxing1992b95d1942025-01-28 15:04:02 -060029from typing import Optional, Tuple
30
awlane749b07a2025-02-05 11:11:05 -060031from mininet.log import error
32
matianxing1992b95d1942025-01-28 15:04:02 -060033from minindn.apps.application import Application
awlane749b07a2025-02-05 11:11:05 -060034from minindn.util import getPopen
matianxing1992b95d1942025-01-28 15:04:02 -060035
36
37class Gpsd(Application):
38 """
39 A class that simulates a GPS device and provides GPS data in NMEA format.
40 This class can generate NMEA GGA and VTG sentences and feeds the GPS data to a GPSD server.
41
42 To use this class, you need to install gpsd and netcat (nc) for communication.
43 """
44
45 def __init__(self, node, lat: float = 0.0, lon: float = 0.0, altitude: float = 0.0, update_interval: float = 0.2) -> None:
46 """
47 Initialize the GPSd application for a node.
48
49 :param node: The node for which the GPS data will be provided.
50 :param lat: Latitude of the point (0, 0, 0).
51 :param lon: Longitude of the point (0, 0, 0).
52 :param altitude: Altitude of the point (0, 0, 0).
53 :param update_interval: The time interval in seconds to send gps data to gpsd, and it should be more than 0.2
54 """
55 Application.__init__(self, node)
56 self.lat = lat
57 self.lon = lon
58 self.altitude = altitude
59 self.update_interval = update_interval
60 self.stop_event = threading.Event()
61 self.location_thread = None
62
63
64 def calculate_coordinates(self, offset_lat: float, offset_lon: float, altitude_offset: float) -> Tuple[float, float, float]:
65 """
66 Calculate the coordinates of the target point.
67
68 :param offset_lat: Latitude offset (unit: meters)
69 :param offset_lon: Longitude offset (unit: meters)
70 :param altitude_offset: Altitude offset (unit: meters)
71 :return: New GPS coordinates (latitude, longitude, altitude)
72 """
73 # Each degree of latitude is approximately 111 kilometers
74 lat_offset_deg = offset_lat / 111000.0 # Convert to degrees
75 # Distance per degree of longitude changes based on latitude
76 lon_offset_deg = offset_lon / (111000.0 * math.cos(math.radians(self.lat))) # Convert to degrees
77 # Calculate the latitude, longitude, and altitude of the target point
78 lat2 = self.lat + lat_offset_deg
79 lon2 = self.lon + lon_offset_deg
80 alt2 = self.altitude + altitude_offset
81
82 return lat2, lon2, alt2
83
84 @staticmethod
85 def nmea_checksum(sentence: str) -> str:
86 """
87 Calculate the checksum for an NMEA sentence.
88 :param sentence: The NMEA sentence to calculate the checksum for.
89 :return: The checksum for the sentence.
90 """
91 checksum = 0
92 for char in sentence:
93 checksum ^= ord(char)
94 return f"{checksum:02X}"
95
96 @staticmethod
97 def generate_vtg_sentence(vx: float, vy: float) -> str:
98 """
99 Generate a NMEA VTG sentence based on a 2D velocity vector.
100
101 Parameters:
102 vx (float): Velocity in the x-direction (eastward, in m/s)
103 vy (float): Velocity in the y-direction (northward, in m/s)
104
105 Returns:
106 str: The corresponding NMEA VTG sentence
107 """
108 # Calculate ground speed (horizontal speed)
109 ground_speed = math.sqrt(vx**2 + vy**2) # Ground speed in m/s
110 ground_speed_knots = ground_speed * 1.94384 # Convert speed to knots
111 ground_speed_kmh = ground_speed * 3.6 # Convert speed to km/h
112 # Calculate heading angle (relative to true north, clockwise)
113 angle = math.atan2(vx, vy) # atan2(y, x)
114 true_course = (math.degrees(angle) + 360) % 360 # Normalize angle to 0° - 360°
115 # Create the VTG NMEA sentence
116 nmea_sentence = f"GPVTG,{true_course:.1f},T,,M,{ground_speed_knots:.2f},N,{ground_speed_kmh:.2f},K"
117
118 nmea_sentence_with_checksum = f"${nmea_sentence}*{Gpsd.nmea_checksum(nmea_sentence)}"
119
120 return nmea_sentence_with_checksum
121
122 @staticmethod
123 def generate_gga_sentence(lat: float, lon: float, altitude: float, utc_time: Optional[str] = None) -> str:
124 """
125 Convert latitude, longitude, and altitude into NMEA format data.
126
127 Parameters:
128 lat (float): Latitude (unit: degrees)
129 lon (float): Longitude (unit: degrees)
130 altitude (float): Altitude (unit: meters)
131 utc_time (str): UTC Time in 'hhmmss.sss' format, optional
132
133 Returns:
134 str: Formatted NMEA data sentence
135 """
136 lat_direction = 'N' if lat >= 0 else 'S'
137 lon_direction = 'E' if lon >= 0 else 'W'
138
139 lat_deg = int(abs(lat))
140 lat_min = (abs(lat) - lat_deg) * 60
141 lon_deg = int(abs(lon))
142 lon_min = (abs(lon) - lon_deg) * 60
143
144 if utc_time is None:
145 utc_time = datetime.datetime.now(datetime.timezone.utc).strftime("%H%M%S.%f")[:-3]# Generate UTC time
146
147 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,,"
148
149 nmea_sentence = f"${nmea_sentence}*{Gpsd.nmea_checksum(nmea_sentence)}"
150
151 return nmea_sentence
152
153 @staticmethod
154 def generate_rmc_sentence(lat: float, lon: float, vx: float, vy: float, utc_time: Optional[str] = None, date: Optional[str] = None) -> str:
155 """
156 Generate an NMEA GPRMC sentence using vx and vy (speed components along X and Y axes).
157
158 Parameters:
159 lat (float): Latitude (degrees)
160 lon (float): Longitude (degrees)
161 vx (float): Speed along the X-axis (knots)
162 vy (float): Speed along the Y-axis (knots)
163 utc_time (str, optional): UTC time in 'hhmmss.sss' format
164 date (str, optional): UTC date in 'ddmmyy' format
165
166 Returns:
167 str: Formatted NMEA GPRMC sentence
168 """
169 # Calculate speed and course
170 speed = math.sqrt(vx**2 + vy**2) # Speed (ground speed)
171 course = math.degrees(math.atan2(vy, vx)) # Course (direction in degrees)
172
173 # Normalize course to be within 0 to 360 degrees
174 if course < 0:
175 course += 360
176
177 # Convert latitude and longitude to NMEA format
178 lat_direction = 'N' if lat >= 0 else 'S'
179 lon_direction = 'E' if lon >= 0 else 'W'
180
181 lat_deg = int(abs(lat))
182 lat_min = (abs(lat) - lat_deg) * 60
183 lon_deg = int(abs(lon))
184 lon_min = (abs(lon) - lon_deg) * 60
185
186 # Use current utc_time and date if not provided
187 if utc_time is None or date is None:
188 now = datetime.datetime.now(datetime.timezone.utc)
189 if utc_time is None:
190 utc_time = now.strftime("%H%M%S.%f")[:-3] # hhmmss.sss
191 if date is None:
192 date = now.strftime("%d%m%y") # ddmmyy
193
194 # Construct NMEA sentence
195 status = "A" # A = Active, V = Void (No fix)
196 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"
197
198 nmea_sentence = f"${nmea_sentence}*{Gpsd.nmea_checksum(nmea_sentence)}"
199
200 return nmea_sentence
201
202 def __feedGPStoGPSD(self, node) -> None:
203 """
204 Continuously feed GPS data to the GPSD server.
205 """
206 current_position = node.position
207 current_time_seconds = time.monotonic()
208 while not self.stop_event.is_set():
209 time.sleep(self.update_interval)
210 lat, lon, altitude = self.calculate_coordinates(node.position[0], node.position[1], node.position[2])
211 gga_sentence = self.generate_gga_sentence(lat, lon, altitude)
212
213 tmp_position = node.position
214 tmp_time_seconds = time.monotonic()
215 vx = (tmp_position[0] - current_position[0]) / (tmp_time_seconds - current_time_seconds)
216 vy = (tmp_position[1] - current_position[1]) / (tmp_time_seconds - current_time_seconds)
217 current_position = tmp_position
218 current_time_seconds = tmp_time_seconds
219 rmc_sentence = self.generate_rmc_sentence(lat, lon, vx, vy)
220 vtg_sentence = self.generate_vtg_sentence(vx, vy)
221
222 cmd = f"echo '{gga_sentence}\n{rmc_sentence}\n{vtg_sentence}\n' | nc -u -w 1 127.0.0.1 7150"
223 process = node.popen(cmd, shell=True)
224
225 def start(self) -> None:
226 """
227 Start a thread to periodically send GPS data for the node.
228 """
awlane749b07a2025-02-05 11:11:05 -0600229 try:
230 gpsd_present = getPopen(self.node, "gpsd -V", stdout=DEVNULL, stderr=DEVNULL).wait(timeout=5)
231 nc_present = getPopen(self.node, "nc -h", stdout=DEVNULL, stderr=DEVNULL).wait(timeout=5)
232 if gpsd_present > 0:
233 error("The application is currently missing gpsd as a dependency. This must be installed manually.\n")
234 elif nc_present > 0:
235 error("The application is currently missing netcat as a dependency. This must be installed manually.\n")
236 if (gpsd_present + nc_present) > 0:
237 raise RuntimeError("Missing dependency for gpsd helper")
238 except TimeoutExpired as e:
239 error("The application is unable to validate if you have all required dependencies for gpsd.\n")
240 raise e
241
matianxing1992b95d1942025-01-28 15:04:02 -0600242 Application.start(self, command="gpsd -n udp://127.0.0.1:7150")
243
244 self.location_thread = threading.Thread(target=self.__feedGPStoGPSD, args=(self.node,))
245 self.location_thread.start()
246
247 def stop(self) -> None:
248 """
249 Stop all the __feedGPStoGPSD threads
250 """
251 if not self.stop_event.is_set():
252 self.stop_event.set()
253 self.location_thread.join()
254 Application.stop(self)