/**
* For working with GPS receivers
* @module j5e/gps
* @requires module:j5e/event
* @requires module:j5e/fn
*/
import { Emitter } from "j5e/event";
import { normalizeIO, getProvider, toFixed } from "j5e/fn";
/**
* Class representing a GPS receiver
* @classdesc The GPS class allows communication with GPS receivers
* @extends Emitter
* @fires GPS#sentence
* @fires GPS#operations
* @fires GPS#acknowledge
* @fires GPS#unkown
* @fires GPS#data
* @fires GPS#change
* @fires GPS#navigation
*/
class GPS extends Emitter {
#state = {
input: "",
frequency: 1,
fixed: 6,
sat: {},
latitude: 0.0,
longitude: 0.0,
altitude: 0.0,
speed: 0.0,
course: 0.0,
time: null,
lowPowerMode: false
};
/**
* Instantiate a GPS Receiver
* @param {(number[]|string[]|object)} io - The pin numbers, pin identifiers or a complete IO options object
* @param {number} [io.baud=9600] - The baud rate for serial communication
* @param {(number[]|string[])} [io.pins] - If passing an object, the pin numbers or pin identifiers for transmit and receive in a 2 element array
* @param {(number|string)} [io.transmit] - The pin number or pin identifier for transmit
* @param {(number|string)} [io.receive] - The pin number or pin identifier for receive
* @param {(number|string)} [io.port=0] - The serial port number or port identifier
*/
constructor(io) {
return (async() => {
io = normalizeIO(io);
if (Array.isArray(io.pins)) {
io.transmit = io.pins[0];
io.receive = io.pins[1];
}
super();
const Provider = await getProvider(io, "Serial");
this.io = new Provider({
baud: io.baud || 9600,
transmit: io.transmit,
receive: io.receive,
port: io.port || 0,
format: "buffer",
onReadable: (count) => {
this.processData(count);
}
});
return this;
})();
}
/**
* Configure a GPS
* @returns {GPS} The instance on which the method was called
* @param {object} [options={}] - An object containing device options
* @param {number} [options.frequency=1] - The frequency of updates in Hz
* @param {number} [options.fixed=6] - Precision for latitude and longitude readings
* @example
* import GPS from "j5e/gps";
*
* const gps = await new GPS({
* transmit: 17,
* receive: 16,
* port: 2,
* baud: 9600
* });
*
* gps.configure({
* frequency: 2
* });
*/
config(options) {
if (options.frequency) {
this.frequency = options.frequency;
this.#state.frequency = options.frequency;
}
if (options.fixed) {
this.#state.fixed = options.fixed;
}
}
/**
* The most recent measured latitude
* @type {number}
* @readonly
*/
get latitude() {
return this.#state.latitude;
}
/**
* The most recent measured longitude
* @type {number}
* @readonly
*/
get longitude() {
return this.#state.longitude;
}
/**
* The most recent measured altitude
* @type {number}
* @readonly
*/
get altitude() {
return this.#state.altitude;
}
/**
* Satellite operation details {pdop, hdop, vdop}
* @type {object}
* @readonly
*/
get sat() {
return this.#state.sat;
}
/**
* The most recent measured ground speed
* @type {number}
* @readonly
*/
get speed() {
return this.#state.speed;
}
/**
* The most recent measured course
* @type {number}
* @readonly
*/
get course() {
return this.#state.course;
}
/**
* Time of last fix
* @type {number}
* @readonly
*/
get time() {
return this.#state.time;
}
/**
* Frequency of updates in hz
* @type {number}
*/
get frequency() {
return this.#state.frequency;
}
set frequency(frequency) {
throw "Frequency setter not defined";
}
/*
* Internal method used to process incoming serial data
* @private
* @param {number} count - Length of data available for serial read
*/
processData(count) {
let x = this.io.read(count);
this.#state.input += String.fromCharCode.apply(null, new Uint8Array(x));
let sentences = this.#state.input.split("\r\n");
if (sentences.length > 1) {
for (let i = 0; i < sentences.length - 1; i++) {
this.parseNmeaSentence(sentences[i]);
}
this.#state.input = sentences[sentences.length - 1];
}
}
/*
* Send a command to the GPS receiver module
* @param {string} command - The command (minus the checksum) to send
* @returns null
*/
sendCommand = function(command) {
// Append *, checksum and cr/lf
command += getNmeaChecksum(command.substring(1));
let commandAB = str2ab(command);
this.io.write(commandAB);
};
/*
* Internal method used parsing NMEA data
* @param {string} sentence - The NMEA sentence to be parsed
* @private
* @see {@link http://aprs.gids.nl/nmea|NMEA Sentence Information}
*/
parseNmeaSentence(sentence) {
const cksum = sentence.split("*");
// Check for valid sentence
if (cksum[1] !== getNmeaChecksum(cksum[0].substring(1))) {
return;
}
this.emit("sentence", sentence);
const segments = cksum[0].split(",");
const last = {
latitude: this.#state.latitude,
longitude: this.#state.longitude,
altitude: this.#state.altitude,
speed: this.#state.speed,
course: this.#state.course
};
let now = new Date();
switch (segments[0]) {
case "$GPGGA":
// Time, position and fix related data
this.#state.time = new Date(now.getUTCFullYear(), now.getMonth(), now.getDay(), Number(segments[1].substring(0, 2)), Number(segments[1].substring(2, 4)), Number(segments[1].substring(4, 6)));
this.#state.latitude = degToDec(segments[2], 2, segments[3], this.#state.fixed);
this.#state.longitude = degToDec(segments[4], 3, segments[5], this.#state.fixed);
this.#state.altitude = Number.parseFloat(Number(segments[9]).toFixed(this.#state.fixed));
break;
case "$GPGSA":
// Operating details
this.#state.sat.satellites = segments.slice(3, 15);
this.#state.sat.pdop = Number(segments[15]);
this.#state.sat.hdop = Number(segments[16]);
this.#state.sat.vdop = Number(segments[17]);
this.emit("operations", this.#state.sat);
break;
case "$GPRMC":
// GPS & Transit data
this.#state.time = new Date(now.getUTCFullYear(), now.getMonth(), now.getDay(), Number(segments[1].substring(0, 2)), Number(segments[1].substring(2, 4)), Number(segments[1].substring(4, 6)));
this.#state.latitude = degToDec(segments[3], 2, segments[4], this.#state.fixed);
this.#state.longitude = degToDec(segments[5], 3, segments[6], this.#state.fixed);
this.#state.course = Number(segments[8]);
this.#state.speed = toFixed(segments[7] * 0.514444, this.#state.fixed);
break;
case "$GPVTG":
// Track Made Good and Ground Speed
this.#state.course = Number(segments[1]);
this.#state.speed = toFixed(segments[5] * 0.514444, this.#state.fixed);
break;
case "$GPGSV":
// Satellites in view
break;
case "$PGACK":
// Acknowledge command
this.emit("acknowledge", sentence);
break;
default:
this.emit("unknown", sentence);
break;
}
this.emit("data", {
latitude: this.#state.latitude,
longitude: this.#state.longitude,
altitude: this.#state.altitude,
speed: this.#state.speed,
course: this.#state.course,
sat: this.#state.sat,
time: this.#state.time
});
if (last.latitude !== this.#state.latitude ||
last.longitude !== this.#state.longitude ||
last.altitude !== this.#state.altitude) {
this.emit("change", {
latitude: this.#state.latitude,
longitude: this.#state.longitude,
altitude: this.#state.altitude
});
}
if (last.speed !== this.#state.speed ||
last.course !== this.#state.course) {
this.emit("navigation", {
speed: this.#state.speed,
course: this.#state.course
});
}
};
}
/*
* Internal method used to convert degrees to decimal
* @param {string} degrees - Degrees from NMEA sentence
* @param {number} intDigitsLength - Length of the degrees value part fo the string
* @param {string} cardinal - Cardinal direction [N, S, E, W]
* @param {number} fixed - Max number of digits after the decimal in the returned value
* @private
*/
function degToDec(degrees, intDigitsLength, cardinal, fixed) {
if (degrees) {
let decimal = Number(degrees.substring(0, intDigitsLength)) + Number(degrees.substring(intDigitsLength)) / 60;
if (cardinal === "S" || cardinal === "W") {
decimal *= -1;
}
return Number(decimal.toFixed(fixed));
} else {
return 0;
}
}
/*
* Internal method used to calculate NMEA chuecksum
* @param {string} sentence - Sentence from GPS receiver
* @private
*/
function getNmeaChecksum(sentence) {
let cksum = 0x00;
for (let i = 0; i < sentence.length; ++i) {
cksum ^= sentence.charCodeAt(i);
}
cksum = cksum.toString(16).toUpperCase();
if (cksum.length < 2) {
cksum = ("00" + cksum).slice(-2);
}
return cksum;
}
/*
* Internal method used to convert from string to array buffer
* @param {string} str - String to convert
* @private
*/
function str2ab(str) {
let buf = new ArrayBuffer(str.length);
let bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
export default GPS;