UNPKG

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