├── .gitattributes ├── README.md ├── .gitignore └── NMEA0183.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NMEA0183 2 | ======== 3 | 4 | NMEA0183 is a python script that enables connection to serial NMEA0183 devices and interpretation of incoming data. 5 | 6 | There are other python NMEA0183 scripts available, but this one is aiming to provide a more wholistic approach to the task. Most of the scripts do not provide a means of connecting to actual NMEA device. This one does. 7 | 8 | Usually, a knowledge of the incoming sentences is also required. With this script, incoming data is stored in variable 'clusters' that group similar bits of data into easily understood sets. 9 | 10 | This project is still incomplete. There are various NMEA data types still missing. I've basically included the ones I needed at the moment. More will be coming. It is not difficult to add additional data types if you'de like to do this yourself. 11 | 12 | Requirements 13 | ------------ 14 | 15 | - pyserial is required for serial connections - http://pyserial.sourceforge.net/ 16 | - Requires a serial NMEA0183 device. 17 | 18 | 19 | How to Use 20 | ---------- 21 | 22 | Here is an example of its use: 23 | 24 | ```python 25 | import NMEA0183 26 | 27 | serial_location = '/dev/ttyUSB0' 28 | serial_baudrate = 4800 29 | serial_timeout = 5 30 | 31 | #Provides the required serial device info 32 | nmea = NMEA0183(serial_location,serial_baudrate,serial_timeout) 33 | 34 | #Starts the serial connection 35 | nmea.start() 36 | 37 | #Checks if there is a valid connection 38 | if nmea.exit == False: 39 | print 'Connection!' 40 | 41 | #More info on data names below 42 | #Different data types require different devices...obviously... 43 | #Some examples... 44 | 45 | #GPS data 46 | print nmea.data_gps['lat'] 47 | print nmea.data_gps['lon'] 48 | 49 | #Depth data 50 | print nmea.data_depth['feet'] 51 | 52 | #Weather data 53 | print nmea.data_weather['wind_angle'] 54 | print nmea.data_weather['water_temp'] 55 | 56 | #Rudder data 57 | print nmea.data_rudder['stbd_angle'] 58 | 59 | #Quit the NMEA connection 60 | nmea.quit() 61 | 62 | else: 63 | print 'No connection!' 64 | 65 | ``` 66 | 67 | Current NMEA Sentences 68 | ---------------------- 69 | 70 | ##### GPS Data 71 | - Latitude in decimal format = data_gps['lat'] 72 | - Longitude in decimal format = data_gps['lon'] 73 | - Speed in knots = data_gps['speed'] 74 | - Course track in degrees = data_gps['track'] 75 | - UTC in standard format, ie. 1994-11-05T13:15:30Z = data_gps['utc'] 76 | - Status, A is Valid, V is Invalid = data_gps['status'] 77 | 78 | ##### Depth Data 79 | You must understand the source of the depth data, as it could be coming from a waterline reading, or below waterline reading. 80 | 81 | - Depth in feet = data_depth['feet'] 82 | - Depth in meters = data_depth['meters'] 83 | - Depth in fathoms = data_depth['fathoms'] 84 | - Depth offset, positive is transducer to waterline, negative is transducer to keel = data_depth['offset'] 85 | 86 | ##### Weather Data 87 | 88 | - Wind angle = data_weather['wind_angle'] 89 | - Wind reference, R is relative, T is true = data_weather['wind_ref'] 90 | - Wind speed = data_weather['wind_speed'] 91 | - Wind unit, K/M/N = data_weather['wind_unit'] 92 | - Water temp = data_weather['water_temp'] 93 | - Water unit, C/F = data_weather['water_unit'] 94 | - Air temp = data_weather['air_temp'] 95 | - Air unit, C/F = data_weather['air_unit'] 96 | 97 | ##### Rudder Data 98 | 99 | - Starboard (or single) rudder angle = data_rudder['stdb_angle'] 100 | - Starboard (or single) status, A is valid, V is invalid = data_rudder['stdb_status'] 101 | - Port rudder angle = data_rudder['port_angle'] 102 | - Port status, A is valid, V is invalid = data_rudder['port_status'] 103 | 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | -------------------------------------------------------------------------------- /NMEA0183.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | import re 5 | import serial 6 | import binascii 7 | from threading import Thread 8 | 9 | 10 | class NMEA0183(): 11 | 12 | 13 | def __init__(self, location, baud_rate, timeout): 14 | ''' 15 | Initiates variables. 16 | 17 | Keyword arguments: 18 | location -- the location of the serial connection 19 | baud_rate -- the baud rate of the connection 20 | timeout -- the timeout of the connection 21 | 22 | ''' 23 | self.exit = False 24 | self.location = location 25 | self.baud_rate = baud_rate 26 | self.timeout = timeout 27 | self.serial_dev = None 28 | self.serial_data = None 29 | 30 | #Ready the GPS variables 31 | self.data_gps = {'lat': float(0.0), 'lon': float(0.0), 'speed': float(0.0), 'track': float(0.0), 'utc': '0.0', 'status': 'A'} 32 | #Ready the depth variables 33 | self.data_depth = {'feet': float(0.0), 'meters': float(0.0), 'fathoms': float(0.0), 'offset': float(0.0)} 34 | #Ready the weather variables 35 | self.data_weather = {'wind_angle': float(0.0), 'wind_ref': 'R', 'wind_speed': float(0.0), 'wind_unit': 'K', 'water_temp': float(0.0), 'water_unit': 'C', 'air_temp': float(0.0), 'air_unit': 'C'} 36 | #Ready the rudder variables 37 | self.data_rudder = {'stdb_angle': float(0.0), 'stdb_status': 'A', 'port_angle': float(0.0), 'port_status': 'A'} 38 | #Ready the turn variables 39 | self.data_turn = {'rate': float(0.0), 'status': 'A'} 40 | 41 | def start(self): 42 | ''' 43 | Creates a thread to read serial connection data. 44 | ''' 45 | try: 46 | self.serial_dev = serial.Serial(self.location, self.baud_rate, self.timeout) 47 | serial_thread = Thread(None,self.read_thread,None,()) 48 | serial_thread.start() 49 | except: 50 | self.quit() 51 | 52 | def read_thread(self): 53 | ''' 54 | The thread used to read incoming serial data. 55 | ''' 56 | dat_new = '' 57 | dat_old = '' 58 | #Loops until the connection is broken, or is instructed to quit 59 | try: 60 | while self.is_open(): 61 | #Instructed to quit 62 | if self.exit: 63 | break 64 | if dat_new: 65 | dat_old = dat_new 66 | dat_new = '' 67 | dat_old = dat_old + self.buffer() 68 | if re.search("\r\n", dat_old): 69 | try: 70 | self.serial_data, dat_new = dat_old.split("\r\n") 71 | except: 72 | pass 73 | #The checksum is correct, so the data will be deconstructed 74 | if self.checksum(self.serial_data): 75 | self.check_type() 76 | dat_old = '' 77 | except: 78 | self.quit() 79 | 80 | def is_open(self): 81 | ''' 82 | Checks whether the serial connection is still open. 83 | ''' 84 | return self.serial_dev.isOpen() 85 | 86 | def buffer(self): 87 | ''' 88 | Creates a buffer for serial data reading. Avoids reading lines for better performance. 89 | ''' 90 | dat_cur = self.serial_dev.read(1) 91 | x = self.serial_dev.inWaiting() 92 | if x: dat_cur = dat_cur + self.serial_dev.read(x) 93 | return dat_cur 94 | 95 | def make_checksum(self,data): 96 | ''' 97 | Calculates a checksum from a NMEA sentence. 98 | 99 | Keyword arguments: 100 | data -- the NMEA sentence to create 101 | 102 | ''' 103 | csum = 0 104 | i = 0 105 | # Remove ! or $ and *xx in the sentence 106 | data = data[1:data.rfind('*')] 107 | while (i < len(data)): 108 | input = binascii.b2a_hex(data[i]) 109 | input = int(input,16) 110 | #xor 111 | csum = csum ^ input 112 | i += 1 113 | return csum 114 | 115 | def checksum(self,data): 116 | ''' 117 | Reads the checksum of an NMEA sentence. 118 | 119 | Keyword arguments: 120 | data -- the NMEA sentence to check 121 | 122 | ''' 123 | try: 124 | # Create an integer of the two characters after the *, to the right 125 | supplied_csum = int(data[data.rfind('*')+1:data.rfind('*')+3], 16) 126 | except: 127 | return '' 128 | # Create the checksum 129 | csum = self.make_checksum(data) 130 | # Compare and return 131 | if csum == supplied_csum: 132 | return True 133 | else: 134 | return False 135 | 136 | def check_type(self): 137 | ''' 138 | Reads the sentence type, and directs the data to its respective function. 139 | ''' 140 | self.serial_data = self.serial_data.split('*') 141 | #Splits up the NMEA data by comma 142 | self.serial_data = self.serial_data[0].split(',') 143 | #Incoming serial data is GPS related 144 | if self.serial_data[0][3:6] == 'RMC': 145 | self.nmea_rmc() 146 | #Incoming serial data is depth related 147 | elif self.serial_data[0][3:6] in ('DBS','DBT', 'DBK'): 148 | self.nmea_dbs() 149 | #Incoming serial data is depth related 150 | elif self.serial_data[0][3:6] == 'DPT': 151 | self.nmea_dpt() 152 | #Incoming serial data is weather related 153 | elif self.serial_data[0][3:6] == 'MWV': 154 | self.nmea_mwv() 155 | #Incoming serial data is weather related 156 | elif self.serial_data[0][3:6] == 'MTW': 157 | self.nmea_mtw() 158 | #Incoming serial data is weather related 159 | elif self.serial_data[0][3:6] == 'MTA': 160 | self.nmea_mta() 161 | #Incoming serial data is rudder related 162 | elif self.serial_data[0][3:6] == 'RSA': 163 | self.nmea_rsa() 164 | #Incoming serial data is turn related 165 | elif self.serial_data[0][3:6] == 'ROT': 166 | self.nmea_rot() 167 | elif self.serial_data[0][3:6] == 'XDR': 168 | self.nmea_rot() 169 | 170 | def nmea_rmc(self): 171 | ''' 172 | Deconstructs NMEA gps readings. 173 | ''' 174 | self.data_gps['utc'] = self.gps_nmea2utc() 175 | self.data_gps['status'] = self.serial_data[2] 176 | self.data_gps['lat'] = self.gps_nmea2dec(0) 177 | self.data_gps['lon'] = self.gps_nmea2dec(1) 178 | self.data_gps['speed'] = float(self.serial_data[7]) 179 | self.data_gps['track'] = float(self.serial_data[8]) 180 | 181 | def nmea_dbs(self): 182 | ''' 183 | Deconstructs NMEA depth readings. 184 | ''' 185 | self.data_depth['feet'] = self.serial_data[1] 186 | self.data_depth['meters'] = self.serial_data[3] 187 | self.data_depth['fathoms'] = self.serial_data[5] 188 | self.data_depth['offset'] = 0 189 | 190 | def nmea_dpt(self): 191 | ''' 192 | Deconstructs NMEA depth readings. 193 | ''' 194 | self.data_depth['meters'] = self.serial_data[1] 195 | self.data_depth['offset'] = self.serial_data[2] 196 | 197 | def nmea_mwv(self): 198 | ''' 199 | Deconstructs NMEA weather readings. 200 | ''' 201 | self.data_weather['wind_angle'] = self.serial_data[1] 202 | self.data_weather['wind_ref'] = self.serial_data[2] 203 | self.data_weather['wind_speed'] = self.serial_data[3] 204 | self.data_weather['wind_unit'] = self.serial_data[4] 205 | 206 | def nmea_mtw(self): 207 | ''' 208 | Deconstructs NMEA water readings. 209 | ''' 210 | self.data_weather['water_temp'] = self.serial_data[1] 211 | self.data_weather['water_unit'] = self.serial_data[2] 212 | 213 | def nmea_mta(self): 214 | ''' 215 | Deconstructs NMEA air temp readings. 216 | ''' 217 | self.data_weather['air_temp'] = self.serial_data[1] 218 | self.data_weather['air_unit'] = self.serial_data[2] 219 | 220 | def nmea_rsa(self): 221 | ''' 222 | Deconstructs NMEA rudder angle readings. 223 | ''' 224 | self.data_rudder['stbd_angle'] = self.serial_data[1] 225 | self.data_rudder['stdb_status'] = self.serial_data[2] 226 | self.data_rudder['port_angle'] = self.serial_data[3] 227 | self.data_rudder['port_status'] = self.serial_data[4] 228 | 229 | def nmea_rot(self): 230 | ''' 231 | Deconstructs NMEA rudder angle readings. 232 | ''' 233 | self.data_turn['rate'] = self.serial_data[1] 234 | self.data_turn['status'] = self.serial_data[2] 235 | 236 | def nmea_xdr(self): 237 | ''' 238 | Deconstructs NMEA weather readings. 239 | ''' 240 | if self.serial_data[0][0:2] == '$WI': 241 | self.data_weather['wind_angle'] = self.serial_data[1] 242 | self.data_weather['wind_ref'] = self.serial_data[2] 243 | self.data_weather['wind_speed'] = self.serial_data[3] 244 | self.data_weather['wind_unit'] = self.serial_data[4] 245 | 246 | def gps_nmea2dec(self,type): 247 | ''' 248 | Converts NMEA lat/long format to decimal format. 249 | 250 | Keyword arguments: 251 | type -- tells whether it is a lat or long. 0=lat,1=long 252 | 253 | ''' 254 | #Represents the difference in list position between lat/long 255 | x = type*2 256 | #Converts NMEA format to decimal format 257 | data = float(self.serial_data[3+x][0:2+type]) + float(self.serial_data[3+x][2+type:9+type])/60 258 | #Adds negative value based on N/S, W/E 259 | if self.serial_data[4+x] == 'S': data = data*(-1) 260 | elif self.serial_data[4+x] == 'W': data = data*(-1) 261 | return data 262 | 263 | def gps_nmea2utc(self): 264 | ''' 265 | Converts NMEA utc format to more standardized format. 266 | ''' 267 | time = self.serial_data[1][0:2] + ':' + self.serial_data[1][2:4] + ':' + self.serial_data[1][4:6] 268 | date = '20' + self.serial_data[9][4:6] + '-' + self.serial_data[9][2:4] + '-' + self.serial_data[9][0:2] 269 | return date + 'T' + time + 'Z' 270 | 271 | def quit(self): 272 | ''' 273 | Enables quiting the serial connection. 274 | ''' 275 | self.exit = True 276 | --------------------------------------------------------------------------------