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

Source Code for Module ais.nmea

  1  #!/usr/bin/env python 
  2   
  3  __version__ = '$Revision: 6288 $'.split()[1] 
  4  __date__ = '$Date: 2007-06-04 06:06:42 -0700 (Mon, 04 Jun 2007) $'.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__ parser 
 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 >>> cabEncode(prefix='xx') 177 '$xxCAB,0,0,,*40' 178 179 Note that xx is probably not valid in this next example, but it is used by L3 180 181 Made up example: 182 183 >>> cabEncode(True,True,True,True,prefix='xx') 184 '$xxCAB,1,1,1,1*40' 185 186 >>> cabEncode(True,True,prefix='L3') 187 '$L3CAB,1,1,,*3F' 188 189 @param TransA: Transmissions enabled on channel A 190 @type TransA: bool 191 @param TransB: Transmissions enabled on channel B 192 @type TransB: bool 193 @param Restart: If true, command AIS Base station to restart operations in last known configuration 194 @type Restart: bool 195 @param Reset: 196 @type Reset: bool 197 @param prefix: string to put between the $ and CAB 198 @type prefix: str 199 @return: A CAB NMEA string 200 @rtype: str 201 ''' 202 r = ['$'+prefix+'CAB'] 203 if TransA: r.append('1') 204 else: r.append('0') 205 if TransB: r.append('1') 206 else: r.append('0') 207 if Restart: r.append('1') 208 else: r.append('') 209 if Reset: r.append('1') 210 else: r.append('') 211 rStr = ','.join(r) 212 return rStr+'*'+checksumStr(rStr) 213 214
215 -def cabDecode(msg,validate=True):
216 ''' 217 218 >>> cabDecode('$AICAB,,,,*48') 219 {'Reset': False, 'nmeaPrefix': 'AI', 'nmeaCmd': 'CAB', 'TransB': False, 'TransA': False, 'Restart': False} 220 221 Note that ZZ is probably not valid in this next example 222 223 >>> cabDecode('$ZZCAB,1,1,1,1*40') 224 {'Reset': False, 'nmeaPrefix': 'ZZ', 'nmeaCmd': 'CAB', 'TransB': True, 'TransA': True, 'Restart': True} 225 226 @param msg: NMEA string of a CAB message 227 @type msg: str 228 @param validate: Set to False to turn off validation for speed. 229 @type validate: bool 230 @return: lookup table of key/values 231 @rtype: dict 232 233 @todo: How do I make stable doctests with dictionary returns 234 @todo FIX: throw an exception if not valid 235 ''' 236 if validate and not isChecksumValid(msg,verbose=True): 237 print 'FIX: this should be an exception in cabDecode. Bad checksum' 238 return False 239 fields=msg.split(',') 240 if validate and len(fields) not in (5,6,7): # Allow for USCG station and timestamp 241 # check for USCG log tail 242 print 'FIX: this should be an exception in cabDecode. wrong number of fields' 243 return False 244 245 # FIX: for validate... make sure that the other case from 1 is an empty string 246 r = {} 247 if '1'==fields[1]: r['TransA']=True 248 else: r['TransA']=False 249 if '1'==fields[2]: r['TransB']=True 250 else: r['TransB']=False 251 if '1'==fields[3]: r['Restart']=True 252 else: r['Restart']=False 253 if '1'==fields[4]: r['Reset']=True 254 else: r['Reset']=False 255 r['nmeaCmd']='CAB' 256 r['nmeaPrefix']=fields[0][1:3] 257 return r
258
259 -def verQuery(prefix='xx',appendEOL=True):
260 ''' 261 Ask for the version string from a base station 262 263 >>> verQuery(appendEOL=False) 264 '$xxBSQ,VER*2D' 265 266 >>> verQuery('AI',appendEOL=False) 267 '$AIBSQ,VER*25' 268 269 ''' 270 271 rStr = '$' + prefix+'BSQ,VER' 272 rStr += '*' + checksumStr(rStr) 273 274 if appendEOL: rStr += EOL 275 return rStr
276
277 -def encodeQuery(query, prefix='xx',appendEOL=True):
278 ''' 279 Ask for the version string from a base station 280 281 >>> encodeQuery('VER',appendEOL=False) 282 '$xxBSQ,VER*2D' 283 284 >>> encodeQuery('VER', prefix='L3', appendEOL=False) 285 '$L3BSQ,VER*52' 286 287 >>> encodeQuery('VER','AI',appendEOL=False) 288 '$AIBSQ,VER*25' 289 290 >>> encodeQuery('ACA',appendEOL=False) 291 '$xxBSQ,ACA*2F' 292 293 >>> encodeQuery('CBM',appendEOL=False) 294 '$xxBSQ,CBM*20' 295 296 >>> encodeQuery('DLM',appendEOL=False) 297 '$xxBSQ,DLM*29' 298 299 >>> encodeQuery('DLM', prefix='L3', appendEOL=False) 300 '$L3BSQ,DLM*56' 301 302 >>> encodeQuery('BCF', prefix='L3', appendEOL=False) 303 '$L3BSQ,BCF*54' 304 305 >>> encodeQuery('CAB', prefix='L3', appendEOL=False) 306 '$L3BSQ,CAB*53' 307 308 309 ''' 310 311 # >>> encodeQuery('SID',appendEOL=False) 312 # '$xxBSQ,SID*32' 313 314 315 rStr = '$' + prefix+'BSQ,'+query 316 rStr += '*' + checksumStr(rStr) 317 318 if appendEOL: rStr += EOL 319 return rStr
320 321 322 # FIX: generic encode query function goes here 323 324 325 txrxLUT={ 326 0: 'tx a and b, rx on a and b' 327 ,1: 'tx a, rx a and b' 328 ,2: 'tx b, rx a and b' 329 ,3: 'no tx, rx a and b' 330 ,4: 'no tx, rx a' 331 ,5: 'no tx, no rx' 332 } 333 ''' 334 Transmit and Received modes. See Page 88 61993-2 and XXXX??? 335 ''' 336 337 acaInfoSrcLUT = { 338 'A':'ITU-R M.1371 message 22: addressed message' 339 ,'B':'ITU-R M.1371 message 22: broadcast message' 340 ,'C':'IEC 61162-1 AIS Channel Assignment setence' 341 ,'D':'DSC Channel 70 telecommand' 342 ,'M':'Operator manual input' 343 ,'I':'Why is the L-3 unit returning I??? It is not defined on page 88 or 61993-2' 344 } 345 ''' Lookup table of codes to use in the Information Source of an ACA 346 message. See acaEncode() and acaDecode() 347 ''' 348 349 powerLUT={ 350 0: 'high', 351 1: 'low' 352 } 353 354 powerEncode={ 355 'high':0, 356 'low':1 357 } 358
359 -def acaEncode(seqnum='',north='',east='',south='',west='',transitionSize='' 360 ,chanA='2087',chanAbandwidth='0' # default to normal channel and 0 is default bandwidth 361 ,chanB='2088',chanBbandwidth='0' 362 ,txrxMode='' # '3' # Default to no tx, but rx both 363 ,power='' # Default to low power 364 ,infosrc='' # Should be empty when sent to an AIS unit 365 ,timeinuse='' # Should be empty for sent to an AIS unit 366 ,prefix='xx',appendEOL=True 367 ,validate=True 368 ):
369 '''Encode an AIS Regional Channel Assignment Message. 370 371 372 >>> acaEncode(appendEOL=False) 373 '$xxACA,,,,,,,2087,0,2088,0,,,,*4C' 374 375 Set to high power 376 377 >>> acaEncode(power=powerEncode['high'],appendEOL=False) 378 '$xxACA,,,,,,,2087,0,2088,0,,0,,*7C' 379 380 Set to low power 381 382 >>> acaEncode(power=powerEncode['low'],appendEOL=False) 383 '$xxACA,,,,,,,2087,0,2088,0,,1,,*7D' 384 385 @see: 61993-2 Page 87. 386 @param seqnum: 387 @type seqnum: int 388 @param north: llll.ll northern boundary 389 @type north: float string 390 @param east: yyyyy.yy (perhaps y was a bad choice) 391 @type east: float string 392 @param south: 393 @type south:float string 394 @param west:yyyyy.yy (perhaps y was a bad choice) 395 @type west:float string 396 @param transitionSize: (nautical miles) 397 @type transitionSize: int 398 @param chanA: Channel A number 399 @type chanA: int 400 @param chanAbandwidth: 0 is the default, 1 is 12.5 kHz 401 @type chanAbandwidth: int 402 @param chanB: Channel B number 403 @type chanB: int 404 @param chanBbandwidth: 0 is the default, 1 is 12.5 kHz 405 @type chanBbandwidth: int 406 @param txrxMode: See txrxLUT for the numbers 407 @type txrxMode: int 408 @param power: 0 for high, 1 for low 409 @type power: int 410 @param infosrc: should be empty for sending to an AIS device. See acaInfoSrcLUT 411 @type infosrc: letter 412 @param timeinuse: should be empty for sending to an AIS device. Time in UTC that the device changed to this state 413 @type timeinuse: hhmmss.ss 414 @param prefix: Vendor specific prefix. FIX: what should be used here? 415 @type prefix: Two letters 416 @param appendEOL: Do you want a DOS end of line appended? 417 @type appendEOL: bool 418 @param validate: Set to true to validate the message 419 @type validate: bool 420 ''' 421 if validate: 422 if ''!=seqnum: assert int(seqnum) in range(10) 423 if ''!=north: assert float(north) >= -90. and float(north<=90.) 424 if ''!=south: assert float(south) >= -90. and float(south<=90.) 425 if ''!=east: assert float(east) >= -180. and float(east<=180.) 426 if ''!=west: assert float(west) >= -180. and float(west<=180.) 427 428 if transitionSize!='': assert int(transitionSize) in range(1,9) 429 if chanA !='': assert int(chanA) > 2000 and int(chanA) <= 2290 # FIX: what is the real range or is this it? 430 if chanAbandwidth!='': assert int(chanAbandwidth) in (0,1) 431 if chanB !='': assert int(chanB) > 2000 and int(chanB) <= 2290 # FIX: what is the real range or is this it? 432 if chanBbandwidth!='': assert int(chanBbandwidth) in (0,1) 433 434 if txrxMode!='': assert int(txrxMode) in range(6) 435 if power!='': assert int(power) in (0,1) 436 if infosrc!='': assert infosrc in ('A','B','C','D','M') # Sorry L-3, but I does not appear to be valid 437 if timeinuse!='': 438 v = float(timeinuse) 439 assert v>=0. and v<24. 440 441 r = ['$'+prefix+'ACA',str(seqnum) 442 ,str(north),str(east),str(south),str(west),str(transitionSize) 443 ,str(chanA),str(chanAbandwidth) 444 ,str(chanB),str(chanBbandwidth) 445 ,str(txrxMode),str(power),str(infosrc),str(timeinuse)] 446 447 rStr = ','.join(r) 448 rStr += '*'+checksumStr(rStr) 449 if validate: assert(len(rStr)<=81) 450 if appendEOL: rStr+=EOL 451 452 return rStr
453 454
455 -def acaDecode(msg,validate=True):
456 ''' 457 Decode AIS Regional Channel Assignment Message. 458 459 460 This is an example of an unconfigured base station, plus there is a USCG timestamp at the end. 461 462 >>> acaDecode('$AIACA,0,,,,,,,,,5,2087,0,2088,0,0,0,I,1,000000*15,1172786646.1') 463 {'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'} 464 465 @see: 61993-2 Page 87. 466 @todo: get a complete example to decode as a doctest 467 ''' 468 if validate and not isChecksumValid(msg,verbose=True): 469 print 'FIX: this should be an exception in acaDecode. Bad checksum' 470 return False 471 #assert msg[0]=='$' 472 fields = msg.split(',') 473 r = {} 474 r['nmeaCmd']='ACA' 475 r['nmeaPrefix']=fields[0][1:3] 476 r['seqnum'] = fields[1] 477 if len(fields[2])>0: 478 lat = float(fields[2]) # FIX: what format is this?? 479 assert len(fields[3])==1 480 if fields[3]=='S': lat= -1 * lat 481 else: lat = None # empty string 482 r['north']=lat 483 del lat 484 485 if len(fields[4])>0: 486 lon = float(fields[4]) # FIX: what format is this?? 487 assert len(fields[5])==1 488 if fields[5]=='W': lat= -1 * lat 489 else: lon = None # empty string 490 r['east']=lon 491 del lon 492 493 if len(fields[6])>0: 494 lat = float(fields[6]) # FIX: what format is this?? 495 assert len(fields[7])==1 496 if fields[7]=='S': lat= -1 * lat 497 else: lat = None # empty string 498 r['south']=lat 499 del lat 500 501 if len(fields[8])>0: 502 lon = float(fields[8]) # FIX: what format is this?? 503 assert len(fields[9])==1 504 if fields[9]=='W': lat= -1 * lat 505 else: lon = None # empty string 506 r['west']=lon 507 del lon 508 509 r['transitionSize'] = fields[10] # Transition zone size in nm 510 r['chanA'] = fields[11] # Should probably be 2087 511 r['chanAbandwidth'] = fields[12] # 12.5 or 25 kHz 512 r['chanB'] = fields[13] # Should probably be 2087 513 r['chanBbandwidth'] = fields[14] # 12.5 or 25 kHz 514 r['txrxMode'] = fields[15] # See lookup table txrxLUT 515 r['power'] = fields[16] # 0 low, 1 hight 516 r['infosrc'] = fields[17] # See acaInfoSrcLUT, can also be null 517 r['inuse'] = fields[18] # 0 is not in use, 1 in-use. Can also be null 518 r['timeinuse'] = fields[19].split('*')[0] # hhmmss.ss 519 return r
520 521
522 -def cbmDecode(msg,validate=True):
523 ''' 524 Decode Configure Base Station Message Broadcast Reporting Rates message. 525 526 527 >>> cbmDecode('$AICBM,61,76,35,2,60,999,100,999,52,999,1,60,999,100,999*55,1172787005.46') 528 {'msg17chanAnumslots': '1', 'nmeaPrefix': 'AI', 'msg4slot': '61', 'msg17chanAslotinterval': '999', 'nmeaCmd': 'CBM', 'msg20chanAslotinterval': '999', 'msg20chanAstartslot': '60', 'msg17chanAstartslot': '52', 'msg22chanAslotinterval': '999', 'msg22chanAstartslot': '100'} 529 530 @see: 62320-1/CDV 80/427/CDV page 78, A.1.8 531 ''' 532 533 if validate and not isChecksumValid(msg,verbose=True): 534 print 'FIX: this should be an exception in acaDecode. Bad checksum' 535 return False 536 #assert msg[0]=='$' 537 fields = msg.split(',') 538 r = {} 539 r['nmeaCmd']=fields[0][3:] # CBM 540 r['nmeaPrefix']=fields[0][1:3] 541 r['msg4slot'] = fields[1] 542 i = 2 543 r['msg17chanAstartslot'] = fields[i]; i+=1 544 r['msg17chanAslotinterval'] = fields[i]; i+=1 545 r['msg17chanAnumslots'] = fields[i]; i+=1 546 r['msg20chanAstartslot'] = fields[i]; i+=1 547 r['msg20chanAslotinterval'] = fields[i]; i+=1 548 r['msg22chanAstartslot'] = fields[i]; i+=1 549 r['msg22chanAslotinterval'] = fields[i]; i+=1 550 551 r['msg17chanAstartslot'] = fields[i]; i+=1 552 r['msg17chanAslotinterval'] = fields[i]; i+=1 553 r['msg17chanAnumslots'] = fields[i]; i+=1 554 r['msg20chanAstartslot'] = fields[i]; i+=1 555 r['msg20chanAslotinterval'] = fields[i]; i+=1 556 r['msg22chanAstartslot'] = fields[i]; i+=1 557 r['msg22chanAslotinterval'] = fields[i].split('*')[0]; i+=1 # 15 558 return r
559 560 561 ownershipLUT = { 562 'L':'local' 563 ,'R':'remote' 564 ,'C':'clear reservation' 565 } 566
567 -def dlmDecode(msg, validate=True):
568 ''' 569 Decode Data Link Management slot allocation for Base Station nmea message 570 571 572 >>> dlmDecode ('$AIDLM,0,A,L,0,2,7,540,L,4,1,7,250,L,2511,1,7,0,,,,,*40,1172787005.5') 573 {'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'} 574 575 @see: 62320-1/CDV 80/427/CDV page 79, A.1.9 576 577 ''' 578 579 if validate and not isChecksumValid(msg,verbose=True): 580 print 'FIX: this should be an exception in acaDecode. Bad checksum' 581 return False 582 #assert msg[0]=='$' 583 fields = msg.split(',') 584 r = {} 585 r['nmeaCmd']=fields[0][3:] # CBM 586 r['nmeaPrefix']=fields[0][1:3] 587 i = 1 588 589 r['seqNum'] = fields[i]; i+=1 590 r['aisChannel'] = fields[i]; i+=1 591 592 # reservations 593 r['ownership1'] = fields[i]; i+=1 # See ownership 594 r['startslot1'] = fields[i]; i+=1 595 r['numslots1'] = fields[i]; i+=1 596 r['timeout1'] = fields[i]; i+=1 597 r['incr1'] = fields[i]; i+=1 598 599 r['ownership2'] = fields[i]; i+=1 600 r['startslot2'] = fields[i]; i+=1 601 r['numslots2'] = fields[i]; i+=1 602 r['timeout2'] = fields[i]; i+=1 603 r['incr2'] = fields[i]; i+=1 604 605 r['ownership3'] = fields[i]; i+=1 606 r['startslot3'] = fields[i]; i+=1 607 r['numslots3'] = fields[i]; i+=1 608 r['timeout3'] = fields[i]; i+=1 609 r['incr3'] = fields[i]; i+=1 610 611 r['ownership4'] = fields[i]; i+=1 612 r['startslot4'] = fields[i]; i+=1 613 r['numslots4'] = fields[i]; i+=1 614 r['timeout4'] = fields[i]; i+=1 615 r['incr4'] = fields[i]; i+=1 616 617 return r
618
619 -def bbmEncode(totSent, sentNum, seqId, aisChan, msgId, data, numFillBits 620 ,prefix='xx',appendEOL=True 621 ,validate=True 622 ):
623 '''Encode a binary broadcast message. 624 625 I have no idea what this message says... 626 627 !AIVDM,1,1,,A,85NqMF1Kf=Vsdt`l;0bnfFjd<uQeT2p<vmIRTB=mM5mtIT;sUL2t,0*54,rs003669982,1172918061 628 629 >>> bbmEncode(1,1,0,3,8,'Fs[Ifs?:=2h:ec]dc3?HKI0f3?eFHa4[MGAMO6I2vqG0g',4) 630 '!xxBBM,1,1,0,3,8,Fs[Ifs?:=2h:ec]dc3?HKI0f3?eFHa4[MGAMO6I2vqG0g,4*32' 631 632 633 634 635 636 Here are the test messages from 62320-1 80/427/CDV Page 58, 10.2.2.1.2: 637 638 >>> bbmEncode(1,1,0,1,8,'7E3B3C3E7E',0,appendEOL=False) 639 '!xxBBM,1,1,0,1,8,7E3B3C3E7E,0*1F' 640 641 Make it go on both 642 643 >>> bbmEncode(1,1,0,3,8,'7E3B3C3E7E',0,appendEOL=False) 644 '!xxBBM,1,1,0,3,8,7E3B3C3E7E,0*1D' 645 646 647 648 @todo: put in some doc tests with know messages and what would be received as the VDM message(s) 649 @see: IEC-PAS 61162-100 80/330/PAS, Page 19 650 @param totSent: Total number of sentences needed for the message (1-9) 651 @type totSent: int 652 @param sentNum: Which sentence is this in the series (1-9) 653 @type sentNum: int 654 @param seqId: need to increment this for each message??!?!? (0-9) Linked to ABK 655 @type seqId: int 656 @param aisChan: AIS channel to use to send the message 657 0. No channel preference 658 1. AIS Channel A 659 2. AIS Channel B 660 3. Broadcast on both A and B 661 @type aisChan: str 662 @param msgId: AIS message 8 (binary broadcast message) or 14 (safety related broadcast) 663 @type msgId: int 664 @param data: Content of the binary data. First sentence must be 58 characters or less. 665 The rest can be up to 60 characters. 666 @param numFillBits: Number of bits of padding in the last character of the data (0-5) 667 @type numFillBits: int 668 @return: nmea string 669 @rtype: str 670 ''' 671 if validate: 672 # obsesive error checking follows 673 tot = int(totSent) 674 assert (0<tot and tot<=9) 675 num = int(sentNum) 676 assert (0<num and num<=9) 677 assert (num<=tot) 678 seq = int(seqId) 679 assert (0<=seq and seq <=9) 680 assert (int(aisChan) in range(0,5)) 681 assert (int(msgId) in (8,14)) 682 #if num==1: assert(len(data)<=58) 683 #else: assert(len(data)<=60) 684 # FIX: add validation of the characters in the data string 685 assert(int(numFillBits) in range(0,6)) 686 r = ','.join(('!'+prefix+'BBM',str(totSent),str(sentNum),str(seqId),str(aisChan),str(msgId),data,str(numFillBits))) 687 688 r += '*'+checksumStr(r) 689 if validate: assert(len(r)) <= 81 # Max nmea string length 690 return r
691 692
693 -def bbmDecode(msg,validate=True):
694 ''' 695 Decode a binary broadcast message NMEA string 696 697 >>> bbmDecode('!xxBBM,1,1,0,3,8,Fs[Ifs?:=2h:ec]dc3?HKI0f3?eFHa4[MGAMO6I2vqG0g,4*32') 698 {'numFillBits': '4', 'nmeaPrefix': 'xx', 'msgId': '8', 'aisChan': '3', 'data': 'Fs[Ifs?:=2h:ec]dc3?HKI0f3?eFHa4[MGAMO6I2vqG0g', 'seqId': '0', 'nmeaCmd': 'BBM', 'sentNum': '1', 'totSent': '1'} 699 700 @todo: make the doctest stable 701 @todo: doctests with known messages 702 @param msg: NMEA string of a CAB message 703 @type msg: str 704 @param validate: Set to False to turn off validation for speed. 705 @type validate: bool 706 @return: lookup table of key/values 707 @rtype: dict 708 @see: IEC-PAS 61162-100 80/330/PAS, Page 19 709 ''' 710 if validate and not isChecksumValid(msg,verbose=True): 711 print 'FIX: this should be an exception in bbmDecode. Bad checksum' 712 return False 713 fields=msg.split(',') 714 if validate and len(fields) < 8: # Allow for USCG station and timestamp 715 print 'FIX: this should be an exception in bbmDecode. wrong number of fields', len(fields) 716 print ' ', msg 717 return False 718 719 fields = msg.split(',') 720 r = {} 721 r['nmeaCmd']=fields[0][3:] # CBM # 0 722 r['nmeaPrefix']=fields[0][1:3] 723 i = 1 724 # FIX: convert from strings or not?? 725 r['totSent'] = fields[1]; i+= 1 726 r['sentNum'] = fields[2]; i+= 1 727 r['seqId'] = fields[3]; i+= 1 728 r['aisChan'] = fields[4]; i+= 1 729 r['msgId'] = fields[5]; i+= 1 730 r['data'] = fields[6]; i+= 1 731 r['numFillBits'] = fields[7].split('*')[0]; i+= 1 732 733 return r
734
735 -def bcfDecode(msg,validate=True):
736 ''' 737 Decode a General Base Station Configuration NMEA string. 738 739 >>> bcfDecode('$AIBCF,12345,7,4731.0,N,05249.0,W,1,2087,2088,2087,2088,1,1,3,0,AI*51') 740 {'posAccuracy': '1', 'nmeaPrefix': 'AI', 'TxChanB': '2088', 'mmsi': '12345', 'RepeatIndicator': '0', 'lon': -5249.0, 'PowerB': '1', 'posSrc': '7', 'nmeaCmd': 'BCF', 'PowerA': '1', 'BaseStationTalkerID': 'AI', 'RxChanB': '2088', 'lat': 4731.0, 'RxChanA': '2087', 'TxChanA': '2087', 'VDLretries': '3'} 741 742 @see: 62320-1/CDV 80/427/CDV, Page 76, A.1.6 743 @param msg: NMEA string of a CAB message 744 @type msg: str 745 @param validate: Set to False to turn off validation for speed. 746 @type validate: bool 747 @return: lookup table of key/values 748 @rtype: dict 749 ''' 750 751 if validate and not isChecksumValid(msg,verbose=True): 752 print 'FIX: this should be an exception in bcfDecode. Bad checksum' 753 return False 754 fields=msg.split(',') 755 if validate and len(fields) < 16: # Allow for USCG station and timestamp 756 print 'FIX: this should be an exception in bcfDecode. wrong number of fields', len(fields) 757 print ' ', msg 758 return False 759 760 fields = msg.split(',') 761 r = {} 762 r['nmeaCmd']=fields[0][3:] # Had better be BCF 763 r['nmeaPrefix']=fields[0][1:3] 764 i = 1 765 r['mmsi'] = fields[i]; i+= 1 # AKA UserID 766 r['posSrc'] = fields[i]; i+= 1 767 768 lat=fields[i]; i+= 1 769 latNS=fields[i]; i+= 1 770 if lat == '': r['lat'] = '' 771 else: 772 lat = float(lat) 773 if 'S'==latNS: lat = -lat 774 r['lat']=lat 775 776 lon = fields[i]; i+= 1 777 lonEW = fields[i]; i+= 1 778 if lon == '': r['lon'] = '' 779 else: 780 lon = float(lon) 781 if 'W'==lonEW: lon = -lon 782 r['lon'] = lon 783 784 r['posAccuracy'] = fields[i]; i+= 1 785 r['RxChanA'] = fields[i]; i+= 1 786 r['RxChanB'] = fields[i]; i+= 1 787 r['TxChanA'] = fields[i]; i+= 1 788 r['TxChanB'] = fields[i]; i+= 1 789 r['PowerA'] = fields[i]; i+= 1 790 r['PowerB'] = fields[i]; i+= 1 791 r['VDLretries'] = fields[i]; i+= 1 792 r['RepeatIndicator'] = fields[i]; i+= 1 793 r['BaseStationTalkerID'] = fields[i].split('*')[0] 794 795 return r
796 797 798 posSrcLUT = { 799 0:'surveyed' 800 ,1:'internal EPFD in use' 801 ,2:'external EPFD in use' 802 ,3:'internal EPFD in use with auto fallback to surveyed' 803 ,4:'internal EPFD in use with auto fallback to external EPFD' 804 ,5:'external EPFD in use with auto fallback to surveyed' 805 ,6:'external EPFD in use with auto fallback to internal EPFD' 806 ,7:'FIX: What exactly is 7 supposed to be... not in the source list in item 2.' 807 } 808 ''' 809 Position source used in a BCF message 810 811 @note: EPFD = el;ectronic position fixing device 812 @see: 62320-1 80/427/CDV Page 76 A.1.6 813 ''' 814
815 -def bcfEncode(mmsi='',posSrc='' 816 ,lat='',latNS='' 817 ,lon='',lonEW='' 818 ,posAccuracy='' 819 820 ,RxChanA='',RxChanB='' 821 ,TxChanA='',TxChanB='' 822 ,PowerA ='',PowerB ='' 823 824 ,VDLretries='' 825 ,RepeatIndicator='' 826 ,BaseStationTalkerID='' 827 ,prefix='AI',appendEOL=True 828 ,validate=True 829 ):
830 ''' 831 Enocde a General Base Station Configuation Message. Defaults to not changing anything 832 833 >>> bcfEncode(appendEOL=False) 834 '!AIBCF,,,,,,,,,,,,,,,,AI*47' 835 836 >>> bcfEncode(12345,7,4731.0,'N',5249.0,'W',1,2087,2088,2087,2088,1,1,3,0,'AI',appendEOL=False) 837 '!AIBCF,12345,7,4731.0,N,5249.0,W,1,2087,2088,2087,2087,1,1,3,0,AI*6E' 838 839 This is what the L-3 base station returns this. I am not sure what the format of the position is. 840 841 '$AIBCF,12345,7,4731.0,N,05249.0,W,1,2087,2088,2087,2088,1,1,3,0,AI*51' 842 843 844 Set to low power: 845 846 >>> bcfEncode(PowerA=powerEncode['low'],PowerB=powerEncode['low'],appendEOL=False) 847 '!AIBCF,,,,,,,,,,,,1,1,,,AI*47' 848 849 My MMSI that is registered for testing at UNH 850 851 >>> bcfEncode(mmsi=338040883, appendEOL=False,prefix='L3') 852 '!L3BCF,338040883,,,,,,,,,,,,,,,L3*78' 853 854 855 @see: 62320-1/CDV 80/427/CDV, Page 76, A.1.6 856 @param mmsi: UserID for the base station 857 @type mmsi: int 858 @param posSrc: See posSrcLUT. 0..6 859 @type posSrc: int 860 @param lat: Surveyed latitude position. FIX: how is this encoded!!?!?! 861 @type lat: float 862 @param latNS: N or S 863 @type latNS: str(1) 864 @param lon: Suveyed longitude position. FIX: how is this encoded!!?!?! 865 @type lon: float 866 @param lonEW: E or W 867 @type lonEW: str(1) 868 @param posAccuracy: 0 for low, 1 for hight 869 @type posAccuracy: int 870 @param RxChanA: Receive channel to use (default is 2087) 871 @type RxChanA: int 872 @param RxChanB: Receive channel to use (default is 2088) 873 @type RxChanB: int 874 @param TxChanA: Transmit channel to use (default is 2087) 875 @type TxChanA: int 876 @param TxChanB: Transmit channel to use (default is 2089) 877 @type TxChanB: int 878 @param PowerA: Transmit power for channel A - 0 high (12.5 W), 1 low (Nominal 2 watts). 879 FIX: Seems there is a disagreement between specs on the power levels. 880 2 or 5 watts for low? 2..9 reservered 881 @type PowerA: int 882 @param PowerB:Transmit power for channel A - 0 high (12.5 W), 1 low (Nominal 2 watts). 883 FIX: Seems there is a disagreement between specs on the power levels. 884 2 or 5 watts for low? 2..9 reservered 885 @type PowerB: int 886 @param VDLretries: FIX: what does this mean? 887 @type VDLretries: int 888 @param RepeatIndicator: ? 889 @type RepeatIndicator: int 890 @param BaseStationTalkerID: Usually AI. The prefix that does before NMEA string identifiers 891 @type BaseStationTalkerID: str(2) 892 @param prefix: Vendor specific prefix. FIX: what should be used here? 893 @type prefix: Two letters 894 @param appendEOL: Do you want a DOS end of line appended? 895 @type appendEOL: bool 896 @param validate: Set to true to validate the message 897 @type validate: bool 898 ''' 899 900 if validate: 901 #print 'FIX: write validation code' 902 pass 903 904 905 r = ('!'+prefix+'BCF',str(mmsi),str(posSrc) 906 ,str(lat),str(latNS) 907 ,str(lon),str(lonEW) 908 ,str(posAccuracy) 909 ,str(RxChanA),str(RxChanB) 910 ,str(TxChanA),str(TxChanA) 911 ,str(PowerA),str(PowerB) 912 ,str(VDLretries),str(RepeatIndicator),str(prefix) 913 ) 914 915 rStr = ','.join(r) 916 rStr += '*'+checksumStr(rStr) 917 if validate: assert(len(rStr)<=81) 918 if appendEOL: rStr+=EOL 919 920 return rStr
921 922 923 ###################################################################### 924 if __name__=='__main__': 925 from optparse import OptionParser 926 parser = OptionParser(usage="%prog [options]", 927 version="%prog "+__version__) 928 parser.add_option('--test','--doc-test',dest='doctest',default=False,action='store_true', 929 help='run the documentation tests') 930 parser.add_option('-v','--verbose',dest='verbose',default=False,action='store_true', 931 help='run the tests run in verbose mode') 932 (options,args) = parser.parse_args() 933 934 success=True 935 936 if options.doctest: 937 import os; print os.path.basename(sys.argv[0]), 'doctests ...', 938 argvOrig = sys.argv 939 sys.argv= [sys.argv[0]] 940 if options.verbose: sys.argv.append('-v') 941 import doctest 942 numfail,numtests=doctest.testmod() 943 if numfail==0: print 'ok' 944 else: 945 print 'FAILED' 946 success=False 947 sys.argv = argvOrig # Restore the original args 948 del argvOrig # hide from epydoc 949 950 if not success: 951 sys.exit('Something Failed') 952 953 del success # Hide success from epydoc 954