UNPKG

10.7 kBJavaScriptView Raw
1const Board = require("./board");
2const Emitter = require("./mixins/emitter");
3const Pin = require("./pin");
4const {toFixed} = require("./fn");
5const priv = new Map();
6
7const Breakouts = {
8
9 /*
10 * https://www.adafruit.com/products/746
11 */
12 ADAFRUIT_ULTIMATE_GPS: {
13 receiver: {
14 value: "FGPMMOPA6H"
15 }
16 }
17
18};
19
20// GPS Antenna Modules
21const Receivers = {
22
23 /*
24 * http://www.gtop-tech.com/en/product/LadyBird-1-PA6H/MT3339_GPS_Module_04.html
25 */
26 FGPMMOPA6H: {
27 // Later, when we add logging that code will go here
28 chip: {
29 value: "MT3339"
30 }
31 }
32
33};
34
35// GPS chips
36const Chips = {
37
38 DEFAULT: {
39 baud: {
40 value: 9600,
41 writable: true
42 },
43 configure: {
44 value(callback) {
45 process.nextTick(callback);
46 }
47 }
48 },
49
50 /*
51 * http://www.mediatek.com/en/products/connectivity/gps/mt3339/
52 */
53 MT3339: {
54 baud: {
55 value: 9600,
56 writable: true
57 },
58 configure: {
59 value(callback) {
60 process.nextTick(callback);
61 }
62 },
63 frequency: {
64 get() {
65 const state = priv.get(this);
66 return state.frequency;
67 },
68 set(frequency) {
69 const state = priv.get(this);
70
71 // Enforce maximum frequency of 10hz
72 if (frequency < 10) {
73 frequency = 10;
74 }
75
76 state.frequency = frequency;
77 this.sendCommand(`$PMTK220,${String(1000 / state.frequency)}`);
78 }
79 },
80 restart: {
81 // Reboot the receiver
82 value(coldRestart) {
83
84 if (coldRestart === true) {
85 this.sendCommand("$PMTK103");
86 } else {
87 this.sendCommand("$PMTK101");
88 setTimeout(() => {
89 this.sendCommand("");
90 }, 1000);
91 }
92
93 }
94 }
95 }
96};
97
98
99/**
100 *
101 * @constructor
102 *
103 * @param {Object} options Options:
104 * pin(s), chip, receiver,
105 * breakout, fixed, serialport, frequency
106 *
107 * Sample initialization
108 *
109 * new five.GPS({ pins: {rx: 10, tx: 11});
110 *
111 */
112
113class GPS extends Emitter {
114 constructor(options) {
115 super();
116
117 // Allow users to pass in a 2 element array for rx and tx pins
118 if (Array.isArray(options)) {
119 options = {
120 pins: {
121 rx: options[0],
122 tx: options[1],
123 onOff: options[2]
124 }
125 };
126 }
127
128 if (typeof options.pins === "undefined") {
129 options.pins = {};
130 }
131
132 Board.Component.call(
133 this, options = Board.Options(options)
134 );
135
136 // Get user values for breakout, receiver and chip
137 let breakout = options.breakout || {};
138 let receiver = options.receiver;
139 let chip = options.chip;
140
141 // If a breakout is defined check for receiver and chip
142 if (Breakouts[breakout]) {
143 if (!receiver && Breakouts[breakout].receiver) {
144 receiver = Breakouts[breakout].receiver.value;
145 }
146
147 if (!chip && Breakouts[breakout].chip) {
148 chip = Breakouts[breakout].chip.value;
149 }
150 }
151
152 // If a receiver was defined or derived but chip was not
153 if (!chip) {
154 if (receiver && Receivers[receiver].chip) {
155 chip = Receivers[receiver].chip.value;
156 } else {
157 chip = "DEFAULT";
158 }
159 }
160
161 // Allow users to pass in custom breakouts types
162 breakout = typeof breakout === "string" ?
163 Chips[breakout] : options.breakout;
164
165 // Allow users to pass in custom chip types
166 chip = typeof chip === "string" ?
167 Chips[chip] : options.chip;
168
169 // Allow users to pass in custom receiver types
170 receiver = typeof receiver === "string" ?
171 Receivers[receiver] : options.receiver;
172
173 // Chip decorates the instance
174 if (chip) {
175 Object.defineProperties(this, chip);
176 }
177
178 // Receiver decorates this instance
179 if (receiver) {
180 Object.defineProperties(this, receiver);
181 }
182
183 // Breakout decorates this instance
184 if (breakout) {
185 Object.defineProperties(this, breakout);
186 }
187
188 // If necessary set default property values
189 this.fixed = options.fixed || 6;
190 this.baud = options.baud || this.baud;
191
192 // Create a "state" entry for privately
193 // storing the state of the instance
194 let state = {
195 sat: {},
196 latitude: 0.0,
197 longitude: 0.0,
198 altitude: 0.0,
199 speed: 0.0,
200 course: 0.0,
201 frequency: 1,
202 lowPowerMode: false
203 };
204
205 priv.set(this, state);
206
207 // Getters for private state values
208 Object.defineProperties(this, {
209 latitude: {
210 get() {
211 return state.latitude;
212 }
213 },
214 longitude: {
215 get() {
216 return state.longitude;
217 }
218 },
219 altitude: {
220 get() {
221 return state.altitude;
222 }
223 },
224 sat: {
225 get() {
226 return state.sat;
227 }
228 },
229 speed: {
230 get() {
231 return state.speed;
232 }
233 },
234 course: {
235 get() {
236 return state.course;
237 }
238 },
239 time: {
240 get() {
241 return state.time;
242 }
243 }
244 });
245
246 if (this.initialize) {
247 this.initialize(options);
248 }
249 }
250
251 /*
252 * Default intialization for serial GPS
253 */
254 initialize(options) {
255
256 const state = priv.get(this);
257 state.portId = options.serialPort ||
258 options.portId ||
259 options.port ||
260 options.bus;
261
262 // firmata.js has a SERIAL_PORT_IDs.DEFAULT that is not
263 // necessary in other IO plugins so it won't always exist.
264 if (typeof state.portId === "undefined" && this.io.SERIAL_PORT_IDs) {
265 state.portId = this.io.SERIAL_PORT_IDs.DEFAULT;
266 }
267
268 // Set the pin modes
269 ["tx", "rx"].forEach(pin => {
270 if (this.pins[pin]) {
271 this.io.pinMode(this.pins[pin], this.io.MODES.SERIAL);
272 }
273 });
274
275 if (this.pins.onOff) {
276 this.io.pinMode(this.pins.onOff, this.io.MODES.OUTPUT);
277 this.onOff = new Pin(this.pins.onOff);
278 }
279
280 this.io.serialConfig({
281 portId: state.portId,
282 baud: this.baud,
283 rxPin: this.pins.rx,
284 txPin: this.pins.tx
285 });
286
287 if (this.configure) {
288 this.configure(() => {
289 this.listen();
290 if (options.frequency) {
291 this.frequency = options.frequency;
292 }
293 });
294 }
295
296 }
297
298 sendCommand(string) {
299
300 const state = priv.get(this);
301 const cc = [];
302
303 // Convert the string to a charCode array
304 for (let i = 0; i < string.length; ++i) {
305 cc[i] = string.charCodeAt(i);
306 }
307
308 // Append *, checksum and cr/lf
309 const hexsum = getNmeaChecksum(string.substring(1));
310 cc.push(42, hexsum.charCodeAt(0), hexsum.charCodeAt(1), 13, 10);
311
312 this.io.serialWrite(state.portId, cc);
313 }
314
315 listen() {
316
317 const state = priv.get(this);
318 let input = "";
319
320 // Start the read loop
321 this.io.serialRead(state.portId, data => {
322
323 input += new Buffer(data).toString("ascii");
324 const sentences = input.split("\r\n");
325
326 if (sentences.length > 1) {
327 for (let i = 0; i < sentences.length - 1; i++) {
328 this.parseNmeaSentence(sentences[i]);
329 }
330 input = sentences[sentences.length - 1];
331 }
332 });
333 }
334
335 /*
336 * NMEA Sentence Information
337 * http://aprs.gids.nl/nmea
338 */
339 parseNmeaSentence(sentence) {
340
341 const state = priv.get(this);
342 const cksum = sentence.split("*");
343
344 // Check for valid sentence
345 if (cksum[1] !== getNmeaChecksum(cksum[0].substring(1))) {
346 return;
347 }
348
349 this.emit("sentence", sentence);
350
351 const segments = cksum[0].split(",");
352 const last = {
353 latitude: state.latitude,
354 longitude: state.longitude,
355 altitude: state.altitude,
356 speed: state.speed,
357 course: state.course
358 };
359
360 switch (segments[0]) {
361 case "$GPGGA":
362 // Time, position and fix related data
363 state.time = segments[1];
364 state.latitude = degToDec(segments[2], 2, segments[3], this.fixed);
365 state.longitude = degToDec(segments[4], 3, segments[5], this.fixed);
366 state.altitude = Number(segments[9]);
367 break;
368
369 case "$GPGSA":
370 // Operating details
371 state.sat.satellites = segments.slice(3, 15);
372 state.sat.pdop = Number(segments[15]);
373 state.sat.hdop = Number(segments[16]);
374 state.sat.vdop = Number(segments[17]);
375 this.emit("operations", sentence);
376 break;
377
378 case "$GPRMC":
379 // GPS & Transit data
380 state.time = segments[1];
381 state.latitude = degToDec(segments[3], 2, segments[4], this.fixed);
382 state.longitude = degToDec(segments[5], 3, segments[6], this.fixed);
383 state.course = Number(segments[8]);
384 state.speed = toFixed(segments[7] * 0.514444, this.fixed);
385 break;
386
387 case "$GPVTG":
388 // Track Made Good and Ground Speed
389 state.course = Number(segments[1]);
390 state.speed = toFixed(segments[5] * 0.514444, this.fixed);
391 break;
392
393 case "$GPGSV":
394 // Satellites in view
395 break;
396
397 case "$PGACK":
398 // Acknowledge command
399 this.emit("acknowledge", sentence);
400 break;
401
402 default:
403 this.emit("unknown", sentence);
404 break;
405 }
406
407 this.emit("data", {
408 latitude: state.latitude,
409 longitude: state.longitude,
410 altitude: state.altitude,
411 speed: state.speed,
412 course: state.course,
413 sat: state.sat,
414 time: state.time
415 });
416
417 if (last.latitude !== state.latitude ||
418 last.longitude !== state.longitude ||
419 last.altitude !== state.altitude) {
420
421 this.emit("change", {
422 latitude: state.latitude,
423 longitude: state.longitude,
424 altitude: state.altitude
425 });
426 }
427
428 if (last.speed !== state.speed ||
429 last.course !== state.course) {
430
431 this.emit("navigation", {
432 speed: state.speed,
433 course: state.course
434 });
435 }
436
437 }
438}
439
440// Convert Lat or Lng to decimal degrees
441function degToDec(degrees, intDigitsLength, cardinal, fixed) {
442 if (degrees) {
443 let decimal = Number(degrees.substring(0, intDigitsLength)) +
444 Number(degrees.substring(intDigitsLength)) / 60;
445
446 if (cardinal === "S" || cardinal === "W") {
447 decimal *= -1;
448 }
449 return Number(decimal.toFixed(fixed));
450 } else {
451 return 0;
452 }
453}
454
455function getNmeaChecksum(string) {
456 let cksum = 0x00;
457 for (let i = 0; i < string.length; ++i) {
458 cksum ^= string.charCodeAt(i);
459 }
460 cksum = cksum.toString(16).toUpperCase();
461
462 if (cksum.length < 2) {
463 cksum = (`00${cksum}`).slice(-2);
464 }
465
466 return cksum;
467}
468
469/* istanbul ignore else */
470if (!!process.env.IS_TEST_MODE) {
471 GPS.Breakouts = Breakouts;
472 GPS.Chips = Chips;
473 GPS.Receivers = Receivers;
474 GPS.purge = () => {
475 priv.clear();
476 };
477}
478module.exports = GPS;