1
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
32 import time, sys
33
34
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
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
73
74
75
76 end = data.find('*')
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
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
92
93
94 nmeaChecksumRegExStr = r"""\*[0-9A-F][0-9A-F]"""
95 nmeaChecksumRE = re.compile(nmeaChecksumRegExStr)
96
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
126
127
128 if nmeaStr[-3]!='*':
129 print 'FIX: warning... bad nmea string'
130 return False
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)
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'):
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
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):
241
242 print 'FIX: this should be an exception in cabDecode. wrong number of fields'
243 return False
244
245
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
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
312
313
314
315 rStr = '$' + prefix+'BSQ,'+query
316 rStr += '*' + checksumStr(rStr)
317
318 if appendEOL: rStr += EOL
319 return rStr
320
321
322
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'
361 ,chanB='2088',chanBbandwidth='0'
362 ,txrxMode=''
363 ,power=''
364 ,infosrc=''
365 ,timeinuse=''
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
430 if chanAbandwidth!='': assert int(chanAbandwidth) in (0,1)
431 if chanB !='': assert int(chanB) > 2000 and int(chanB) <= 2290
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')
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
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
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])
479 assert len(fields[3])==1
480 if fields[3]=='S': lat= -1 * lat
481 else: lat = None
482 r['north']=lat
483 del lat
484
485 if len(fields[4])>0:
486 lon = float(fields[4])
487 assert len(fields[5])==1
488 if fields[5]=='W': lat= -1 * lat
489 else: lon = None
490 r['east']=lon
491 del lon
492
493 if len(fields[6])>0:
494 lat = float(fields[6])
495 assert len(fields[7])==1
496 if fields[7]=='S': lat= -1 * lat
497 else: lat = None
498 r['south']=lat
499 del lat
500
501 if len(fields[8])>0:
502 lon = float(fields[8])
503 assert len(fields[9])==1
504 if fields[9]=='W': lat= -1 * lat
505 else: lon = None
506 r['west']=lon
507 del lon
508
509 r['transitionSize'] = fields[10]
510 r['chanA'] = fields[11]
511 r['chanAbandwidth'] = fields[12]
512 r['chanB'] = fields[13]
513 r['chanBbandwidth'] = fields[14]
514 r['txrxMode'] = fields[15]
515 r['power'] = fields[16]
516 r['infosrc'] = fields[17]
517 r['inuse'] = fields[18]
518 r['timeinuse'] = fields[19].split('*')[0]
519 return r
520
521
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
537 fields = msg.split(',')
538 r = {}
539 r['nmeaCmd']=fields[0][3:]
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
558 return r
559
560
561 ownershipLUT = {
562 'L':'local'
563 ,'R':'remote'
564 ,'C':'clear reservation'
565 }
566
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
583 fields = msg.split(',')
584 r = {}
585 r['nmeaCmd']=fields[0][3:]
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
593 r['ownership1'] = fields[i]; i+=1
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
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
683
684
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
690 return r
691
692
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:
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:]
722 r['nmeaPrefix']=fields[0][1:3]
723 i = 1
724
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
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:
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:]
763 r['nmeaPrefix']=fields[0][1:3]
764 i = 1
765 r['mmsi'] = fields[i]; i+= 1
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
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
948 del argvOrig
949
950 if not success:
951 sys.exit('Something Failed')
952
953 del success
954