Package ais :: Module nmea
[hide private]
[frames] | no frames]

Source Code for Module ais.nmea

  1  #!/usr/bin/env python 
  2   
  3  __version__ = '$Revision: 4799 $'.split()[1] 
  4  __date__ = '$Date: 2006-09-25 11:09:02 -0400 (Mon, 25 Sep 2006) $'.split()[1] 
  5  __author__ = 'Kurt Schwehr' 
  6   
  7  __doc__=''' 
  8  Handle creation and extraction of NMEA strings.  Maybe need a separate VDM class like ais-py? 
  9   
 10  @requires: U{lxml<http://codespeak.net/lxml/>} - For libxml2 ElementTree interface.  Not actually required for the template, but this is just a demo or requirements. 
 11  @requires: U{Python<http://python.org/>} >= 2.4 
 12  @requires: U{epydoc<http://epydoc.sourceforge.net/>} >= 3.0alpha3 
 13  @requires: BitVector 
 14   
 15  @author: U{'''+__author__+'''<http://schwehr.org/>} 
 16  @version: ''' + __version__ +''' 
 17  @copyright: 2006 
 18  @var __date__: Date of last svn commit 
 19  @undocumented: __version__ __author__ __doc__ myparser 
 20  @since: 2006-Sep-26  FIX: replace with the file creation date 
 21  @status: under development 
 22  @organization: U{CCOM<http://ccom.unh.edu/>} - FIX: if not CCOM change the name and link 
 23   
 24  @license: GPL v2 
 25   
 26  @note: This package does not respence the maximum number of characters 
 27  per line that is required in the NMEA specification. 
 28   
 29  ''' 
 30   
 31  # Python standard libraries 
 32  import time, sys 
 33   
 34  # Local Modules 
 35  import binary 
 36   
 37  import re 
 38   
 39  EOL = "\x0D\x0A" 
 40  ''' 
 41  DOS style end-of-line (<cr><lf>) for talking to AIS base stations 
 42  ''' 
 43   
 44   
