GPS Parte 2: Decodificando uma sentença NMEA

· 6 minutos de leitura
Map

Na primeira parte sobre GPS (Entendendo o seu funcionamento) vimos como este sistema de rastreamento por satélite funciona. Também aproveitamos para explicar o que é o protocolo NMEA, utilizado pelos sistemas de navegação global.

Uma sentença NMEA é formada por caracteres passíveis de impressão e CR (carriage return) e LF (line feed). Toda sentença inicia com $ e termina com <CR> <LF>. Existem três tipos básicos de sentenças: talker sentences, proprietary sentences e query sentences.

As talker sentences são as sentenças genéricas de comunicação do protocolo, já as proprietary sentences são sentenças proprietárias dos fabricantes e as query sentences são sentenças utilizadas para requisitar informações a partir de um receptor.

Neste artigo iremos ver como implementar um decodificar de sentenças do protocolo NMEA 0183 versão 2.3.

Para implementar o decodificador iremos utilizar Python sem nenhuma biblioteca adicional. O código desenvolvido é apenas para ilustrar como é feito este tipo de processo, se você procura algo para utilizar em seu sistema eu recomendo a biblioteca pynmea2.

Decodificando uma sentença NMEA

Para decodificar uma sentença primeiro é necessário entender o seu funcionamento. No caso da sentença GGA (Global Positioning System Fix Data) podemos observar a descrição dos campos abaixo:

$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47

Para entender melhor, vamos explicar o que é cada parte:

Parte Descrição
GP Talker (GPS)
GGA Nome da sentença
123519 Hora da Fix (12:35:19 UTC)
4807.038,N Latitude 48 deg 07.038’ N
01131.000,E Longitude 11 deg 31.000’ E
1 Qualidade da Fix
08 Número de satélites visíveis
0.9 Posição horizontal
545.4,M Altitude, em metros, acima do nível do mar
46.9,M Nível médio do mar
(vazio) Tempo em segundos desde a última atualização do DGPS
(vazio) DGPS ID
*47 Checksum

Se você deseja conhecer as demais sentenças e seus campos eu recomento ler a especificação do protocolo NMEA 0183 (em inglês).

Bem, para entender melhor, vamos primeiro ver as principais partes do código, no fim será exibido o código por completo com alguns exemplos de uso.

Classe Sentence

A classe Sentence é a base de todas as sentenças NMEA. Toda sentença deve estender esta classe, como podemos observar na classe GGLSentence.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Sentence(object):
    sentence_name = 'Unknown'
    sentence_description = 'Unknown Sentence'
    fields = ()

    def __init__(self):
        self._fields_count = len(self.fields)
        self._last = self._fields_count - 1

    def parse(self, data):
        data = str(data)
        sentence, checksum = data.split('*')
        raw_fields = sentence.split(',')

        if len(raw_fields) != self._fields_count:
            raise ParseException('Field count mismatch. Expected %d fields, but found %d.' % (self._fields_count, len(raw_fields)))

        for index, field in enumerate(self.fields):
            field_name, _, field_type = field
            try:
                value = raw_fields[index]
                value = value.strip()

                if value:
                    setattr(self, field_name, field_type(value))
                else:
                    setattr(self, field_name, None)
            except:
                raise ParseException('Can\'t parse value into field "%s": %s' % (field_name, value))

        return self

    @property
    def is_valid(self):
        return False

    def __repr__(self):
        return '%sSentence(is_valid=%s)' % (self.sentence_name, self.is_valid)

Toda sentença deve possuir um nome (sentence_name), uma descrição (sentence_description) e seus respectivos campos (fields). Além disto, as classes devem implementar um método validador (is_valid) para verificar se a sentença recebida é valida.

Podemos observar que existe um método para decodificar (parse) a sentença. Como as sentenças devem seguir o mesmo padrão, o método é genérico para todas as classes derivadas de Sentence.

Inicialmente é extraído da sentença o seu checksum caso exista. Após é verificado se a quantidade de campos recebidos na sentença corresponde a quantidade de campos registrados. Por fim, é convertido o valor do campo recebido para um tipo Python compatível.

Como sabemos para que tipo devemos converter determinado campo? É isso que você verá na classe GLLSentence.

Estendendo a classe Sentence

