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
def checksum(source):
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.

class NMEATransform(logger.transforms.transform.Transform):
 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.

NMEATransform(nmea_list: list = [], **kwargs)
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.

transforms
def transform(self, record):
 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.

class MWDTransform:
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.

MWDTransform(kwargs)
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.
true_wind_dir_field
true_wind_speed_kt_field
true_wind_speed_ms_field
magnetic_variation_field
mwd_talker_id
true_wind_dir
true_wind_speed_kt
true_wind_speed_ms
magnetic_variation
def transform(self, record):
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.

class XDRTransform:
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.

XDRTransform(kwargs)
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.
barometer_field
barometer_output_field
air_temp_field
air_temp_output_field
sea_temp_field
sea_temp_output_field
xdr_talker_id
def transform(self, record):
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.

class DPTTransform:
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 \ Field Number: 1) Depth in meters 2) Offset from transducer: Positive - distance from transducer to water line, or Negative - distance from transducer to keel n) Checksum

e.g. $GPDPT,200.3,0.0*46

DPTTransform(kwargs)
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.
depth_field
offset_field
dpt_talker_id
def transform(self, record):
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.

class STNTransform:
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 Field Number: 1) Talker ID Number/Name 2) Checksum

e.g. $

STNTransform(kwargs)
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.
id_field
stn_talker_id
def transform(self, record):
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.