1
2
3 __version__ = '$Revision: 5698 $'.split()[1]
4 __date__ = '$Date: 2007-03-03 12:58:28 -0500 (Sat, 03 Mar 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__ 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
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 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
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):
235
236 print 'FIX: this should be an exception in cabDecode. wrong number of fields'
237 return False
238
239
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
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
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
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])
313 assert len(fields[3])==1
314 if fields[3]=='S': lat= -1 * lat
315 else: lat = None
316 r['north']=lat
317 del lat
318
319 if len(fields[4])>0:
320 lon = float(fields[4])
321 assert len(fields[5])==1
322 if fields[5]=='W': lat= -1 * lat
323 else: lon = None
324 r['east']=lon
325 del lon
326
327 if len(fields[6])>0:
328 lat = float(fields[6])
329 assert len(fields[7])==1
330 if fields[7]=='S': lat= -1 * lat
331 else: lat = None
332 r['south']=lat
333 del lat
334
335 if len(fields[8])>0:
336 lon = float(fields[8])
337 assert len(fields[9])==1
338 if fields[9]=='W': lat= -1 * lat
339 else: lon = None
340 r['west']=lon
341 del lon
342
343 r['transitionSize'] = fields[10]
344 r['chanA'] = fields[11]
345 r['chanAbandwidth'] = fields[12]
346 r['chanB'] = fields[13]
347 r['chanBbandwidth'] = fields[14]
348 r['txrxMode'] = fields[15]
349 r['power'] = fields[16]
350 r['infosrc'] = fields[17]
351 r['inuse'] = fields[18]
352 r['timeinuse'] = fields[19].split('*')[0]
353 return r
354
355
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
371 fields = msg.split(',')
372 r = {}
373 r['nmeaCmd']=fields[0][3:]
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
392 return r
393
394
395 ownershipLUT = {
396 'L':'local'
397 ,'R':'remote'
398 ,'C':'clear reservation'
399 }
400
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
417 fields = msg.split(',')
418 r = {}
419 r['nmeaCmd']=fields[0][3:]
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
427 r['ownership1'] = fields[i]; i+=1
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 bbmEncode(totSent, sentNum, seqId, aisChan, msgId, data, numFillBits
454 ,prefix='xx',appendEOL=True
455 ,validate=True
456 ):
457 '''Encode a binary broadcast message.
458
459 I have no idea what this message says...
460
461 !AIVDM,1,1,,A,85NqMF1Kf=Vsdt`l;0bnfFjd<uQeT2p<vmIRTB=mM5mtIT;sUL2t,0*54,rs003669982,1172918061
462
463 >>> bbmEncode(1,1,0,3,8,'Fs[Ifs?:=2h:ec]dc3?HKI0f3?eFHa4[MGAMO6I2vqG0g',4)
464 '!xxBBM,1,1,0,3,8,Fs[Ifs?:=2h:ec]dc3?HKI0f3?eFHa4[MGAMO6I2vqG0g,4*32'
465
466 @todo: put in some doc tests with know messages and what would be received as the VDM message(s)
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
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
503 assert(int(numFillBits) in range(0,6))
504 r = ','.join(('!'+prefix+'BBM',str(totSent),str(sentNum),str(seqId),str(aisChan),str(msgId),data,str(numFillBits)))
505
506 r += '*'+checksumStr(r)
507 if validate: assert(len(r)) <= 81
508 return r
509
510
512 '''
513 Decode a binary broadcast message NMEA string
514
515 >>> bbmDecode('!xxBBM,1,1,0,3,8,Fs[Ifs?:=2h:ec]dc3?HKI0f3?eFHa4[MGAMO6I2vqG0g,4*32')
516 {'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'}
517
518 @todo: make the doctest stable
519 @todo: doctests with known messages
520 @param msg: NMEA string of a CAB message
521 @type msg: str
522 @param validate: Set to False to turn off validation for speed.
523 @type validate: bool
524 @return: lookup table of key/values
525 @rtype: dict
526 @see: IEC-PAS 61162-100 80/330/PAS, Page 19
527 '''
528 if validate and not isChecksumValid(msg,verbose=True):
529 print 'FIX: this should be an exception in cabDecode. Bad checksum'
530 return False
531 fields=msg.split(',')
532 if validate and len(fields) < 8:
533 print 'FIX: this should be an exception in cabDecode. wrong number of fields', len(fields)
534 print ' ', msg
535 return False
536
537 r = {}
538
539 fields = msg.split(',')
540 r = {}
541 r['nmeaCmd']=fields[0][3:]
542 r['nmeaPrefix']=fields[0][1:3]
543 i = 1
544
545 r['totSent'] = fields[1]; i+= 1
546 r['sentNum'] = fields[2]; i+= 1
547 r['seqId'] = fields[3]; i+= 1
548 r['aisChan'] = fields[4]; i+= 1
549 r['msgId'] = fields[5]; i+= 1
550 r['data'] = fields[6]; i+= 1
551 r['numFillBits'] = fields[7].split('*')[0]; i+= 1
552
553 return r
554
555
556 if __name__=='__main__':
557 from optparse import OptionParser
558 myparser = OptionParser(usage="%prog [options]",
559 version="%prog "+__version__)
560 myparser.add_option('--test','--doc-test',dest='doctest',default=False,action='store_true',
561 help='run the documentation tests')
562 myparser.add_option('-v','--verbose',dest='verbose',default=False,action='store_true',
563 help='run the tests run in verbose mode')
564 (options,args) = myparser.parse_args()
565
566 success=True
567
568 if options.doctest:
569 import os; print os.path.basename(sys.argv[0]), 'doctests ...',
570 argvOrig = sys.argv
571 sys.argv= [sys.argv[0]]
572 if options.verbose: sys.argv.append('-v')
573 import doctest
574 numfail,numtests=doctest.testmod()
575 if numfail==0: print 'ok'
576 else:
577 print 'FAILED'
578 success=False
579 sys.argv = argvOrig
580 del argvOrig
581
582 if not success:
583 sys.exit('Something Failed')
584
585 del success
586