openrvdas.logger.transforms.nmea_transform
Take in a dict of various values and emit NMEA strings appropriate for them. NMEATransform is a thin wrapper around a set of individual NMEA message-generating transforms.
Each transform's __init__(self) method should expect a single 'kwargs' dict as its initialization argument, in which it will search for the keyword args it needs in order to function. If it does not find the necessary args, then rather than throwing an error, its transform method should just always return None.
Each transform's transform(record) method should expect its input in standard OpenRVDAS dict format:
{'timestamp':5345345, 'fields':{'field1': value1, 'field2':value2,...}}
Each transform should return a (possibly empty) list of NMEA strings.
If they're not too terribly ugly, the NMEA transforms should be placed in this file so as to minimize the risk that they may be inadvertently used elsewhere.
1#!/usr/bin/env python3 2"""Take in a dict of various values and emit NMEA strings appropriate 3for them. NMEATransform is a thin wrapper around a set of individual 4NMEA message-generating transforms. 5 6Each transform's __init__(self) method should expect a single 'kwargs' 7dict as its initialization argument, in which it will search for the 8keyword args it needs in order to function. If it does not find the 9necessary args, then rather than throwing an error, its transform 10method should just always return None. 11 12Each transform's transform(record) method should expect its input in 13standard OpenRVDAS dict format: 14 15{'timestamp':5345345, 'fields':{'field1': value1, 'field2':value2,...}} 16 17Each transform should return a (possibly empty) list of NMEA strings. 18 19If they're not too terribly ugly, the NMEA transforms should be placed 20in this file so as to minimize the risk that they may be inadvertently 21used elsewhere. 22""" 23# flake8: noqa E501 - ignore long comment lines that describe formats 24 25import logging 26import importlib 27import inspect 28 29# For efficient checksum code 30from functools import reduce 31from operator import xor 32 33from logger.transforms.transform import Transform # noqa: E402 34 35 36############################ 37def checksum(source): 38 """Return hex checksum for source string.""" 39 return '%02X' % reduce(xor, (ord(c) for c in source)) 40 41 42################################################################################ 43class NMEATransform(Transform): 44 """Call our various component transforms and generate NMEA strings from them. 45 """ 46 47 def __init__(self, nmea_list: list = [], **kwargs): 48 """ 49 nmea_list 50 List of the nmea transforms that will be used. 51 **kwargs 52 Arugments needed for the nmea transforms, see transforms below for what will be included. 53 """ 54 super().__init__(**kwargs) # processes 'quiet' and type hints 55 56 self.transforms = [] 57 58 # If nmea_list is not given as list, force it into one 59 if not isinstance(nmea_list, list): 60 nmea_list = [nmea_list] 61 62 if not nmea_list: 63 self.transforms = [MWDTransform(kwargs), XDRTransform(kwargs)] 64 return 65 66 class_module_name = 'logger.transforms.nmea_transform' 67 module = importlib.import_module(class_module_name) 68 69 # Get all classes within this file 70 classes = [cls_name for cls_name, cls_obj in inspect.getmembers(module) if 71 inspect.isclass(cls_obj)] 72 73 for transform in nmea_list: 74 if transform in classes: 75 class_const = getattr(module, transform) 76 self.transforms.append(class_const(kwargs)) 77 else: 78 logging.error('%s is not in classes %s', transform, classes) 79 80 81 ############################ 82 def transform(self, record): 83 """Expect a record dict (with 'timestamp' and 'fields' keys.""" 84 results = [] 85 86 # Do we have more than one record here? Normalize so that 87 # following code assumes a list of records. 88 if not type(record) is list: 89 record = [record] 90 91 for single_record in record: 92 for t in self.transforms: 93 result = t.transform(single_record) 94 logging.debug('transform %s: %s', t, result) 95 96 # Transforms may return zero, one or more results 97 if not result: 98 continue 99 elif type(result) is list: 100 results.extend(result) 101 else: 102 results.append(result) 103 104 # Just keep the results that are non-empty 105 pruned_results = [r for r in results if r] 106 107 # Return None, a single result or a list of results 108 if len(pruned_results) == 0: 109 return None 110 elif len(pruned_results) == 1: 111 return pruned_results[0] 112 else: 113 return pruned_results 114 115 116################################################################################ 117"""MWD - Wind Direction & Speed 118$--MWD, x.x,T,x.x,M,x.x,N,x.x,M*hh<CR><LF> 119 120$--: Talker identifier* 121MWD: Sentence formatter* 122x.x,T: Wind direction, 0° to 359° true* 123x.x,M: Wind direction, 0° to 359° magnetic* 124x.x,N: Wind speed, knots* 125x.x,M: Wind speed, meters/second* 126*hh: Checksum* 127 128We get true wind direction ab initio, but if we don't have access to 129vessel's magnetic variation, we can't generate the magnetic wind 130direction, so omit if not available. 131""" 132################################################################################ 133 134 135class MWDTransform: 136 """Output a NMEA MWD string, given true wind and (when available) 137 magnetic variation. 138 """ 139 140 def __init__(self, kwargs): 141 """ 142 Look for these keys in the kwargs dict: 143 ``` 144 true_wind_dir_field 145 Field name to look for true wind direction 146 true_wind_speed_kt_field 147 Field name to look for wind speed in knots. Either this 148 or true_wind_speed_ms_field must be non-empty. 149 true_wind_speed_ms_field 150 Field name to look for wind speed in meters per second. 151 Either this or true_wind_speed_kt_field must be non-empty. 152 magnetic_variation_field 153 Vessel magnetic variation. If omitted, only true winds 154 will be emitted. 155 mwd_talker_id 156 Should be format '--MWD' to identify the instrument 157 that's creating the message. 158 ``` 159 """ 160 self.true_wind_dir_field = kwargs.get('true_wind_dir_field') 161 self.true_wind_speed_kt_field = kwargs.get('true_wind_speed_kt_field') 162 self.true_wind_speed_ms_field = kwargs.get('true_wind_speed_ms_field') 163 self.magnetic_variation_field = kwargs.get('magnetic_variation_field') 164 self.mwd_talker_id = kwargs.get('mwd_talker_id') 165 166 self.true_wind_dir = None 167 self.true_wind_speed_kt = None 168 self.true_wind_speed_ms = None 169 self.magnetic_variation = None 170 171 ############################ 172 def transform(self, record): 173 """Incorporate any useable fields in this record. If it gives us a 174 new MWD record, return it. 175 """ 176 # Check that we've got the right record type - it should be a 177 # single record. 178 if not record or type(record) is not dict: 179 logging.warning('Improper type for record: %s', type(record)) 180 return None 181 fields = record.get('fields') 182 if not fields: 183 logging.debug('MWDTransform got record with no fields: %s', record) 184 return None 185 186 # Grab any relevant values 187 self.true_wind_dir = fields.get(self.true_wind_dir_field, 188 self.true_wind_dir) 189 if self.true_wind_speed_kt_field: 190 self.true_wind_speed_kt = fields.get(self.true_wind_speed_kt_field, 191 self.true_wind_speed_kt) 192 if self.true_wind_speed_ms_field: 193 self.true_wind_speed_ms = fields.get(self.true_wind_speed_ms_field, 194 self.true_wind_speed_ms) 195 if self.magnetic_variation_field: 196 self.magnetic_variation = fields.get(self.magnetic_variation_field, 197 self.magnetic_variation) 198 199 # Do we have enough values to emit a record? If not, go home. 200 if self.true_wind_dir is None: 201 logging.debug('Not all required values present - skipping') 202 return None 203 if self.true_wind_speed_kt is None and self.true_wind_speed_ms is None: 204 logging.debug('Not all required values present - skipping') 205 return None 206 207 # Are we filling in meters per second from knots? 208 if self.true_wind_speed_ms_field is None and \ 209 self.true_wind_speed_kt_field and \ 210 self.true_wind_speed_kt is not None: 211 self.true_wind_speed_ms = self.true_wind_speed_kt * 0.514444 212 213 # Are we filling in knots from meters per second from? 214 if self.true_wind_speed_kt_field is None and \ 215 self.true_wind_speed_ms_field and \ 216 self.true_wind_speed_ms is not None: 217 self.true_wind_speed_kt = self.true_wind_speed_kt * 1.94384 218 219 # Do we have a magnetic variation? If so, provide mag winds, 220 # otherwise use an empty string. 221 if self.magnetic_variation is not None: 222 mag_winds = '%3.1f' % (self.true_wind_dir - self.magnetic_variation) 223 else: 224 mag_winds = '' 225 226 # Assemble string, compute checksum, and return it. 227 result_str = '%s,%3.1f,T,%s,M,%3.1f,N,%3.1f,M' % \ 228 (self.mwd_talker_id, self.true_wind_dir, mag_winds, 229 self.true_wind_speed_kt, self.true_wind_speed_ms) 230 checksum = reduce(xor, (ord(c) for c in result_str)) 231 return '$%s*%02X' % (result_str, checksum) 232 233 234################################################################################# 235"""Take in records and emit a NMEA XDR string, as per format: 236 237 $--XDR,a,x.x,a,c--c, ..... *hh<CR><LF> \\ 238Field Number: 2391) Transducer Type 2402) Measurement Data 2413) Units of measurement 2424) Name of transducer 243x) More of the same 244n) Checksum 245Example: 246$IIXDR,C,19.52,C,TempAir*19 247$IIXDR,P,1.02481,B,Barometer*29 248Measured Value | Transducer Type | Measured Data | Unit of measure | Transducer Name 249------------------------------------------------------------------------------------------------------ 250barometric | "P" pressure | 0.8..1.1 or 800..1100 | "B" bar | "Barometer" 251air temperature| "C" temperature | 2 decimals | "C" celsius | "TempAir" or "ENV_OUTAIR_T" 252pitch | "A" angle |-180..0 nose down 0..180 nose up | "D" degrees | "PTCH" or "PITCH" 253rolling | "A" angle |-180..0 L 0..180 R | "D" degrees | "ROLL" 254water temp | "C" temperature | 2 decimals | "C" celsius | "ENV_WATER_T" 255----------------------------------------------------------------------------------------------------- 256 257We're going to cheat a bit here, as traditionally, a Transform is only 258supposed to output zero or one record for every input record it 259gets. We're going to emit multiple records as separate lines in a 260single record and count on whatever gets them next (UDPWriter or 261TextFileWriter, for example) acting appropriately. 262""" 263################################################################################ 264 265 266class XDRTransform: 267 """Output a NMEA XDR string, given whatever variables we can find. 268 """ 269 270 def __init__(self, kwargs): 271 """ 272 Look for these keys in the kwargs dict: 273 ``` 274 barometer_field 275 Name of field that contains barometric pressure. 276 barometer_output_field 277 Transducer name of that should be output with barometer data. 278 Defaults to barometer_field. 279 air_temp_field 280 Name of field that contains air temperature 281 air_temp_output_field 282 Transducer name of that should be output with air temp data. 283 Defaults to air_temp_field. 284 sea_temp_field 285 Name of field that contains water temperature 286 sea_temp_output_field 287 Transducer name of that should be output with sea temp data. 288 Defaults to sea_temp_field. 289 talker_id 290 Should be format '--XDR' to identify the instrument 291 that's creating the message. 292 ``` 293 """ 294 self.barometer_field = kwargs.get('barometer_field') 295 self.barometer_output_field = kwargs.get('barometer_output_field', 296 self.barometer_field) 297 self.air_temp_field = kwargs.get('air_temp_field') 298 self.air_temp_output_field = kwargs.get('air_temp_output_field', 299 self.air_temp_field) 300 self.sea_temp_field = kwargs.get('sea_temp_field') 301 self.sea_temp_output_field = kwargs.get('sea_temp_output_field', 302 self.sea_temp_field) 303 self.xdr_talker_id = kwargs.get('xdr_talker_id') 304 305 ############################ 306 def transform(self, record): 307 """Incorporate any useable fields in this record, and if it gives us a 308 new true wind value, return the results. 309 """ 310 # Assume we have a single record; check that we've got the right 311 # record type. 312 if not record or type(record) is not dict: 313 logging.warning('Improper type for value dict: %s', type(record)) 314 return None 315 fields = record.get('fields') 316 if not fields: 317 logging.debug('XDRTransform got record with no fields: %s', record) 318 return None 319 320 # Grab any relevant values 321 results = [] 322 if self.barometer_field in fields: 323 barometer = fields.get(self.barometer_field) 324 barometer_data = '%s,P,%s,B,%s' % (self.xdr_talker_id, barometer, 325 self.barometer_output_field) 326 barometer_str = '$%s*%s' % (barometer_data, checksum(barometer_data)) 327 results.append(barometer_str) 328 329 if self.air_temp_field in fields: 330 air_temp = fields.get(self.air_temp_field) 331 air_temp_data = '%s,C,%3.2f,C,%s' % (self.xdr_talker_id, float(air_temp), 332 self.air_temp_output_field) 333 air_temp_str = '$%s*%s' % (air_temp_data, checksum(air_temp_data)) 334 results.append(air_temp_str) 335 336 if self.sea_temp_field in fields: 337 sea_temp = fields.get(self.sea_temp_field) 338 sea_temp_data = '%s,C,%3.2f,C,%s' % (self.xdr_talker_id, float(sea_temp), 339 self.sea_temp_output_field) 340 sea_temp_str = '$%s*%s' % (sea_temp_data, checksum(sea_temp_data)) 341 results.append(sea_temp_str) 342 343 return results 344 345################################################################################ 346 347class DPTTransform: 348 """Take in records and emit a NMEA DPT string, as per format: 349 $--DPT,x.x,x.x,*nn<CR><LF> \\ 350 Field Number: 351 1) Depth in meters 352 2) Offset from transducer: Positive - distance from transducer to water line, 353 or Negative - distance from transducer to keel 354 n) Checksum 355 356 e.g. $GPDPT,200.3,0.0*46 357 """ 358 359 def __init__(self, kwargs): 360 """ 361 Look for these keys in the kwargs dict: 362 ``` 363 depth_field 364 name of field that contains Depth 365 offset_field 366 Name of field that contains Offset 367 position_source_field 368 Name of field that contains position source 369 dpt_talker_id 370 Should be format '--DPT' to identify the instrument 371 that's creating the message. 372 ``` 373 """ 374 375 self.depth_field = kwargs.get('depth_field') 376 self.offset_field = kwargs.get('offset_field') 377 378 self.dpt_talker_id = kwargs.get('dpt_talker_id') 379 380 ############################ 381 def transform(self, record): 382 """Incorporate any useable fields in this record, and if it gives us a 383 new true wind value, return the results. 384 """ 385 # Check that we've got the right record type - it should be a 386 # single record. 387 if not record or type(record) is not dict: 388 logging.warning('Improper type for record: %s', type(record)) 389 return None 390 fields = record.get('fields') 391 if not fields: 392 logging.debug('MWDTransform got record with no fields: %s', record) 393 return None 394 395 depth = fields.get(self.depth_field) 396 offset = fields.get(self.offset_field) 397 398 if depth: 399 data = f'{self.dpt_talker_id},{depth},{offset}' 400 string = f'${data}*{checksum(data)}' 401 return string 402 403 return None 404 405 406################################################################################ 407 408class STNTransform: 409 """This sentence is transmitted before each individual sentence where there is a need for the 410 Listener to determine the exact source of data in the system. Examples might include 411 dual-frequency depth sounding equipment or equipment that integrates data from a 412 number of sources and produces a single output. 413 414 Take in records and emit a NMEA STN string, as per format: 415 $--STN,x.x*hh<CR><LF> 416 Field Number: 417 1) Talker ID Number/Name 418 2) Checksum 419 420 e.g. $ 421 """ 422 423 def __init__(self, kwargs): 424 """ 425 Look for these keys in the kwargs dict: 426 ``` 427 id_field 428 name of field that contains id 429 stn_talker_id 430 Should be format '--STN' to identify the instrument 431 that's creating the message. 432 ``` 433 """ 434 self.id_field = kwargs.get('id_field') 435 436 self.stn_talker_id = kwargs.get('stn_talker_id') 437 438 ############################ 439 def transform(self, record): 440 """Incorporate any useable fields in this record. 441 """ 442 # Check that we've got the right record type - it should be a 443 # single record. 444 if not record or type(record) is not dict: 445 logging.warning('Improper type for record: %s', type(record)) 446 return None 447 fields = record.get('fields') 448 if not fields: 449 logging.debug('MWDTransform got record with no fields: %s', record) 450 return None 451 452 id = fields.get(self.id_field) 453 454 if id: 455 data = f'{self.stn_talker_id},{id}' 456 string = f'${data}*{checksum(data)}' 457 return string 458 459 return None
38def checksum(source): 39 """Return hex checksum for source string.""" 40 return '%02X' % reduce(xor, (ord(c) for c in source))
Return hex checksum for source string.
44class NMEATransform(Transform): 45 """Call our various component transforms and generate NMEA strings from them. 46 """ 47 48 def __init__(self, nmea_list: list = [], **kwargs): 49 """ 50 nmea_list 51 List of the nmea transforms that will be used. 52 **kwargs 53 Arugments needed for the nmea transforms, see transforms below for what will be included. 54 """ 55 super().__init__(**kwargs) # processes 'quiet' and type hints 56 57 self.transforms = [] 58 59 # If nmea_list is not given as list, force it into one 60 if not isinstance(nmea_list, list): 61 nmea_list = [nmea_list] 62 63 if not nmea_list: 64 self.transforms = [MWDTransform(kwargs), XDRTransform(kwargs)] 65 return 66 67 class_module_name = 'logger.transforms.nmea_transform' 68 module = importlib.import_module(class_module_name) 69 70 # Get all classes within this file 71 classes = [cls_name for cls_name, cls_obj in inspect.getmembers(module) if 72 inspect.isclass(cls_obj)] 73 74 for transform in nmea_list: 75 if transform in classes: 76 class_const = getattr(module, transform) 77 self.transforms.append(class_const(kwargs)) 78 else: 79 logging.error('%s is not in classes %s', transform, classes) 80 81 82 ############################ 83 def transform(self, record): 84 """Expect a record dict (with 'timestamp' and 'fields' keys.""" 85 results = [] 86 87 # Do we have more than one record here? Normalize so that 88 # following code assumes a list of records. 89 if not type(record) is list: 90 record = [record] 91 92 for single_record in record: 93 for t in self.transforms: 94 result = t.transform(single_record) 95 logging.debug('transform %s: %s', t, result) 96 97 # Transforms may return zero, one or more results 98 if not result: 99 continue 100 elif type(result) is list: 101 results.extend(result) 102 else: 103 results.append(result) 104 105 # Just keep the results that are non-empty 106 pruned_results = [r for r in results if r] 107 108 # Return None, a single result or a list of results 109 if len(pruned_results) == 0: 110 return None 111 elif len(pruned_results) == 1: 112 return pruned_results[0] 113 else: 114 return pruned_results
Call our various component transforms and generate NMEA strings from them.
48 def __init__(self, nmea_list: list = [], **kwargs): 49 """ 50 nmea_list 51 List of the nmea transforms that will be used. 52 **kwargs 53 Arugments needed for the nmea transforms, see transforms below for what will be included. 54 """ 55 super().__init__(**kwargs) # processes 'quiet' and type hints 56 57 self.transforms = [] 58 59 # If nmea_list is not given as list, force it into one 60 if not isinstance(nmea_list, list): 61 nmea_list = [nmea_list] 62 63 if not nmea_list: 64 self.transforms = [MWDTransform(kwargs), XDRTransform(kwargs)] 65 return 66 67 class_module_name = 'logger.transforms.nmea_transform' 68 module = importlib.import_module(class_module_name) 69 70 # Get all classes within this file 71 classes = [cls_name for cls_name, cls_obj in inspect.getmembers(module) if 72 inspect.isclass(cls_obj)] 73 74 for transform in nmea_list: 75 if transform in classes: 76 class_const = getattr(module, transform) 77 self.transforms.append(class_const(kwargs)) 78 else: 79 logging.error('%s is not in classes %s', transform, classes)
nmea_list List of the nmea transforms that will be used. **kwargs Arugments needed for the nmea transforms, see transforms below for what will be included.
83 def transform(self, record): 84 """Expect a record dict (with 'timestamp' and 'fields' keys.""" 85 results = [] 86 87 # Do we have more than one record here? Normalize so that 88 # following code assumes a list of records. 89 if not type(record) is list: 90 record = [record] 91 92 for single_record in record: 93 for t in self.transforms: 94 result = t.transform(single_record) 95 logging.debug('transform %s: %s', t, result) 96 97 # Transforms may return zero, one or more results 98 if not result: 99 continue 100 elif type(result) is list: 101 results.extend(result) 102 else: 103 results.append(result) 104 105 # Just keep the results that are non-empty 106 pruned_results = [r for r in results if r] 107 108 # Return None, a single result or a list of results 109 if len(pruned_results) == 0: 110 return None 111 elif len(pruned_results) == 1: 112 return pruned_results[0] 113 else: 114 return pruned_results
Expect a record dict (with 'timestamp' and 'fields' keys.
136class MWDTransform: 137 """Output a NMEA MWD string, given true wind and (when available) 138 magnetic variation. 139 """ 140 141 def __init__(self, kwargs): 142 """ 143 Look for these keys in the kwargs dict: 144 ``` 145 true_wind_dir_field 146 Field name to look for true wind direction 147 true_wind_speed_kt_field 148 Field name to look for wind speed in knots. Either this 149 or true_wind_speed_ms_field must be non-empty. 150 true_wind_speed_ms_field 151 Field name to look for wind speed in meters per second. 152 Either this or true_wind_speed_kt_field must be non-empty. 153 magnetic_variation_field 154 Vessel magnetic variation. If omitted, only true winds 155 will be emitted. 156 mwd_talker_id 157 Should be format '--MWD' to identify the instrument 158 that's creating the message. 159 ``` 160 """ 161 self.true_wind_dir_field = kwargs.get('true_wind_dir_field') 162 self.true_wind_speed_kt_field = kwargs.get('true_wind_speed_kt_field') 163 self.true_wind_speed_ms_field = kwargs.get('true_wind_speed_ms_field') 164 self.magnetic_variation_field = kwargs.get('magnetic_variation_field') 165 self.mwd_talker_id = kwargs.get('mwd_talker_id') 166 167 self.true_wind_dir = None 168 self.true_wind_speed_kt = None 169 self.true_wind_speed_ms = None 170 self.magnetic_variation = None 171 172 ############################ 173 def transform(self, record): 174 """Incorporate any useable fields in this record. If it gives us a 175 new MWD record, return it. 176 """ 177 # Check that we've got the right record type - it should be a 178 # single record. 179 if not record or type(record) is not dict: 180 logging.warning('Improper type for record: %s', type(record)) 181 return None 182 fields = record.get('fields') 183 if not fields: 184 logging.debug('MWDTransform got record with no fields: %s', record) 185 return None 186 187 # Grab any relevant values 188 self.true_wind_dir = fields.get(self.true_wind_dir_field, 189 self.true_wind_dir) 190 if self.true_wind_speed_kt_field: 191 self.true_wind_speed_kt = fields.get(self.true_wind_speed_kt_field, 192 self.true_wind_speed_kt) 193 if self.true_wind_speed_ms_field: 194 self.true_wind_speed_ms = fields.get(self.true_wind_speed_ms_field, 195 self.true_wind_speed_ms) 196 if self.magnetic_variation_field: 197 self.magnetic_variation = fields.get(self.magnetic_variation_field, 198 self.magnetic_variation) 199 200 # Do we have enough values to emit a record? If not, go home. 201 if self.true_wind_dir is None: 202 logging.debug('Not all required values present - skipping') 203 return None 204 if self.true_wind_speed_kt is None and self.true_wind_speed_ms is None: 205 logging.debug('Not all required values present - skipping') 206 return None 207 208 # Are we filling in meters per second from knots? 209 if self.true_wind_speed_ms_field is None and \ 210 self.true_wind_speed_kt_field and \ 211 self.true_wind_speed_kt is not None: 212 self.true_wind_speed_ms = self.true_wind_speed_kt * 0.514444 213 214 # Are we filling in knots from meters per second from? 215 if self.true_wind_speed_kt_field is None and \ 216 self.true_wind_speed_ms_field and \ 217 self.true_wind_speed_ms is not None: 218 self.true_wind_speed_kt = self.true_wind_speed_kt * 1.94384 219 220 # Do we have a magnetic variation? If so, provide mag winds, 221 # otherwise use an empty string. 222 if self.magnetic_variation is not None: 223 mag_winds = '%3.1f' % (self.true_wind_dir - self.magnetic_variation) 224 else: 225 mag_winds = '' 226 227 # Assemble string, compute checksum, and return it. 228 result_str = '%s,%3.1f,T,%s,M,%3.1f,N,%3.1f,M' % \ 229 (self.mwd_talker_id, self.true_wind_dir, mag_winds, 230 self.true_wind_speed_kt, self.true_wind_speed_ms) 231 checksum = reduce(xor, (ord(c) for c in result_str)) 232 return '$%s*%02X' % (result_str, checksum)
Output a NMEA MWD string, given true wind and (when available) magnetic variation.
141 def __init__(self, kwargs): 142 """ 143 Look for these keys in the kwargs dict: 144 ``` 145 true_wind_dir_field 146 Field name to look for true wind direction 147 true_wind_speed_kt_field 148 Field name to look for wind speed in knots. Either this 149 or true_wind_speed_ms_field must be non-empty. 150 true_wind_speed_ms_field 151 Field name to look for wind speed in meters per second. 152 Either this or true_wind_speed_kt_field must be non-empty. 153 magnetic_variation_field 154 Vessel magnetic variation. If omitted, only true winds 155 will be emitted. 156 mwd_talker_id 157 Should be format '--MWD' to identify the instrument 158 that's creating the message. 159 ``` 160 """ 161 self.true_wind_dir_field = kwargs.get('true_wind_dir_field') 162 self.true_wind_speed_kt_field = kwargs.get('true_wind_speed_kt_field') 163 self.true_wind_speed_ms_field = kwargs.get('true_wind_speed_ms_field') 164 self.magnetic_variation_field = kwargs.get('magnetic_variation_field') 165 self.mwd_talker_id = kwargs.get('mwd_talker_id') 166 167 self.true_wind_dir = None 168 self.true_wind_speed_kt = None 169 self.true_wind_speed_ms = None 170 self.magnetic_variation = None
Look for these keys in the kwargs dict:
true_wind_dir_field
Field name to look for true wind direction
true_wind_speed_kt_field
Field name to look for wind speed in knots. Either this
or true_wind_speed_ms_field must be non-empty.
true_wind_speed_ms_field
Field name to look for wind speed in meters per second.
Either this or true_wind_speed_kt_field must be non-empty.
magnetic_variation_field
Vessel magnetic variation. If omitted, only true winds
will be emitted.
mwd_talker_id
Should be format '--MWD' to identify the instrument
that's creating the message.
173 def transform(self, record): 174 """Incorporate any useable fields in this record. If it gives us a 175 new MWD record, return it. 176 """ 177 # Check that we've got the right record type - it should be a 178 # single record. 179 if not record or type(record) is not dict: 180 logging.warning('Improper type for record: %s', type(record)) 181 return None 182 fields = record.get('fields') 183 if not fields: 184 logging.debug('MWDTransform got record with no fields: %s', record) 185 return None 186 187 # Grab any relevant values 188 self.true_wind_dir = fields.get(self.true_wind_dir_field, 189 self.true_wind_dir) 190 if self.true_wind_speed_kt_field: 191 self.true_wind_speed_kt = fields.get(self.true_wind_speed_kt_field, 192 self.true_wind_speed_kt) 193 if self.true_wind_speed_ms_field: 194 self.true_wind_speed_ms = fields.get(self.true_wind_speed_ms_field, 195 self.true_wind_speed_ms) 196 if self.magnetic_variation_field: 197 self.magnetic_variation = fields.get(self.magnetic_variation_field, 198 self.magnetic_variation) 199 200 # Do we have enough values to emit a record? If not, go home. 201 if self.true_wind_dir is None: 202 logging.debug('Not all required values present - skipping') 203 return None 204 if self.true_wind_speed_kt is None and self.true_wind_speed_ms is None: 205 logging.debug('Not all required values present - skipping') 206 return None 207 208 # Are we filling in meters per second from knots? 209 if self.true_wind_speed_ms_field is None and \ 210 self.true_wind_speed_kt_field and \ 211 self.true_wind_speed_kt is not None: 212 self.true_wind_speed_ms = self.true_wind_speed_kt * 0.514444 213 214 # Are we filling in knots from meters per second from? 215 if self.true_wind_speed_kt_field is None and \ 216 self.true_wind_speed_ms_field and \ 217 self.true_wind_speed_ms is not None: 218 self.true_wind_speed_kt = self.true_wind_speed_kt * 1.94384 219 220 # Do we have a magnetic variation? If so, provide mag winds, 221 # otherwise use an empty string. 222 if self.magnetic_variation is not None: 223 mag_winds = '%3.1f' % (self.true_wind_dir - self.magnetic_variation) 224 else: 225 mag_winds = '' 226 227 # Assemble string, compute checksum, and return it. 228 result_str = '%s,%3.1f,T,%s,M,%3.1f,N,%3.1f,M' % \ 229 (self.mwd_talker_id, self.true_wind_dir, mag_winds, 230 self.true_wind_speed_kt, self.true_wind_speed_ms) 231 checksum = reduce(xor, (ord(c) for c in result_str)) 232 return '$%s*%02X' % (result_str, checksum)
Incorporate any useable fields in this record. If it gives us a new MWD record, return it.
267class XDRTransform: 268 """Output a NMEA XDR string, given whatever variables we can find. 269 """ 270 271 def __init__(self, kwargs): 272 """ 273 Look for these keys in the kwargs dict: 274 ``` 275 barometer_field 276 Name of field that contains barometric pressure. 277 barometer_output_field 278 Transducer name of that should be output with barometer data. 279 Defaults to barometer_field. 280 air_temp_field 281 Name of field that contains air temperature 282 air_temp_output_field 283 Transducer name of that should be output with air temp data. 284 Defaults to air_temp_field. 285 sea_temp_field 286 Name of field that contains water temperature 287 sea_temp_output_field 288 Transducer name of that should be output with sea temp data. 289 Defaults to sea_temp_field. 290 talker_id 291 Should be format '--XDR' to identify the instrument 292 that's creating the message. 293 ``` 294 """ 295 self.barometer_field = kwargs.get('barometer_field') 296 self.barometer_output_field = kwargs.get('barometer_output_field', 297 self.barometer_field) 298 self.air_temp_field = kwargs.get('air_temp_field') 299 self.air_temp_output_field = kwargs.get('air_temp_output_field', 300 self.air_temp_field) 301 self.sea_temp_field = kwargs.get('sea_temp_field') 302 self.sea_temp_output_field = kwargs.get('sea_temp_output_field', 303 self.sea_temp_field) 304 self.xdr_talker_id = kwargs.get('xdr_talker_id') 305 306 ############################ 307 def transform(self, record): 308 """Incorporate any useable fields in this record, and if it gives us a 309 new true wind value, return the results. 310 """ 311 # Assume we have a single record; check that we've got the right 312 # record type. 313 if not record or type(record) is not dict: 314 logging.warning('Improper type for value dict: %s', type(record)) 315 return None 316 fields = record.get('fields') 317 if not fields: 318 logging.debug('XDRTransform got record with no fields: %s', record) 319 return None 320 321 # Grab any relevant values 322 results = [] 323 if self.barometer_field in fields: 324 barometer = fields.get(self.barometer_field) 325 barometer_data = '%s,P,%s,B,%s' % (self.xdr_talker_id, barometer, 326 self.barometer_output_field) 327 barometer_str = '$%s*%s' % (barometer_data, checksum(barometer_data)) 328 results.append(barometer_str) 329 330 if self.air_temp_field in fields: 331 air_temp = fields.get(self.air_temp_field) 332 air_temp_data = '%s,C,%3.2f,C,%s' % (self.xdr_talker_id, float(air_temp), 333 self.air_temp_output_field) 334 air_temp_str = '$%s*%s' % (air_temp_data, checksum(air_temp_data)) 335 results.append(air_temp_str) 336 337 if self.sea_temp_field in fields: 338 sea_temp = fields.get(self.sea_temp_field) 339 sea_temp_data = '%s,C,%3.2f,C,%s' % (self.xdr_talker_id, float(sea_temp), 340 self.sea_temp_output_field) 341 sea_temp_str = '$%s*%s' % (sea_temp_data, checksum(sea_temp_data)) 342 results.append(sea_temp_str) 343 344 return results
Output a NMEA XDR string, given whatever variables we can find.
271 def __init__(self, kwargs): 272 """ 273 Look for these keys in the kwargs dict: 274 ``` 275 barometer_field 276 Name of field that contains barometric pressure. 277 barometer_output_field 278 Transducer name of that should be output with barometer data. 279 Defaults to barometer_field. 280 air_temp_field 281 Name of field that contains air temperature 282 air_temp_output_field 283 Transducer name of that should be output with air temp data. 284 Defaults to air_temp_field. 285 sea_temp_field 286 Name of field that contains water temperature 287 sea_temp_output_field 288 Transducer name of that should be output with sea temp data. 289 Defaults to sea_temp_field. 290 talker_id 291 Should be format '--XDR' to identify the instrument 292 that's creating the message. 293 ``` 294 """ 295 self.barometer_field = kwargs.get('barometer_field') 296 self.barometer_output_field = kwargs.get('barometer_output_field', 297 self.barometer_field) 298 self.air_temp_field = kwargs.get('air_temp_field') 299 self.air_temp_output_field = kwargs.get('air_temp_output_field', 300 self.air_temp_field) 301 self.sea_temp_field = kwargs.get('sea_temp_field') 302 self.sea_temp_output_field = kwargs.get('sea_temp_output_field', 303 self.sea_temp_field) 304 self.xdr_talker_id = kwargs.get('xdr_talker_id')
Look for these keys in the kwargs dict:
barometer_field
Name of field that contains barometric pressure.
barometer_output_field
Transducer name of that should be output with barometer data.
Defaults to barometer_field.
air_temp_field
Name of field that contains air temperature
air_temp_output_field
Transducer name of that should be output with air temp data.
Defaults to air_temp_field.
sea_temp_field
Name of field that contains water temperature
sea_temp_output_field
Transducer name of that should be output with sea temp data.
Defaults to sea_temp_field.
talker_id
Should be format '--XDR' to identify the instrument
that's creating the message.
307 def transform(self, record): 308 """Incorporate any useable fields in this record, and if it gives us a 309 new true wind value, return the results. 310 """ 311 # Assume we have a single record; check that we've got the right 312 # record type. 313 if not record or type(record) is not dict: 314 logging.warning('Improper type for value dict: %s', type(record)) 315 return None 316 fields = record.get('fields') 317 if not fields: 318 logging.debug('XDRTransform got record with no fields: %s', record) 319 return None 320 321 # Grab any relevant values 322 results = [] 323 if self.barometer_field in fields: 324 barometer = fields.get(self.barometer_field) 325 barometer_data = '%s,P,%s,B,%s' % (self.xdr_talker_id, barometer, 326 self.barometer_output_field) 327 barometer_str = '$%s*%s' % (barometer_data, checksum(barometer_data)) 328 results.append(barometer_str) 329 330 if self.air_temp_field in fields: 331 air_temp = fields.get(self.air_temp_field) 332 air_temp_data = '%s,C,%3.2f,C,%s' % (self.xdr_talker_id, float(air_temp), 333 self.air_temp_output_field) 334 air_temp_str = '$%s*%s' % (air_temp_data, checksum(air_temp_data)) 335 results.append(air_temp_str) 336 337 if self.sea_temp_field in fields: 338 sea_temp = fields.get(self.sea_temp_field) 339 sea_temp_data = '%s,C,%3.2f,C,%s' % (self.xdr_talker_id, float(sea_temp), 340 self.sea_temp_output_field) 341 sea_temp_str = '$%s*%s' % (sea_temp_data, checksum(sea_temp_data)) 342 results.append(sea_temp_str) 343 344 return results
Incorporate any useable fields in this record, and if it gives us a new true wind value, return the results.
348class DPTTransform: 349 """Take in records and emit a NMEA DPT string, as per format: 350 $--DPT,x.x,x.x,*nn<CR><LF> \\ 351 Field Number: 352 1) Depth in meters 353 2) Offset from transducer: Positive - distance from transducer to water line, 354 or Negative - distance from transducer to keel 355 n) Checksum 356 357 e.g. $GPDPT,200.3,0.0*46 358 """ 359 360 def __init__(self, kwargs): 361 """ 362 Look for these keys in the kwargs dict: 363 ``` 364 depth_field 365 name of field that contains Depth 366 offset_field 367 Name of field that contains Offset 368 position_source_field 369 Name of field that contains position source 370 dpt_talker_id 371 Should be format '--DPT' to identify the instrument 372 that's creating the message. 373 ``` 374 """ 375 376 self.depth_field = kwargs.get('depth_field') 377 self.offset_field = kwargs.get('offset_field') 378 379 self.dpt_talker_id = kwargs.get('dpt_talker_id') 380 381 ############################ 382 def transform(self, record): 383 """Incorporate any useable fields in this record, and if it gives us a 384 new true wind value, return the results. 385 """ 386 # Check that we've got the right record type - it should be a 387 # single record. 388 if not record or type(record) is not dict: 389 logging.warning('Improper type for record: %s', type(record)) 390 return None 391 fields = record.get('fields') 392 if not fields: 393 logging.debug('MWDTransform got record with no fields: %s', record) 394 return None 395 396 depth = fields.get(self.depth_field) 397 offset = fields.get(self.offset_field) 398 399 if depth: 400 data = f'{self.dpt_talker_id},{depth},{offset}' 401 string = f'${data}*{checksum(data)}' 402 return string 403 404 return None
Take in records and emit a NMEA DPT string, as per format:
$--DPT,x.x,x.x,*nn
e.g. $GPDPT,200.3,0.0*46
360 def __init__(self, kwargs): 361 """ 362 Look for these keys in the kwargs dict: 363 ``` 364 depth_field 365 name of field that contains Depth 366 offset_field 367 Name of field that contains Offset 368 position_source_field 369 Name of field that contains position source 370 dpt_talker_id 371 Should be format '--DPT' to identify the instrument 372 that's creating the message. 373 ``` 374 """ 375 376 self.depth_field = kwargs.get('depth_field') 377 self.offset_field = kwargs.get('offset_field') 378 379 self.dpt_talker_id = kwargs.get('dpt_talker_id')
Look for these keys in the kwargs dict:
depth_field
name of field that contains Depth
offset_field
Name of field that contains Offset
position_source_field
Name of field that contains position source
dpt_talker_id
Should be format '--DPT' to identify the instrument
that's creating the message.
382 def transform(self, record): 383 """Incorporate any useable fields in this record, and if it gives us a 384 new true wind value, return the results. 385 """ 386 # Check that we've got the right record type - it should be a 387 # single record. 388 if not record or type(record) is not dict: 389 logging.warning('Improper type for record: %s', type(record)) 390 return None 391 fields = record.get('fields') 392 if not fields: 393 logging.debug('MWDTransform got record with no fields: %s', record) 394 return None 395 396 depth = fields.get(self.depth_field) 397 offset = fields.get(self.offset_field) 398 399 if depth: 400 data = f'{self.dpt_talker_id},{depth},{offset}' 401 string = f'${data}*{checksum(data)}' 402 return string 403 404 return None
Incorporate any useable fields in this record, and if it gives us a new true wind value, return the results.
409class STNTransform: 410 """This sentence is transmitted before each individual sentence where there is a need for the 411 Listener to determine the exact source of data in the system. Examples might include 412 dual-frequency depth sounding equipment or equipment that integrates data from a 413 number of sources and produces a single output. 414 415 Take in records and emit a NMEA STN string, as per format: 416 $--STN,x.x*hh<CR><LF> 417 Field Number: 418 1) Talker ID Number/Name 419 2) Checksum 420 421 e.g. $ 422 """ 423 424 def __init__(self, kwargs): 425 """ 426 Look for these keys in the kwargs dict: 427 ``` 428 id_field 429 name of field that contains id 430 stn_talker_id 431 Should be format '--STN' to identify the instrument 432 that's creating the message. 433 ``` 434 """ 435 self.id_field = kwargs.get('id_field') 436 437 self.stn_talker_id = kwargs.get('stn_talker_id') 438 439 ############################ 440 def transform(self, record): 441 """Incorporate any useable fields in this record. 442 """ 443 # Check that we've got the right record type - it should be a 444 # single record. 445 if not record or type(record) is not dict: 446 logging.warning('Improper type for record: %s', type(record)) 447 return None 448 fields = record.get('fields') 449 if not fields: 450 logging.debug('MWDTransform got record with no fields: %s', record) 451 return None 452 453 id = fields.get(self.id_field) 454 455 if id: 456 data = f'{self.stn_talker_id},{id}' 457 string = f'${data}*{checksum(data)}' 458 return string 459 460 return None
This sentence is transmitted before each individual sentence where there is a need for the Listener to determine the exact source of data in the system. Examples might include dual-frequency depth sounding equipment or equipment that integrates data from a number of sources and produces a single output.
Take in records and emit a NMEA STN string, as per format:
$--STN,x.x*hh
e.g. $
424 def __init__(self, kwargs): 425 """ 426 Look for these keys in the kwargs dict: 427 ``` 428 id_field 429 name of field that contains id 430 stn_talker_id 431 Should be format '--STN' to identify the instrument 432 that's creating the message. 433 ``` 434 """ 435 self.id_field = kwargs.get('id_field') 436 437 self.stn_talker_id = kwargs.get('stn_talker_id')
Look for these keys in the kwargs dict:
id_field
name of field that contains id
stn_talker_id
Should be format '--STN' to identify the instrument
that's creating the message.
440 def transform(self, record): 441 """Incorporate any useable fields in this record. 442 """ 443 # Check that we've got the right record type - it should be a 444 # single record. 445 if not record or type(record) is not dict: 446 logging.warning('Improper type for record: %s', type(record)) 447 return None 448 fields = record.get('fields') 449 if not fields: 450 logging.debug('MWDTransform got record with no fields: %s', record) 451 return None 452 453 id = fields.get(self.id_field) 454 455 if id: 456 data = f'{self.stn_talker_id},{id}' 457 string = f'${data}*{checksum(data)}' 458 return string 459 460 return None
Incorporate any useable fields in this record.