1 | var events = require("events");
|
2 | var util = require("util");
|
3 |
|
4 | var Board = require("./board");
|
5 | var Fn = require("./fn");
|
6 | var Pin = require("./pin");
|
7 |
|
8 | var toFixed = Fn.toFixed;
|
9 |
|
10 |
|
11 | var priv = new Map();
|
12 |
|
13 | var Breakouts = {
|
14 |
|
15 | |
16 |
|
17 |
|
18 | ADAFRUIT_ULTIMATE_GPS: {
|
19 | receiver: {
|
20 | value: "FGPMMOPA6H"
|
21 | }
|
22 | }
|
23 |
|
24 | };
|
25 |
|
26 |
|
27 | var Receivers = {
|
28 |
|
29 | |
30 |
|
31 |
|
32 | FGPMMOPA6H: {
|
33 |
|
34 | chip: {
|
35 | value: "MT3339"
|
36 | }
|
37 | }
|
38 |
|
39 | };
|
40 |
|
41 |
|
42 | var 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 |
|
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 |
|
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 |
|
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 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 | function GPS(opts) {
|
119 |
|
120 | var breakout, receiver, chip, state;
|
121 |
|
122 | if (!(this instanceof GPS)) {
|
123 | return new GPS(opts);
|
124 | }
|
125 |
|
126 |
|
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 |
|
146 | breakout = opts.breakout || {};
|
147 | receiver = opts.receiver;
|
148 | chip = opts.chip;
|
149 |
|
150 |
|
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 |
|
162 | if (!chip) {
|
163 | if (receiver && Receivers[receiver].chip) {
|
164 | chip = Receivers[receiver].chip.value;
|
165 | } else {
|
166 | chip = "DEFAULT";
|
167 | }
|
168 | }
|
169 |
|
170 |
|
171 | chip = typeof chip === "string" ?
|
172 | Chips[chip] : opts.chip;
|
173 |
|
174 |
|
175 | receiver = typeof receiver === "string" ?
|
176 | Receivers[receiver] : opts.receiver;
|
177 |
|
178 |
|
179 | Object.defineProperties(this, chip);
|
180 |
|
181 |
|
182 | if (receiver) {
|
183 | Object.defineProperties(this, receiver);
|
184 | }
|
185 |
|
186 |
|
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 |
|
195 | this.fixed = opts.fixed || 6;
|
196 | this.baud = opts.baud || this.baud;
|
197 |
|
198 |
|
199 |
|
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 |
|
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 |
|
258 | util.inherits(GPS, events.EventEmitter);
|
259 |
|
260 |
|
261 |
|
262 |
|
263 | GPS.prototype.initialize = function(opts) {
|
264 |
|
265 | var state = priv.get(this);
|
266 | state.portId = opts.serialPort || opts.portId || opts.port || opts.bus;
|
267 |
|
268 |
|
269 |
|
270 | if (typeof state.portId === "undefined" && this.io.SERIAL_PORT_IDs) {
|
271 | state.portId = this.io.SERIAL_PORT_IDs.DEFAULT;
|
272 | }
|
273 |
|
274 |
|
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 |
|
304 | GPS.prototype.sendCommand = function(string) {
|
305 |
|
306 | var state = priv.get(this);
|
307 | var cc = [];
|
308 |
|
309 |
|
310 | for (var i = 0; i < string.length; ++i) {
|
311 | cc[i] = string.charCodeAt(i);
|
312 | }
|
313 |
|
314 |
|
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 |
|
321 | GPS.prototype.listen = function() {
|
322 |
|
323 | var state = priv.get(this);
|
324 | var input = "";
|
325 |
|
326 |
|
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 |
|
343 |
|
344 |
|
345 | GPS.prototype.parseNmeaSentence = function(sentence) {
|
346 |
|
347 | var state = priv.get(this);
|
348 | var cksum = sentence.split("*");
|
349 |
|
350 |
|
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 |
|
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 |
|
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 |
|
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 |
|
395 | state.course = Number(segments[1]);
|
396 | state.speed = toFixed(segments[5] * 0.514444, this.fixed);
|
397 | break;
|
398 |
|
399 | case "$GPGSV":
|
400 |
|
401 | break;
|
402 |
|
403 | case "$PGACK":
|
404 |
|
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 |
|
446 | function 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 |
|
459 | function 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 |
|
474 | if (!!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 | }
|
482 | module.exports = GPS;
|