source: core/trunk/client/adsb2nmea.py @ 272

Last change on this file since 272 was 272, checked in by dominic, 11 years ago

Updated Trk,GS,Callsign in ADSB

File size: 11.6 KB
RevLine 
[271]1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
[272]4# This script reads dump1090 input from port, converts it in FLARM specific NMEA sentences and writes these sentences to a local named pipe
5# More information about the ADS-B message format can be found under : http://www.homepages.mcb.net/bones/SBS/Article/Barebones42_Socket_Data.htm
6#
[271]7
8from math import *
9import datetime
10import re
11import os
12from socket import socket, gethostbyname, getaddrinfo, error, \
13  AF_INET, SOCK_STREAM, SOCK_DGRAM
14from sys import argv, stderr
15from time import time
16
17NaN = float('nan')
[272]18Hex = ''
[271]19
20TypeADSB       = 1
21traffic_data = {}       # dictionary for all traffic
22
23class aircraft:
24    def __init__(self):
25        self.id = ""                # ICAO Id
26        self.latitude = self.longitude = 0.0
27        self.altitude = NaN         # Feet
28
29class adsb2udp():
30  "SBS client interface to ADS-B."
31  def __init__(self, src_host='192.168.178.21', src_port=30003, device='', \
32    my_Id='TEST', dst_host='192.168.2.108', dst_port=10110, \
33    base_lat=48.715247, base_long=11.533155, base_alt=400.0):
34    print 'adsb2udp: src host =' , src_host , ', src_port =' , src_port , \
35    ', dst_host =' , dst_host , ', dst_port =' , dst_port , ', base_lat =' , \
36    base_lat , ', base_long =' , base_long , ', base_alt =' , base_alt
37    self.verbose = 0
38    self.linebuffer = ""
39    self.response = ""
40    self.sock = None        # in case we blow up in connect
41    self.pos = aircraft()
42    self.device = device
43    self.src_host = gethostbyname(src_host)
44    self.src_port = src_port
45    self.dst_host = gethostbyname(dst_host)
46    self.dst_port = dst_port
47    self.base_lat = base_lat
48    self.base_long = base_long
49    self.base_alt = base_alt
50    if src_host != None:
51        self.connect(src_host, src_port)
52    self.d = socket(AF_INET, SOCK_DGRAM)
53
54  def connect(self, host, port):
55        """Connect to a host on a given port."""
56        msg = "getaddrinfo returns an empty list"
57        self.sock = None
58        for res in getaddrinfo(host, port, 0, SOCK_STREAM):
59                af, socktype, proto, canonname, sa = res
60                try:
61                        self.sock = socket(af, socktype, proto)
62                        #if self.debuglevel > 0: print 'connect:', (host, port)
63                        self.sock.connect(sa)
64                except error, msg:
65                        #if self.debuglevel > 0: print 'connect fail:', (host, port)
66                        self.close()
67                        continue
68                break
69        if not self.sock:
70                raise error, msg
71               
72        FIFO_PATH = '/tmp/my_fifo'
73        if os.path.exists(FIFO_PATH):
74                os.unlink(FIFO_PATH)
75
76        if not os.path.exists(FIFO_PATH):
77                os.mkfifo(FIFO_PATH)
78        self.my_fifo = open(FIFO_PATH, 'w+')
79        print "my_fifo:",self.my_fifo   
80
81  def close(self):
82      if self.sock:
83          self.sock.close()
84      self.sock = None
85
86  def __del__(self):
87      self.close()
88
89  def read(self):
90      "Wait for and read data being streamed from the daemon."
91      if self.verbose > 1:
92          stderr.write("poll: reading from daemon...\n")
93      eol = self.linebuffer.find('\n')
94      if eol == -1:
95         
96          frag = self.sock.recv(4096)
97          self.linebuffer += frag
98          if self.verbose > 1:
99              stderr.write("poll: read complete.\n")
100          if not self.linebuffer:
101              if self.verbose > 1:
102                  stderr.write("poll: returning -1.\n")
103              # Read failed
104              return -1
105          eol = self.linebuffer.find('\n')
106          if eol == -1:
107              if self.verbose > 1:
108                  stderr.write("poll: returning 0.\n")
109              # Read succeeded, but only got a fragment
110              return 0
111      else:
112          if self.verbose > 1:
113              stderr.write("poll: fetching from buffer.\n")
114
115      # We got a line
116      eol += 1
117      self.response = self.linebuffer[:eol]
118      self.linebuffer = self.linebuffer[eol:]
119
120      # Can happen if daemon terminates while we're reading.
121      if not self.response:
122          return -1
123      if self.verbose:
124          stderr.write("poll: data is %s\n" % repr(self.response))
125      self.received = time()
126      # We got a \n-terminated line
127      return len(self.response)
128
129  def unpack(self, buf):
[272]130      global Hex
131      Hex = ''
[271]132      fields = buf.strip().split(",")
133      # print fields
134      if fields[0] == "MSG" and fields[1] == "3":
135        # print fields
136        self.pos.id = fields[4]
137        self.latitude = self.longitude = 0.0
138        self.altitude = NaN         # Feet
139        if fields[11] != '':
140          self.pos.altitude = float(fields[11])
141        if fields[14] != '':
142          self.pos.latitude = float(fields[14])
143        if fields[15] != '':
144          self.pos.longitude = float(fields[15])
[272]145        if (traffic_data.has_key((fields[4],'CS'))):
146          traffic_data[fields[4],'Lat'] = float(fields[14])
147          traffic_data[fields[4],'Lon'] = float(fields[15])
148          traffic_data[fields[4],'Alt'] = float(fields[11])
149          traffic_data[fields[4],'Tim'] = fields[7]
150          if traffic_data.has_key((fields[4],'Trk')):
151             Hex = fields[4]
152             # print CS
153         
[271]154
[272]155
[271]156      if fields[0] == "MSG" and fields[1] == "1":
[272]157        # Message 1: MT,TT,SID,AID,Hex,FID,DMG,TMG,DML,TML,CS
158        # print "HexID: " + fields[4]
159        # print "Callsign: " + fields[10]
160        if not (traffic_data.has_key((fields[4],'CS'))):
161          # Callsign not known yet ---> store in dict
162          traffic_data[fields[4],'CS'] = fields[10]
163          traffic_data[fields[4],'Tim'] = fields[7]
164          # print traffic_data
165
166
167      if fields[0] == "MSG" and fields[1] == "4":
168        # Message 4: MT,TT,SID,AID,Hex,FID,DMG,TMG,DML,TML,,,GS,Trk,,,VR
169        if (traffic_data.has_key((fields[4],'CS'))):
170          # Callsign known, we can store data to it
171          traffic_data[fields[4],'GS'] = float(fields[12])*0.514444
172          traffic_data[fields[4],'Trk'] = int(fields[13])
173          traffic_data[fields[4],'VR'] = float(fields[16])/3.28084/60
174          traffic_data[fields[4],'Tim'] = fields[7]
175
[271]176               
177  def checksum(self, sentence):
178 
179      """ Remove leading $ """
180      sentence = sentence.lstrip('$')
181 
182      nmeadata,cksum = re.split('\*', sentence)
183      #print nmeadata
184      calc_cksum = 0
185      for s in nmeadata:
186          calc_cksum ^= ord(s)
187 
188      return calc_cksum
189
190
191  def process(self):
192    "Process one incoming ADS-B message into one outgoing UDP packet."
193
194    try:
195      while True:
196 
197        self.read()
198
199        # stderr.write("poll: data is %s\n" % repr(self.response))
[272]200        if self.response.startswith("MSG,1") :
[271]201            # print self.response
[272]202            self.unpack(self.response)
203           
204        if self.response.startswith("MSG,4") :
205            self.unpack(self.response)
206           
207        if self.response.startswith("MSG,2") :
208            # MSG 2 triggered by ground switch - we don't process it here
209            print 'Alt, GS, Trk, Lat, Lng, GND'
210            print self.response
211           
[271]212        if self.response.startswith("MSG,3") :
213            self.unpack(self.response)
[272]214            # Only treat MSG3, if CS is already known
215            if Hex != '':
216              #print '----------------------------------------'
217              #print ' ADS-B reading'
218              #print '----------------------------------------'
219              #print 'id          ' , self.pos.id
220              #print 'latitude    ' , self.pos.latitude
221              #print 'longitude   ' , self.pos.longitude
222              #print 'altitude    ' , self.pos.altitude
[271]223
[272]224              buf = "%s %s %f %f %.1f %.1f %.1f %u" % ( 0, self.pos.id, \
225                self.pos.latitude, self.pos.longitude, self.pos.altitude / 3.2808, \
226                0, 0 , TypeADSB )
[271]227         
[272]228              lon1, lat1, lon2, lat2 = map(radians, [self.base_long, self.base_lat, self.pos.longitude, self.pos.latitude])
[271]229         
[272]230              dlon = lon2 - lon1
231              dlat = lat2 - lat1
232              a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
233              c = 2 * atan2(sqrt(a), sqrt(1-a))
234              Distance = 6371000 * c
[271]235
[272]236              Bearing = atan2(sin(lon2-lon1)*cos(lat2), cos(lat1)*sin(lat2)-sin(lat1)*cos(lat2)*cos(lon2-lon1))             
237              relBearing = degrees(Bearing)
238              absBearing = (relBearing + 360) % 360
239              #stderr.write("Bearing %f ° " % Bearing)
240              #stderr.write("Distance %f km \n" % Distance)
[271]241         
[272]242              relNorth = cos(radians(absBearing))*Distance
243              relEast = sin(radians(absBearing))*Distance
244              relVert = self.pos.altitude/ 3.2808 - self.base_alt
[271]245         
[272]246              #tip: it is necessary to supply both $PFLAU and $PFLAA Flarm(R) NMEA messages to XCSoar, otherwise it does not not show the traffic.
247              #PFLAA,<AlarmLevel>,<RelativeNorth>,<RelativeEast>,<RelativeVertical>,<IDType>,<ID>,<Track>,<TurnRate>,<GroundSpeed>,<ClimbRate>,<AcftType>
248              #PFLAU,<RX>,<TX>,<GPS>,<Power>,<AlarmLevel>,<RelativeBearing>,<AlarmType>,<RelativeVertical>,<RelativeDistance>(,<ID>)
[271]249           
[272]250              str1 = "$PFLAU,,,,0,%d,0,%d,%u,%s*" % \
251              ( relBearing, int(relVert), int(Distance), self.pos.id )
252              csum = self.checksum(str1)
253              str1 += "%02x\r\n" % csum
254              #stderr.write(str1)
255              # XCSoar ignores warning data from PFLAU
256              # $PFLAA,0,1671,186,410,2,DDAA30,252,,21,12.4,1*62
257              # print traffic_data
258              # $PFLAA,0,24,-5,3,1,123456,0,,0,0.1,1*36
259              # $PFLAA,0,16,2,2,1,123457,0,,0,0.1,1*6A
260              # $PFLAA,0,155,-1069,418,1,ABCDEF,221,,37,2.8,1*0B
261              # $PFLAA,0,24,-5,3,1,123456,0,,0,0.1,1*36"
262
263             
264              str2 = "$PFLAA,0,%d,%d,%d,1,%s,%i,,%.1f,%.1f,8*" % \
265              ( int(relNorth), int(relEast), int(relVert), traffic_data[Hex,'CS'].replace(" ",""),int(traffic_data[Hex,'Trk']),traffic_data[Hex,'GS'],traffic_data[Hex,'VR'])
266              csum = self.checksum(str2)
267              str2 += "%02x\r\n" % csum
268              # stderr.write(str2)
[271]269           
[272]270              if (self.base_lat < 0) :
271                  Latstring = "S"
272              else :
273                  Latstring = "N"
274              if (self.base_long < 0) :
275                  Longstring = "W"
276              else :
277                  Longstring = "E"
278              latdeg = int(abs(floor(self.base_lat)))
279              latmin = abs(60*(self.base_lat-latdeg))
280              longdeg = int(abs(floor(self.base_long)))
281              longmin = abs(60*(self.base_long-longdeg))
282              #--GLL Latitude,N/S,Longitude,E/W,UTC,Status (A=Valid, V=Invalid),Checksum
283              #GPRMC,225446,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E*68
284              now = datetime.datetime.utcnow()
285              str3 = "$GPRMC,%02i%02i%02i,A,%s%.2f,%s,%s%.2f,%s,,,%02i%02i%02i,001.0,W*" % \
286              (now.hour, now.minute, now.second, latdeg, latmin, Latstring, longdeg, longmin, Longstring, now.day, now.month, now.year )
287              csum = self.checksum(str3)
288              str3 += "%02x\r\n" % csum
[271]289           
[272]290              #--- GPGGA sentences - required for flarmradar.ch
291              # $GPGGA,173431.00,4934.90002,N,00924.13332,E,7,5,0.96,415.1,M,48.0,M,,*60
292              str4 = "$GPGGA,%02i%02i%02i.00,%02d%.5f,%s,%03d%.5f,%s,7,5,0.96,415.1,M,48.0,M,,*" % \
293              (now.hour, now.minute, now.second, latdeg, latmin, Latstring, longdeg, longmin, Longstring)
294              csum = self.checksum(str4)
295              str4 += "%02x\r\n" % csum
296              # print str4
[271]297           
[272]298              buf = str1 + str2 + str3 + str4
299              # stderr.write(buf)
300              # Write NMEA sentences to pipe for IPC to adsbclient.pl (Flarmradar)
301              self.my_fifo.write(buf)
[271]302               
303           
[272]304              # self.d.sendto(buf, (self.dst_host, self.dst_port))
[271]305 
306    except KeyboardInterrupt:
307      # Avoid garble on ^C
308      print ""
309
310if __name__ == '__main__':
311
312  opts = { }
313
314  argc = len(argv)
315  if argc > 1:
316    opts["src_host"]    = argv[1]
317 
318  if argc > 2:
319    opts["src_port"]    = int(argv[2])
320
321  if argc > 3:
322    opts["dst_host"]    = argv[3]
323 
324  if argc > 4:
325    opts["dst_port"]    = int(argv[4])
326 
327  session = adsb2udp(**opts)
328  session.process()
329
330# driver.py ends here
Note: See TracBrowser for help on using the repository browser.