45 -def checksumStr(data,verbose=False):
46 """ 47 Take a NMEA 0183 string and compute the checksum. 48 49 Checksum is calculated by xor'ing everything between ? or ! and the * 50 51 >>> checksumStr("!AIVDM,1,1,,B,35MsUdPOh8JwI:0HUwquiIFH21>i,0*09") 52 '09' 53 54 >>> checksumStr("AIVDM,1,1,,B,35MsUdPOh8JwI:0HUwquiIFH21>i,0") 55 '09' 56 57 >>> checksumStr('$AIACA,0,,,,,,,,,5,2087,0,2088,0,0,0,I,1,000000*15') 58 '15' 59 60 This is an example I made up 61 62 >>> checksumStr('$xxCAB,1,1,1,1*5D') 63 '40' 64 65 @param data: NMEA message. Leading ?/! and training checksum are optional 66 @type data: str 67 @return: hexidecimal value 68 @rtype: str 69 70 """ 71 72 # FIX: strip off new line at the end too 73 #if data[0]=='!' or data[0]=='?': data = data[1:] 74 #if data[-1]=='*': data = data[:-1] 75 #if data[-3]=='*': data = data[:-3] 76 end = data.find('*') # FIX: would rfind be faster? 77 start=0 78 if data[0] in ('$','!'): start=1 79 if -1 != end: data=data[start:end] 80 else: data=data[start:] 81 if verbose: print 'checking on:',start,end,data 82 # FIX: rename sum to not shadown builting function 83 sum=0 84 for c in data: sum = sum ^ ord(c) 85 sumHex = "%x" % sum 86 if len(sumHex)==1: sumHex = '0'+sumHex 87 return sumHex.upper()
88 89 90 ###################################################################### 91 # common variables 92 #nmeaChecksumRegExStr = r"""\,[0-9]\*[0-9A-F][0-9A-F]""" 93 #nmeaChecksumRegExStr = r"""\,[A-Za-z0-9]\*[0-9A-F][0-9A-F]""" 94 nmeaChecksumRegExStr = r"""\*[0-9A-F][0-9A-F]""" 95 nmeaChecksumRE = re.compile(nmeaChecksumRegExStr) 96
97 -def isChecksumValid(nmeaStr, allowTailData=True,verbose=False):
98 """Return True if the string checks out with the checksum 99 100 101 >>> isChecksumValid("!AIVDM,1,1,,B,35MsUdPOh8JwI:0HUwquiIFH21>i,0*09") 102 True 103 104 Corrupted: 105 106 >>> isChecksumValid("!AIVDM,11,1,,B,35MsUdPOh8JwI:0HUwquiIFH21>i,0*09") 107 False 108 109 >>> isChecksumValid('$AIACA,0,,,,,,,,,5,2087,0,2088,0,0,0,I,1,000000*15') 110 True 111 112 @param allowTailData: Permit handing of Coast Guard format with data after the checksum 113 @param nmeaStr: NMEA message. Leading ?/! are optional 114 @type nmeaStr: str 115 @return: True if the checksum matches 116 @rtype: bool 117 """ 118 119 if allowTailData: 120 match = nmeaChecksumRE.search(nmeaStr) 121 if not match: 122 if verbose: print 'Match failed' 123 return False 124 nmeaStr = nmeaStr[:match.end()] 125 #if checksum.upper()==checksumStr(nmeaStr[match.end() 126 127 128 if nmeaStr[-3]!='*': 129 print 'FIX: warning... bad nmea string' 130 return False # Bad string without proper checksum 131 checksum=nmeaStr[-2:] 132 if checksum.upper()==checksumStr(nmeaStr).upper(): 133 return True 134 if verbose: 135 print 'mismatch checksums:', checksum.upper(),checksumStr(nmeaStr).upper() 136 return False
137
138 -def buildNmea(aisBits,prefix='!',serviceType='AI',msgType='VDM',channelSeq=None,channel='A'):
139 ''' 140 Create one long oversized nmea string for the bits 141 @param aisBits: message payload 142 @type aisBits: BitVector 143 @param prefix: '!' or '$' what is the difference? 144 @param serviceType: 'can this be anything other than AI? 145 @param msgType: VDM. Should not be VDO (own ship) 146 @param channelSeq: 1-9 or None 147 @param channel: AIS channel A or B 148 @todo: sync names of prefix and serviceType to NMEA spec. 149 @see: reference the appropriate spec documents for all this stuff. 150 ''' 151 152 rList = [prefix,serviceType,msgType,',1,1,'] 153 if None != channelSeq: rList.append(str(channelSeq)) 154 rList.append(',') 155 rList.append(channel) 156 rList.append(',') 157 158 payloadStr,pad = binary.bitvectoais6(aisBits) #[0] 159 rList.append(payloadStr) 160 rList.append(','+str(pad)) 161 rStr = ''.join(rList) 162 rStr += '*'+checksumStr(rStr) 163 164 return rStr
165 166 167 ######################################################################
168 -def cabEncode(TransA=False, TransB=False, Restart=False, Reset=False, prefix='AI'): # FIX: default to xx?
169 ''' 170 CAB - Control AIS Base Station. Defaults to a safe state with 171 everything shutdown. 62320-1/CDV, 80/427/CDV, Page 77, A.1.7 172 173 >>> cabEncode() 174 '$AICAB,0,0,,*48' 175 176 Note that xx is probably not valid in this next example, but it is used by L3 177 178 Made up example: 179 180 >>> cabEncode(True,True,True,True,prefix='xx') 181 '$xxCAB,1,1,1,1*40' 182 183 @param TransA: Transmissions enabled on channel A 184 @type TransA: bool 185 @param TransB: Transmissions enabled on channel B 186 @type TransB: bool 187 @param Restart: If true, command AIS Base station to restart operations in last known configuration 188 @type Restart: bool 189 @param Reset: 190 @type Reset: bool 191 @param prefix: string to put between the $ and CAB 192 @type prefix: str 193 @return: A CAB NMEA string 194 @rtype: str 195 ''' 196 r = ['$'+prefix+'CAB'] 197 if TransA: r.append('1') 198 else: r.append('0') 199 if TransB: r.append('1') 200 else: r.append('0') 201 if Restart: r.append('1') 202 else: r.append('') 203 if Reset: r.append('1') 204 else: r.append('') 205 rStr = ','.join(r) 206 return rStr+'*'+checksumStr(rStr) 207 208
209 -def cabDecode(msg,validate=True):
210 ''' 211 212 >>> cabDecode('$AICAB,,,,*48') 213 {'Reset': False, 'nmeaPrefix': 'AI', 'nmeaCmd': 'CAB', 'TransB': False, 'TransA': False, 'Restart': False} 214 215 Note that ZZ is probably not valid in this next example 216 217 >>> cabDecode('$ZZCAB,1,1,1,1*40') 218 {'Reset': False, 'nmeaPrefix': 'ZZ', 'nmeaCmd': 'CAB', 'TransB': True, 'TransA': True, 'Restart': True} 219 220 @param msg: NMEA string of a CAB message 221 @type msg: str 222 @param validate: Set to False to turn off validation for speed. 223 @type validate: bool 224 @return: lookup table of key/values 225 @rtype: dict 226 227 @todo: How do I make stable doctests with dictionary returns 228 @todo FIX: throw an exception if not valid 229 ''' 230 if validate and not isChecksumValid(msg,verbose=True): 231 print 'FIX: this should be an exception in cabDecode. Bad checksum' 232 return False 233 fields=msg.split(',') 234 if validate and len(fields) not in (5,6,7): # Allow for USCG station and timestamp 235 # check for USCG log tail 236 print 'FIX: this should be an exception in cabDecode. wrong number of fields' 237 return False 238 239 # FIX: for validate... make sure that the other case from 1 is an empty string 240 r = {} 241 if '1'==fields[1]: r['TransA']=True 242 else: r['TransA']=False 243 if '1'==fields[2]: r['TransB']=True 244 else: r['TransB']=False 245 if '1'==fields[3]: r['Restart']=True 246 else: r['Restart']=False 247 if '1'==fields[4]: r['Reset']=True 248 else: r['Reset']=False 249 r['nmeaCmd']='CAB' 250 r['nmeaPrefix']=fields[0][1:3] 251 return r
252
253 -def verQuery(prefix='xx',appendEOL=True):
254 ''' 255 Ask for the version string from a base station 256 ''' 257 258 rStr = '$' + prefix+'BSQ,VER' 259 rStr += '*' + checksumStr(rStr) 260 261 if appendEOL: rStr += EOL 262 return rStr
263 264 265 # FIX: generic encode query function goes here 266 267 268 txrxLUT={ 269 0: 'tx a and b, rx on a and b' 270 ,1: 'tx a, rx a and b' 271 ,2: 'tx b, rx a and b' 272 ,3: 'no tx, rx a and b' 273 ,4: 'no tx, rx a' 274 ,5: 'no tx, no rx' 275 } 276 ''' 277 Transmit and Received modes. See Page 88 61993-2 and XXXX??? 278 ''' 279 280 acaInfoSrcLUT = { 281 'A':'ITU-R M.1371 message 22: addressed message' 282 ,'B':'ITU-R M.1371 message 22: broadcast message' 283 ,'C':'IEC 61162-1 AIS Channel Assignment setence' 284 ,'D':'DSC Channel 70 telecommand' 285 ,'M':'Operator manual input' 286 } 287 288
289 -def acaDecode(msg,validate=True):
290 ''' 291 Decode AIS Regional Channel Assignment Message. 292 See 61993-2 Page 87. 293 294 295 This is an example of an unconfigured base station, plus there is a USCG timestamp at the end. 296 297 >>> acaDecode('$AIACA,0,,,,,,,,,5,2087,0,2088,0,0,0,I,1,000000*15,1172786646.1') 298 {'inuse': '1', 'north': None, 'txrxMode': '0', 'power': '0', 'nmeaPrefix': 'AI', 'timeinuse': '000000', 'seqnum': '0', 'chanBbandwidth': '0', 'nmeaCmd': 'ACA', 'chanAbandwidth': '0', 'west': None, 'transitionSize': '5', 'infosrc': 'I', 'east': None, 'chanA': '2087', 'south': None, 'chanB': '2088'} 299 300 @todo: get a complete example to decode as a doctest 301 ''' 302 if validate and not isChecksumValid(msg,verbose=True): 303 print 'FIX: this should be an exception in acaDecode. Bad checksum' 304 return False 305 #assert msg[0]=='$' 306 fields = msg.split(',') 307 r = {} 308 r['nmeaCmd']='ACA' 309 r['nmeaPrefix']=fields[0][1:3] 310 r['seqnum'] = fields[1] 311 if len(fields[2])>0: 312 lat = float(fields[2]) # FIX: what format is this?? 313 assert len(fields[3])==1 314 if fields[3]=='S': lat= -1 * lat 315 else: lat = None # empty string 316 r['north']=lat 317 del lat 318 319 if len(fields[4])>0: 320 lon = float(fields[4]) # FIX: what format is this?? 321 assert len(fields[5])==1 322 if fields[5]=='W': lat= -1 * lat 323 else: lon = None # empty string 324 r['east']=lon 325 del lon 326 327 if len(fields[6])>0: 328 lat = float(fields[6]) # FIX: what format is this?? 329 assert len(fields[7])==1 330 if fields[7]=='S': lat= -1 * lat 331 else: lat = None # empty string 332 r['south']=lat 333 del lat 334 335 if len(fields[8])>0: 336 lon = float(fields[8]) # FIX: what format is this?? 337 assert len(fields[9])==1 338 if fields[9]=='W': lat= -1 * lat 339 else: lon = None # empty string 340 r['west']=lon 341 del lon 342 343 r['transitionSize'] = fields[10] # Transition zone size in nm 344 r['chanA'] = fields[11] # Should probably be 2087 345 r['chanAbandwidth'] = fields[12] # 12.5 or 25 kHz 346 r['chanB'] = fields[13] # Should probably be 2087 347 r['chanBbandwidth'] = fields[14] # 12.5 or 25 kHz 348 r['txrxMode'] = fields[15] # See lookup table txrxLUT 349 r['power'] = fields[16] # 0 low, 1 hight 350 r['infosrc'] = fields[17] # See acaInfoSrcLUT, can also be null 351 r['inuse'] = fields[18] # 0 is not in use, 1 in-use. Can also be null 352 r['timeinuse'] = fields[19].split('*')[0] # hhmmss.ss 353 return r
354 355
356 -def cbmDecode(msg,validate=True):
357 ''' 358 Decode Configure Base Station Message Broadcast Reporting Rates message. 359 360 361 >>> cbmDecode('$AICBM,61,76,35,2,60,999,100,999,52,999,1,60,999,100,999*55,1172787005.46') 362 {'msg17chanAnumslots': '1', 'nmeaPrefix': 'AI', 'msg4slot': '61', 'msg17chanAslotinterval': '999', 'nmeaCmd': 'CBM', 'msg20chanAslotinterval': '999', 'msg20chanAstartslot': '60', 'msg17chanAstartslot': '52', 'msg22chanAslotinterval': '999', 'msg22chanAstartslot': '100'} 363 364 @see: 62320-1/CDV 80/427/CDV page 78, A.1.8 365 ''' 366 367 if validate and not isChecksumValid(msg,verbose=True): 368 print 'FIX: this should be an exception in acaDecode. Bad checksum' 369 return False 370 #assert msg[0]=='$' 371 fields = msg.split(',') 372 r = {} 373 r['nmeaCmd']=fields[0][3:] # CBM 374 r['nmeaPrefix']=fields[0][1:3] 375 r['msg4slot'] = fields[1] 376 i = 2 377 r['msg17chanAstartslot'] = fields[i]; i+=1 378 r['msg17chanAslotinterval'] = fields[i]; i+=1 379 r['msg17chanAnumslots'] = fields[i]; i+=1 380 r['msg20chanAstartslot'] = fields[i]; i+=1 381 r['msg20chanAslotinterval'] = fields[i]; i+=1 382 r['msg22chanAstartslot'] = fields[i]; i+=1 383 r['msg22chanAslotinterval'] = fields[i]; i+=1 384 385 r['msg17chanAstartslot'] = fields[i]; i+=1 386 r['msg17chanAslotinterval'] = fields[i]; i+=1 387 r['msg17chanAnumslots'] = fields[i]; i+=1 388 r['msg20chanAstartslot'] = fields[i]; i+=1 389 r['msg20chanAslotinterval'] = fields[i]; i+=1 390 r['msg22chanAstartslot'] = fields[i]; i+=1 391 r['msg22chanAslotinterval'] = fields[i].split('*')[0]; i+=1 # 15 392 return r
393 394 395 ownershipLUT = { 396 'L':'local' 397 ,'R':'remote' 398 ,'C':'clear reservation' 399 } 400
401 -def dlmDecode(msg, validate=True):
402 ''' 403 Decode Data Link Management slot allocation for Base Station nmea message 404 405 406 >>> dlmDecode ('$AIDLM,0,A,L,0,2,7,540,L,4,1,7,250,L,2511,1,7,0,,,,,*40,1172787005.5') 407 {'nmeaPrefix': 'AI', 'timeout3': '7', 'timeout2': '7', 'timeout1': '7', 'timeout4': '', 'startslot2': '4', 'startslot3': '2511', 'incr4': '*40', 'incr3': '0', 'incr2': '250', 'incr1': '540', 'aisChannel': 'A', 'seqNum': '0', 'startslot1': '0', 'startslot4': '', 'nmeaCmd': 'DLM', 'ownership4': '', 'ownership3': 'L', 'ownership2': 'L', 'ownership1': 'L', 'numslots4': '', 'numslots1': '2', 'numslots2': '1', 'numslots3': '1'} 408 409 @see: 62320-1/CDV 80/427/CDV page 79, A.1.9 410 411 ''' 412 413 if validate and not isChecksumValid(msg,verbose=True): 414 print 'FIX: this should be an exception in acaDecode. Bad checksum' 415 return False 416 #assert msg[0]=='$' 417 fields = msg.split(',') 418 r = {} 419 r['nmeaCmd']=fields[0][3:] # CBM 420 r['nmeaPrefix']=fields[0][1:3] 421 i = 1 422 423 r['seqNum'] = fields[i]; i+=1 424 r['aisChannel'] = fields[i]; i+=1 425 426 # reservations 427 r['ownership1'] = fields[i]; i+=1 # See ownership 428 r['startslot1'] = fields[i]; i+=1 429 r['numslots1'] = fields[i]; i+=1 430 r['timeout1'] = fields[i]; i+=1 431 r['incr1'] = fields[i]; i+=1 432 433 r['ownership2'] = fields[i]; i+=1 434 r['startslot2'] = fields[i]; i+=1 435 r['numslots2'] = fields[i]; i+=1 436 r['timeout2'] = fields[i]; i+=1 437 r['incr2'] = fields[i]; i+=1 438 439 r['ownership3'] = fields[i]; i+=1 440 r['startslot3'] = fields[i]; i+=1 441 r['numslots3'] = fields[i]; i+=1 442 r['timeout3'] = fields[i]; i+=1 443 r['incr3'] = fields[i]; i+=1 444 445 r['ownership4'] = fields[i]; i+=1 446 r['startslot4'] = fields[i]; i+=1 447 r['numslots4'] = fields[i]; i+=1 448 r['timeout4'] = fields[i]; i+=1 449 r['incr4'] = fields[i]; i+=1 450 451 return r
452
453 -def dumb():
454 print
455
456 -def encodeBBM(totSent, sentNum, seqId, aisChan, msgId, data, numFillBits 457 ,prefix='xx',appendEOL=True 458 ,validate=True 459 ):
460 '''Encode a binary broadcast message. 461 462 I have no idea what this message says... 463 464 >>> encodeBBM(1,1,0,3,8,'Fs[Ifs?:=2h:ec]dc3?HKI0f3?eFHa4[MGAMO6I2vqG0g',4) 465 '!xxBBM,1,1,3,8,Fs[Ifs?:=2h:ec]dc3?HKI0f3?eFHa4[MGAMO6I2vqG0g,4*2E' 466 467 @see: IEC-PAS 61162-100 80/330/PAS, Page 19 468 @param totSent: Total number of sentences needed for the message (1-9) 469 @type totSent: int 470 @param sentNum: Which sentence is this in the series (1-9) 471 @type sentNum: int 472 @param seqId: need to increment this for each message??!?!? (0-9) Linked to ABK 473 @type seqId: int 474 @param aisChan: AIS channel to use to send the message 475 0. No channel preference 476 1. AIS Channel A 477 2. AIS Channel B 478 3. Broadcast on both A and B 479 @type aisChan: str 480 @param msgId: AIS message 8 (binary broadcast message) or 14 (safety related broadcast) 481 @type msgId: int 482 @param data: Content of the binary data. First sentence must be 58 characters or less. 483 The rest can be up to 60 characters. 484 @param numFillBits: Number of bits of padding in the last character of the data (0-5) 485 @type numFillBits: int 486 @return: nmea string 487 @rtype: str 488 ''' 489 if validate: 490 # obsesive error checking follows 491 tot = int(totSent) 492 assert (0<tot and tot<=9) 493 num = int(sentNum) 494 assert (0<num and num<=9) 495 assert (num<=tot) 496 seq = int(seqId) 497 assert (0<=seq and seq <=9) 498 assert (int(aisChan) in range(0,5)) 499 assert (int(msgId) in (8,14)) 500 if num==1: assert(len(data)<=58) 501 else: assert(len(data)<=60) 502 # FIX: add validation of the characters in the data string 503 assert(int(numFillBits) in range(0,6)) 504 r = ','.join(('!'+prefix+'BBM',str(totSent),str(sentNum),str(aisChan),str(msgId),data,str(numFillBits))) 505 506 r += '*'+checksumStr(r) 507 if validate: assert(len(r)) <= 81 # Max nmea string length 508 return r
509 510 511 ###################################################################### 512 if __name__=='__main__': 513 from optparse import OptionParser 514 myparser = OptionParser(usage="%prog [options]", 515 version="%prog "+__version__) 516 myparser.add_option('--test','--doc-test',dest='doctest',default=False,action='store_true', 517 help='run the documentation tests') 518 myparser.add_option('-v','--verbose',dest='verbose',default=False,action='store_true', 519 help='run the tests run in verbose mode') 520 (options,args) = myparser.parse_args() 521 522 success=True 523 524 if options.doctest: 525 import os; print os.path.basename(sys.argv[0]), 'doctests ...', 526 argvOrig = sys.argv 527 sys.argv= [sys.argv[0]] 528 if options.verbose: sys.argv.append('-v') 529 import doctest 530 numfail,numtests=doctest.testmod() 531 if numfail==0: print 'ok' 532 else: 533 print 'FAILED' 534 success=False 535 sys.argv = argvOrig # Restore the original args 536 del argvOrig # hide from epydoc 537 538 if not success: 539 sys.exit('Something Failed') 540 541 del success # Hide success from epydoc 542