|
1 | 1 | #!/usr/bin/env python
|
2 | 2 | '''
|
3 | 3 | support for a GCS attached DGPS system
|
| 4 | +Set the ip, port and conntype settings to your requiered connection |
| 5 | +The module supports TCP and UDP connections (conntype) |
| 6 | +
|
| 7 | +For using with Emlid Reach use TCP connection, set the IP to the IP address of the Reach base |
| 8 | +
|
| 9 | +Configure ReachView like so: |
| 10 | +
|
| 11 | +Base mode: |
| 12 | +TCP, Role: Server, address: localhost, Port: 9000 |
| 13 | +Set the RTCM Messages to 10Hz and enable any that are needed. |
4 | 14 | '''
|
5 | 15 |
|
6 |
| -import socket, errno |
| 16 | +import socket, errno, threading |
| 17 | +import time |
| 18 | +from MAVProxy.modules.lib import rtcm3 |
7 | 19 | from pymavlink import mavutil
|
8 | 20 | from MAVProxy.modules.lib import mp_module
|
| 21 | +from MAVProxy.modules.lib import mp_settings |
9 | 22 |
|
10 | 23 | class DGPSModule(mp_module.MPModule):
|
11 | 24 | def __init__(self, mpstate):
|
12 |
| - super(DGPSModule, self).__init__(mpstate, "DGPS", "DGPS injection support for SBP/RTCP/UBC") |
| 25 | + super(DGPSModule, self).__init__(mpstate, "DGPS", "DGPS injection support for SBP/RTCP/UBC over tcp/udp") |
| 26 | + print ("Loading DGPS module") |
| 27 | + self.dgps_settings = mp_settings.MPSettings( |
| 28 | + [('ip', str, "127.0.0.1"), |
| 29 | + ('port', int, 13320), |
| 30 | + ('conntype', str, 'UDP'), |
| 31 | + ('silentFail', bool, True), |
| 32 | + ('logfile', str, None), |
| 33 | + ('connectionTimeout', int, 10) |
| 34 | + ]) |
| 35 | + self.add_command('dgps', self.cmd_dgps, 'DGPS control', |
| 36 | + ["<status>", |
| 37 | + "<start>", |
| 38 | + "<stop>", |
| 39 | + "set (DGPSSETTING)"]) |
| 40 | + self.add_completion_function('(DGPSSETTING)', |
| 41 | + self.dgps_settings.completion) |
| 42 | + |
| 43 | + self.port = None |
| 44 | + self.waiting = False |
| 45 | + self.lastConnAttempt = None |
| 46 | + self.last_pkt = time.time() |
| 47 | + self.inject_seq_nr = 0 |
| 48 | + self.pkt_count = 0 |
| 49 | + self.last_rate = None |
| 50 | + self.rate_total = 0 |
| 51 | + self.rate = 0 |
| 52 | + # RTCM3 parser |
| 53 | + self.rtcm3 = rtcm3.RTCM3() |
| 54 | + self.last_id = None |
| 55 | + self.id_counts = {} |
| 56 | + self.last_by_id = {} |
| 57 | + |
| 58 | + def log_rtcm(self, data): |
| 59 | + '''optionally log rtcm data''' |
| 60 | + if self.dgps_settings.logfile is None: |
| 61 | + return |
| 62 | + if self.logfile is None: |
| 63 | + self.logfile = open(self.dgps_settings.logfile, 'wb') |
| 64 | + if self.logfile is not None: |
| 65 | + self.logfile.write(data) |
| 66 | + |
| 67 | + def cmd_dgps(self, args): |
| 68 | + '''dgps command handling''' |
| 69 | + if len(args) <= 0: |
| 70 | + print("Usage: dgps <start|stop|status|set>") |
| 71 | + return |
| 72 | + if args[0] == "start": |
| 73 | + self.cmd_start() |
| 74 | + if args[0] == "stop": |
| 75 | + self.cmd_stop() |
| 76 | + elif args[0] == "status": |
| 77 | + self.dgps_status() |
| 78 | + elif args[0] == "set": |
| 79 | + self.dgps_settings.command(args[1:]) |
| 80 | + |
| 81 | + def cmd_stop(self): |
| 82 | + self.waiting = False |
| 83 | + self.port = None |
| 84 | + |
| 85 | + def cmd_start(self): |
| 86 | + '''start dgps link''' |
| 87 | + if self.dgps_settings.conntype == "UDP": |
| 88 | + print("Connecting to UDP RTCM Stream") |
| 89 | + self.connect_udp_rtcm_base(self.dgps_settings.ip, self.dgps_settings.port) |
| 90 | + elif self.dgps_settings.conntype == "TCP": |
| 91 | + print("Connecting to TCP RTCM Stream") |
| 92 | + self.connect_tcp_rtcm_base(self.dgps_settings.ip, self.dgps_settings.port) |
| 93 | + else: |
| 94 | + print("Undefined connection type!") |
| 95 | + return |
| 96 | + |
| 97 | + self.last_rate = time.time() |
| 98 | + self.rate_total = 0 |
| 99 | + |
| 100 | + def dgps_status(self): |
| 101 | + '''show dgps status''' |
| 102 | + now = time.time() |
| 103 | + print("\nDGPS Configuration:") |
| 104 | + print("Connection Type: %s" % self.dgps_settings.conntype) |
| 105 | + print("IP Address: %s" % self.dgps_settings.ip) |
| 106 | + print("Port: %s" % self.dgps_settings.port) |
| 107 | + |
| 108 | + if self.waiting is True: |
| 109 | + print("Connection Error attempting to reconnect") |
| 110 | + return |
| 111 | + |
| 112 | + if self.port is None: |
| 113 | + print("DGPS: Not started") |
| 114 | + return |
| 115 | + elif self.last_pkt is None: |
| 116 | + print("DGPS: no data") |
| 117 | + return |
| 118 | + |
| 119 | + frame_size = 0 |
| 120 | + for id in sorted(self.id_counts.keys()): |
| 121 | + print(" %4u: %u (len %u)" % (id, self.id_counts[id], len(self.last_by_id[id]))) |
| 122 | + frame_size += len(self.last_by_id[id]) |
| 123 | + print("dgps: %u packets, %.2f bytes/sec last %.3fs ago framesize %u" % (self.pkt_count, self.rate, now - self.last_pkt, frame_size)) |
| 124 | + |
| 125 | + def connect_udp_rtcm_base(self, ip, port): |
| 126 | + print ("UDP Connection") |
13 | 127 | self.portnum = 13320
|
14 | 128 | self.port = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
15 | 129 | self.port.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
16 | 130 | self.port.bind(("127.0.0.1", self.portnum))
|
17 | 131 | mavutil.set_close_on_exec(self.port.fileno())
|
18 | 132 | self.port.setblocking(0)
|
19 |
| - self.inject_seq_nr = 0 |
20 | 133 | print("DGPS: Listening for RTCM packets on UDP://%s:%s" % ("127.0.0.1", self.portnum))
|
21 |
| - |
22 |
| - def send_rtcm_msg(self, data): |
23 |
| - msglen = 180; |
24 |
| - |
25 |
| - if (len(data) > msglen * 4): |
26 |
| - print("DGPS: Message too large", len(data)) |
| 134 | + |
| 135 | + def connect_tcp_rtcm_base(self, ip, port): |
| 136 | + if self.waiting is False: |
| 137 | + print ("TCP Connection") |
| 138 | + self.waiting = True |
| 139 | + self.rtcm3.reset() |
| 140 | + try: |
| 141 | + self.port = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 142 | + self.port.settimeout(1) |
| 143 | + self.port.connect((ip, port)) |
| 144 | + except: |
| 145 | + if self.dgps_settings.silentFail is False: |
| 146 | + print ("ERROR: Failed to connect to RTCM base over TCP, retrying in 2.5s") |
| 147 | + self.lastConnAttempt = time.time() |
27 | 148 | return
|
28 |
| - |
29 |
| - # How many messages will we send? |
30 |
| - msgs = 0 |
31 |
| - if (len(data) % msglen == 0): |
32 |
| - msgs = len(data) / msglen |
33 |
| - else: |
34 |
| - msgs = (len(data) / msglen) + 1 |
35 |
| - |
36 |
| - for a in range(0, msgs): |
37 |
| - |
38 |
| - flags = 0 |
39 |
| - |
40 |
| - # Set the fragment flag if we're sending more than 1 packet. |
41 |
| - if (msgs) > 1: |
42 |
| - flags = 1 |
43 |
| - |
44 |
| - # Set the ID of this fragment |
45 |
| - flags |= (a & 0x3) << 1 |
46 |
| - |
47 |
| - # Set an overall sequence number |
48 |
| - flags |= (self.inject_seq_nr & 0x1f) << 3 |
49 |
| - |
50 |
| - |
51 |
| - amount = min(len(data) - a * msglen, msglen) |
52 |
| - datachunk = data[a*msglen : a*msglen + amount] |
53 |
| - |
54 |
| - self.master.mav.gps_rtcm_data_send( |
55 |
| - flags, |
56 |
| - len(datachunk), |
57 |
| - bytearray(datachunk.ljust(180, '\0'))) |
58 |
| - |
59 |
| - # Send a terminal 0-length message if we sent 2 or 3 exactly-full messages. |
60 |
| - if (msgs < 4) and (len(data) % msglen == 0) and (len(data) > msglen): |
61 |
| - flags = 1 | (msgs & 0x3) << 1 | (self.inject_seq_nr & 0x1f) << 3 |
62 |
| - self.master.mav.gps_rtcm_data_send( |
63 |
| - flags, |
64 |
| - 0, |
65 |
| - bytearray("".ljust(180, '\0'))) |
66 |
| - |
67 |
| - self.inject_seq_nr += 1 |
68 | 149 |
|
| 150 | + # This prevents the TCP connection from blocking other functions in the system. |
| 151 | + # If this is set before attempting the connection (.connect) the connection is |
| 152 | + # never established. However if this is not set before that attempt then MAVproxy |
| 153 | + # is blocked for the timeout period, in this case 1 sec. |
| 154 | + self.port.setblocking(0) |
69 | 155 |
|
| 156 | + print ("Connected to base using TCP") |
| 157 | + self.waiting = False |
| 158 | + self.last_pkt = time.time() |
70 | 159 |
|
71 | 160 | def idle_task(self):
|
72 | 161 | '''called in idle time'''
|
| 162 | + # Dont do anything if no recieve port is set |
| 163 | + if self.port is None: |
| 164 | + return |
| 165 | + |
| 166 | + # Check to see if we have not recieved packets in a long time and try to reconnect |
| 167 | + now = time.time() |
| 168 | + if self.waiting is False and now - self.last_pkt > self.dgps_settings.connectionTimeout: |
| 169 | + print("Lost Packets attempting reconnection") |
| 170 | + self.cmd_stop() |
| 171 | + self.cmd_start() |
| 172 | + |
| 173 | + # Try to reconnect in case the connection attempt failed. |
| 174 | + if self.waiting is True and self.dgps_settings.conntype == "TCP": |
| 175 | + if (time.time() - self.lastConnAttempt) > 2.5: |
| 176 | + if self.dgps_settings.silentFail is False: |
| 177 | + print("Attempting to connect") |
| 178 | + self.connect_tcp_rtcm_base(self.dgps_settings.ip, self.dgps_settings.port) |
| 179 | + return |
| 180 | + |
| 181 | + # Read data from socket |
73 | 182 | try:
|
74 | 183 | data = self.port.recv(1024) # Attempt to read up to 1024 bytes.
|
75 | 184 | except socket.error as e:
|
76 | 185 | if e.errno in [ errno.EAGAIN, errno.EWOULDBLOCK ]:
|
77 | 186 | return
|
78 | 187 | raise
|
79 |
| - try: |
80 |
| - self.send_rtcm_msg(data) |
81 | 188 |
|
82 |
| - except Exception as e: |
83 |
| - print("DGPS: GPS Inject Failed:", e) |
| 189 | + # Parse and send RTCM3 data |
| 190 | + for element in data: # RTCM3 Library expects to read one byte at a time |
| 191 | + if self.rtcm3.read(element): # Data sent to rtcm library, it returns data once a full packet is recieved |
| 192 | + self.last_id = self.rtcm3.get_packet_ID() |
| 193 | + rtcm3Data = self.rtcm3.get_packet() |
| 194 | + if self.rtcm3.get_packet() is None: |
| 195 | + print ("No packet") |
| 196 | + return |
| 197 | + |
| 198 | + self.log_rtcm(rtcm3Data) # Log the packet |
| 199 | + |
| 200 | + # Store statistics for each RTCM Packet ID |
| 201 | + if not self.last_id in self.id_counts: |
| 202 | + self.id_counts[self.last_id] = 0 |
| 203 | + self.last_by_id[self.last_id] = rtcm3Data[:] |
| 204 | + self.id_counts[self.last_id] += 1 |
| 205 | + |
| 206 | + # Prepare to send the RTCM3 Data through MAVLink |
| 207 | + blen = len(rtcm3Data) |
| 208 | + if blen > 4*180: # can't send this with GPS_RTCM_DATA (wont fit even on fragmented packets?) |
| 209 | + if self.dgps_settings.silentFail is False: |
| 210 | + print("Error, cannot send data with GPS_RTCM_DATA") |
| 211 | + return |
| 212 | + |
| 213 | + self.rate_total += blen |
| 214 | + |
| 215 | + # Check if data needs to be fragmented |
| 216 | + if blen > 180: |
| 217 | + flags = 1 # fragmented |
| 218 | + else: |
| 219 | + flags = 0 |
| 220 | + |
| 221 | + # add in the sequence number |
| 222 | + flags |= (self.pkt_count & 0x1F) << 3 |
| 223 | + |
| 224 | + # Send data through MAVLink |
| 225 | + fragment = 0 |
| 226 | + while blen > 0: |
| 227 | + send_data = bytearray(rtcm3Data[:180]) |
| 228 | + frag_len = len(send_data) |
| 229 | + data = rtcm3Data[frag_len:] |
| 230 | + if frag_len < 180: |
| 231 | + send_data.extend(bytearray([0]*(180-frag_len))) |
| 232 | + self.master.mav.gps_rtcm_data_send(flags | (fragment<<1), frag_len, send_data) |
| 233 | + fragment += 1 |
| 234 | + blen -= frag_len |
| 235 | + self.pkt_count += 1 |
| 236 | + |
| 237 | + # Store time of last packet sent |
| 238 | + now = time.time() |
| 239 | + self.last_pkt = now |
| 240 | + |
| 241 | + # Calculate the rate at which we are reciving data for stats |
| 242 | + now = time.time() |
| 243 | + if now - self.last_rate > 1: |
| 244 | + dt = now - self.last_rate |
| 245 | + rate_now = self.rate_total / float(dt) |
| 246 | + self.rate = 0.9 * self.rate + 0.1 * rate_now |
| 247 | + self.last_rate = now |
| 248 | + self.rate_total = 0 |
84 | 249 |
|
85 | 250 | def init(mpstate):
|
86 | 251 | '''initialise module'''
|
|
0 commit comments