@coremarine/nmea-parser
v2.1.3
Published
Library to parse NMEA 0183 sentences
Downloads
72
Readme
NMEA parser
NMEA Parser is a library to parse NMEA 0183 sentences.
NMEA 0183, or just NMEA, is an standard ASCII text protocol typically used for GNSS (GPS) devices and naval tools.
This library parse ALL NMEA-like sentences (sentences that follow these rules):
- ASCII string
- Start with
$
- Fields separated by
,
- An
*
character splitting info and checksum - Two hexadecimal digits of checksum
- End with
\r\n
If the parser knows the NMEA sentences it gives more metadata. Known frames are:
- AAM
- GGA
- HDT
- ZDA
NMEA is a backwards compatible protocol from 4.x versions until 2.00.
How to use it
import { NMEAParser } from '@coremarine/nmea-parser'
// NMEA parser
const parser = new NMEAParser()
Parse NMEA string data
The main behaviour it's just to create a parser object and with its method parseData()
get the parsed sentences.
// NMEA ascii string with multiple sentences
const input = '$GPGGA,...\r\n$GPAAM,...\r\n'
const output: NMEASentence[] = parser.parseData(input)
The output it is an array of parsed sentences which have this type
type NMEASentence = {
// Sentence ID
id: string,
// Array with ordered fields and their metadata
payload: Array<{
name: string,
value: string | number | bigint | boolean | null,
type: 'string' | 'boolean' | 'uint8' | 'uint16' | 'uint32' | 'uint64' | 'int8' | 'int16' | 'int32' | 'int64' | 'float32' | 'float64' | 'unknown',
units?: string,
description?: string,
metadata?: any
}>,
// Metadata which can be computed fields from payload fields
metadata?: any,
// Protocol information
protocol: {
name: string,
standard: boolean,
version: string,
},
// UTC timestamp when the sentence was parsed
received: number,
// Whole ASCII string sentence
sample: string,
// Sentence checksum
checksum: {
sample: string,
value: number
}
// Sentence talker
talker?: {
value: string,
description: string
}
}
If the sentence is unknown for the parser you have:
protocol
is equal to{ name: 'unknown' }
- In
payload
each element is equal to{ name: 'unknown', value: string, type: string }
Feed the parser
Another feature is to feed or train the parser to know more sentences. You can do it with a user friendly YAML file.
Look the section Feeding the parser
Extra features: memory and internal buffer
You can enable or disable memory in the parser. Why?
- Imagine you are streaming NMEA data into the parser
- You just enters string in slots so a frame could be splitted into string slots
- With memory the parser remember the last half frame
- So you can finally parse with the next string input
Generate fake sentences
It can be asked to the parser if a sentence is supported or known with the ID of the sentence and the method getSentence()
.
If the sentence is supported, parser can generate a fake sentence which is right in terms of NMEA-like requirements but just contains garbage.
To get that is with method getFakeSentenceByID()
. If the sentence it is not supported, it will returns null
.
type Sentence = {
// ID of the sentence
id: string,
// Protocol info
protocol: {
name: string,
standard: boolean,
version?: string
},
// Ordered fields with its info
payload: Array<{
name: string,
type: 'string' | 'boolean' | 'uint8' | 'uint16' | 'uint32' | 'uint64' | 'int8' | 'int16' | 'int32' | 'int64' | 'float32' | 'float64',
units?: string,
description?: string
}>,
// Optional talker
talker?: {
value: string,
description: string
}
// Optional description
description?: string
}
// GET sentence info
const id = 'AAM'
const sentenceInfo: null | Sentence = parser.getSentence(id)
// GET fake sentence
const fakeSentence: string | null = parser.getFakeSentenceByID(id)
Feeding the parser (adding known sentences)
One of the greatest features of the parser is you can expand with more NMEA-like sentences. Standard or propietary sentences, it doesn't mind.
If you make smarter your parser, it will give you more metadata of each sentence and all the values will be in their right type.
Feeding the parser is with its method addProtocols()
. You can use three ways (order by parser priority):
- YAML file path
- Is the
ProtocolsInput.file
property
- Is the
- YAML file string content
- Is the
ProtocolsInput.content
property
- Is the
- Protocol (JS) object
- Is the
ProtocolsInput.protocols
property
- Is the
type ProtocolsInput = {
// YAML file path
file?: string,
// YAML file string content
content?: string,
// JS object similar
protcols?: Array<Protocol>
}
let input: ProtocolsInput
...
parser.addProtocols(input)
YAML file / Protocols File
The YAML file is the most friendly way to feed the parser. It has this structure:
protocols:
# Array of protocols
- protocol: <name>
version: <semantic version>
standard: <true or false>
# Array of sentences
sentences:
# Each sentence has this structure
- id: <ID of sentence 1>
description: <optional description>
# Array of fields
payload:
# Each field has this structure
- name: <field 1>
type: <type>
units: <optional units>
description: <optional note>
It will be parsed into an Protocol
object then (see next section).
Example of a yaml file
protocols:
# NMEA 3.1
- protocol: NMEA
version: '3.1'
standard: true
sentences:
- id: AAM
description: Waypoint Arrival Alarm
payload:
# 1
- name: status
type: string
description: "BOOLEAN\n
A = arrival circle entered\n
V = arrival circle not passed"
# 2
- name: status
type: string
description: "BOOLEAN\n
A = perpendicular passed at waypoint\n
V = perpendicular not passed"
# 3
- name: arrival_circle_radius
type: float32
# 4
- name: radius_units
type: string
units: nautic miles
# 5
- name: waypoint_id
type: string
- id: GGA
description: Global Positioning System Fix Data
payload:
# 1
- name: utc_position
type: string
units: ms
# 2
- name: latitude
type: string
units: deg
# 3
- name: latitude_direction
type: string
description: "N: North\n
S: South"
# 4
- name: longitude
type: string
units: deg
# 5
- name: longitude_direction
type: string
description: "E - East\n
W - West"
# 6
- name: gps_quality
type: int8
description: "0: Fix not valid\n
1: GPS fix\n
2: Differential GPS fix (DGNSS), SBAS, OmniSTAR VBS, Beacon, RTX in GVBS mode\n
3: Not applicable\n
4: RTK Fixed, xFill\n
5: RTK Float, OmniSTAR XP/HP, Location RTK, RTX\n
6: INS Dead reckoning\n
7: Manual Input Mode\n
8: Simulator Mode"
# 7
- name: satellites
type: uint8
# 8
- name: hdop
type: float64
# 9
- name: altitude
type: float64
units: m
description: "Orthometric height Mean-Sea-Level (MSL reference)"
# 10
- name: altitude_units
type: string
units: m
# 11
- name: geoid_separation
type: float64
units: m
description: "Geoidal Separation: the difference between the WGS-84 earth ellipsoid surface and mean-sea-level (geoid) surface, \"-\" = mean-sea-level surface below WGS-84 ellipsoid surface."
# 12
- name: geoid_separation_units
type: string
units: m
# 13
- name: age_of_differential_gps_data
type: float64
units: sec
description: "Time in seconds since last SC104 Type 1 or 9 update, null field when DGPS is not used300"
# 14
- name: reference_station_id
type: uint16
description: "Reference station ID, range 0000 to 4095. A null field when any reference station ID is selected and no corrections are received. See table below for a description of the field values.\n
0002 CenterPoint or ViewPoint RTX\n
0005 RangePoint RTX\n
0006 FieldPoint RTX\n
0100 VBS\n
1000 HP\n
1001 HP/XP (Orbits)\n
1002 HP/G2 (Orbits)\n
1008 XP (GPS)\n
1012 G2 (GPS)\n
1013 G2 (GPS/GLONASS)\n
1014 G2 (GLONASS)\n
1016 HP/XP (GPS)\n
1020 HP/G2 (GPS)\n
1021 HP/G2 (GPS/GLONASS)"
- id: HDT
description: Heading - True
payload:
# 1
- name: heading
type: float32
description: "Heading, degrees True"
# 2
- name: "true"
type: string
description: "T = True"
- id: ZDA
description: Time & Date - UTC, day, month, year and local time zone
payload:
# 1
- name: utc_time
type: string
description: "UTC time (hours, minutes, seconds, may have fractional subseconds)"
# 2
- name: day
type: int8
description: "Day, 01 to 31"
# 3
- name: month
type: int8
description: "Month, 01 to 12"
# 4
- name: year
type: int16
description: "Year (4 digits)"
# 5
- name: local_zone_hours
type: int8
description: "Local zone description, 00 to +- 13 hours"
# 6
- name: local_zone_minutes
type: int8
description: "Local zone minutes description, 00 to 59, apply same sign as local hours"
# Propietary GYROCOMPAS1
- protocol: GYROCOMPAS1
standard: false
sentences:
- id: HEHDT
payload:
- name: heading
type: float
units: deg
- name: symbol
type: string
- id: PHTRO
payload:
- name: pitch
type: float
units: deg
- name: pitch_direction
type: string
description: M bow up, P bow down
- name: roll
type: float
units: deg
- name: roll_direction
type: string
description: M bow up, P bow down
- id: PHINF
payload:
- name: status
type: string
Protocol (JS) Object
It is an object which has the next type.
type Protocol = {
// Protocol name
protocol: string,
// If the protocol is NMEA stantard or propietary (false by default)
standard?: boolean = false,
// Semantic version
version?: string,
// Array of sentences
sentences: Array<{
// Sentence ID
id: string,
// Each field metadata
payload: Array<{
name: string,
type: 'string' | 'boolean' | 'uint8' | 'uint16' | 'uint32' | 'int8' | 'int16' | 'int32' | 'float32' | 'float64',
units?: string,
note?: string,
}>
// Optional description
description?: string
}>
}
API
Get parsed data
// NMEA ascii string with multiple sentences const input = '$GPGGA,...\r\n$GPAAM,...\r\n' const output: NMEASentence[] = parser.parseData(input)
Feed the parser (add more sentences)
let output: NMEASentence[] // From YAML file path const YAML_FILE: string = './my_yaml_file.yaml' output = parser.addProtocols({ file: YAML_FILE }) // From YAML file content const YAML_CONTENT: string = fs.readFileSync(YAML_FILE, 'utf8') output = parser.addProtocols({ content: YAML_CONTENT }) // From Protocol object const PROTOCOL_OBJECT: Protocol[] = [{...}, {...}, ..., {...}] output = parser.addProtocols({ content: PROTOCOL_OBJECT })
Get known protocols with their sentences
type StoredSentence = { id: string, protocol: { name: string, standard: boolean, version?: string }, payload: Array<{ name: string, type: 'string' | 'boolean' | 'uint8' | 'uint16' | 'uint32' | 'int8' | 'int16' | 'int32' | 'float32' | 'float64', units?: string, description?: string }>, description?: string } type ProtocolOutput = Record<string, StoredSentence> // GET protocols const knownProtocols: ProtocolOutput = parser.getSentencesByProtocol()
Get sentence info by id
type Sentence = { // ID of the sentence id: string, // Protocol info protocol: { name: string, standard: boolean, version?: string }, // Ordered fields with its info payload: Array<{ name: string, type: 'string' | 'boolean' | 'uint8' | 'uint16' | 'uint32' | 'int8' | 'int16' | 'int32' | 'float32' | 'float64', units?: string, note?: string }>, // Optional talker talker?: null | { id: string, description: string } // Optional description description?: string } // GET sentence const id = 'AAM' const sentenceInfo: null | Sentence = parser.getSentence(id)
Get fake NMEA-like sentence by id
const id: string = 'AAM' // GET fake sentence const fakeSentence: string | null = parser.getFakeSentenceByID(id)
Get / set memory and / or buffer character length
// Get const memory = parser.memory const charactersLimit = parser.bufferLimit // Set parser.memory = !memory parser.bufferLimit = charactersLimit + 10
Notes
bufferLimit
it is used to stored an incompleted frame if memory
is enabled. So you can add the needed it data later.
bufferLimit
is set to 1024
characters which is more than enough to store an incompleted frame.
NMEA standard fix the max length of a frame to 82 characters (flags included)
It is not recommended you change its value if you don't understand well the NMEA protocol pr how it works.