Na classe GLLSentence sobrescrevemos as propriedades necessárias para o funcionamento correto do parser.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class GLLSentence(Sentence, LatLonMixin):
    sentence_name = 'GLL'
    sentence_description = 'Geographic Position – Latitude/Longitude'
    fields = (
        ('latitude', 'Latitude', str),
        ('ns_indicator', 'N/S Indicator', str.upper),
        ('longitude', 'Longitude', str),
        ('ew_indicator', 'E/W Indicator', str.upper),
        ('utc_time', 'UTC Time', UTCTimeParser),
        ('status', 'Status', str.upper),
        # ('mode', 'Mode', str), NMEA V 3.00
    )

    @property
    def is_valid(self):
        return self.status == 'A'

Como podemos observar, sobrescrevemos os atributos sentence_name e sentence_description com o nome e a descrição da sentença.

O atributo fields foi sobrescrito por uma tupla de tuplas que corresponde ao seguinte: o primeiro valor da tupla é o nome do campo, deve sem um nome de atributo válido, pois ele será atribuído a classe em tempo de execução; o segundo campo é uma descrição para o campo, pode ser qualquer texto; o terceiro campo é uma função/classe que será utilizada para converter o valor. Neste caso deve-se lembrar que a função/classe deve possuir apenas um parâmetro e do tipo str. Isto porque a função/classe é invocada com o valor recebido no campo.

Se observarmos, é possível verificar que o campo latitude será convertido para str, já o campo ns_indicator será convertido para str, porém maiúscula. O campo utc_time usa a classe UTCTimeParser para converter para o tipo datetime.date.

Por fim, implementamos a validação da sentença. No caso da sentença GLL, ela é valida se o status for igual a A. Uma validação não implementada é a do checksum. O checksum serve para verificar se o conteúdo recebido foi o mesmo que o enviado. Como o intuito é apenas exemplificar o funcionamento, podemos ignorar este item.

Classe NMEAParser

Para finalizar, vamos verificar como a classe NMEAParser identifica qual a sentença e sua respectiva classe para conversão.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class NMEAParser(object):
    parsers = {
        GGASentence.sentence_name: GGASentence,
        GLLSentence.sentence_name: GLLSentence,
        RMCSentence.sentence_name: RMCSentence,
        VTGSentence.sentence_name: VTGSentence,
    }

    def _get_parser(self, name):
        parser = NMEAParser.parsers.get(name.upper())

        if parser:
            return parser()

        return None

    def parse(self, data):
        if not data:
            raise ParseException('Can\'t parse empty data.')

        if data[0] == u'$':
            data = data[1:]

        if data[0:2] == u'GP':
            data = data[2:]

        sentence = data[0:3]
        parser = self._get_parser(sentence)

        if not parser:
            raise ParseException('Can\'t find parser for sentence: %s' % sentence)

        return parser.parse(data[4:])

A classe NMEAParser possui um atributo (parsers) responsável por armazenar o nome da sentença e a classe de conversão. Para identificar qual a classe correta extraímos no método parse o nome da sentença recebida. Se não for possível identificar a sentença ou se recebermos uma sentença não suportada é gerada uma exceção.

Abaixo é possível observar o código completo da aplicação com alguns exemplos de uso.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class NMEAParser(object):
    parsers = {
        GGASentence.sentence_name: GGASentence,
        GLLSentence.sentence_name: GLLSentence,
        RMCSentence.sentence_name: RMCSentence,
        VTGSentence.sentence_name: VTGSentence,
    }

    def _get_parser(self, name):
        parser = NMEAParser.parsers.get(name.upper())

        if parser:
            return parser()

        return None

    def parse(self, data):
        if not data:
            raise ParseException('Can\'t parse empty data.')

        if data[0] == u'$':
            data = data[1:]

        if data[0:2] == u'GP':
            data = data[2:]

        sentence = data[0:3]
        parser = self._get_parser(sentence)

        if not parser:
            raise ParseException('Can\'t find parser for sentence: %s' % sentence)

        return parser.parse(data[4:])

Você pode conferir o código fonte completo aqui.

Espero que você tenha gostado deste artigo. Na parte final desta série de artigos, iremos implementar uma pequena aplicação que captura a posição do GPS e informa em uma página Web.

Até a próxima.

Este artigo é uma adaptação da monografia BUSTRACKER: Sistema de rastreamento para transporte coletivo de Alexandre Vicenzi e o texto na íntegra pode ser encontrado aqui.