1 | const Board = require("./board");
|
2 | const Emitter = require("./mixins/emitter");
|
3 | const Pin = require("./pin");
|
4 | const {toFixed} = require("./fn");
|
5 | const priv = new Map();
|
6 |
|
7 | const Breakouts = {
|
8 |
|
9 | |
10 |
|
11 |
|
12 | ADAFRUIT_ULTIMATE_GPS: {
|
13 | receiver: {
|
14 | value: "FGPMMOPA6H"
|
15 | }
|
16 | }
|
17 |
|
18 | };
|
19 |
|
20 |
|
21 | const Receivers = {
|
22 |
|
23 | |
24 |
|
25 |
|
26 | FGPMMOPA6H: {
|
27 |
|
28 | chip: {
|
29 | value: "MT3339"
|
30 | }
|
31 | }
|
32 |
|
33 | };
|
34 |
|
35 |
|
36 | const 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 |
|
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 |
|
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 |
|
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 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 | class GPS extends Emitter {
|
114 | constructor(options) {
|
115 | super();
|
116 |
|
117 |
|
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 |
|
137 | let breakout = options.breakout || {};
|
138 | let receiver = options.receiver;
|
139 | let chip = options.chip;
|
140 |
|
141 |
|
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 |
|
153 | if (!chip) {
|
154 | if (receiver && Receivers[receiver].chip) {
|
155 | chip = Receivers[receiver].chip.value;
|
156 | } else {
|
157 | chip = "DEFAULT";
|
158 | }
|
159 | }
|
160 |
|
161 |
|
162 | breakout = typeof breakout === "string" ?
|
163 | Chips[breakout] : options.breakout;
|
164 |
|
165 |
|
166 | chip = typeof chip === "string" ?
|
167 | Chips[chip] : options.chip;
|
168 |
|
169 |
|
170 | receiver = typeof receiver === "string" ?
|
171 | Receivers[receiver] : options.receiver;
|
172 |
|
173 |
|
174 | if (chip) {
|
175 | Object.defineProperties(this, chip);
|
176 | }
|
177 |
|
178 |
|
179 | if (receiver) {
|
180 | Object.defineProperties(this, receiver);
|
181 | }
|
182 |
|
183 |
|
184 | if (breakout) {
|
185 | Object.defineProperties(this, breakout);
|
186 | }
|
187 |
|
188 |
|
189 | this.fixed = options.fixed || 6;
|
190 | this.baud = options.baud || this.baud;
|
191 |
|
192 |
|
193 |
|
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 |
|
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 |
|
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 |
|
263 |
|
264 | if (typeof state.portId === "undefined" && this.io.SERIAL_PORT_IDs) {
|
265 | state.portId = this.io.SERIAL_PORT_IDs.DEFAULT;
|
266 | }
|
267 |
|
268 |
|
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 |
|
304 | for (let i = 0; i < string.length; ++i) {
|
305 | cc[i] = string.charCodeAt(i);
|
306 | }
|
307 |
|
308 |
|
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 |
|
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 |
|
337 |
|
338 |
|
339 | parseNmeaSentence(sentence) {
|
340 |
|
341 | const state = priv.get(this);
|
342 | const cksum = sentence.split("*");
|
343 |
|
344 |
|
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 |
|
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 |
|
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 |
|
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 |
|
389 | state.course = Number(segments[1]);
|
390 | state.speed = toFixed(segments[5] * 0.514444, this.fixed);
|
391 | break;
|
392 |
|
393 | case "$GPGSV":
|
394 |
|
395 | break;
|
396 |
|
397 | case "$PGACK":
|
398 |
|
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 |
|
441 | function 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 |
|
455 | function 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 |
|
470 | if (!!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 | }
|
478 | module.exports = GPS;
|