├── .gitignore ├── docs └── GPRS-Data-Protocol.xls ├── example └── exampleServer.js ├── lib ├── position.js ├── server.js └── tracker.js ├── package.json ├── readme.md └── test ├── mocha.opts └── server.tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* -------------------------------------------------------------------------------- /docs/GPRS-Data-Protocol.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfromaniello/node-gpstracker/c557974cc8fe2315dee16058eb7354b3cb7472e6/docs/GPRS-Data-Protocol.xls -------------------------------------------------------------------------------- /example/exampleServer.js: -------------------------------------------------------------------------------- 1 | var gpstracker = require("../lib/server"); 2 | 3 | var server = gpstracker.create().listen(8000, function(){ 4 | console.log('listening your gps trackers on port', 8000); 5 | }); 6 | 7 | server.trackers.on("connected", function(tracker){ 8 | 9 | console.log("tracker connected with imei:", tracker.imei); 10 | 11 | tracker.on("position", function(position){ 12 | console.log("tracker {" + tracker.imei + "}: lat", 13 | position.lat, "lng", position.lng); 14 | }); 15 | 16 | tracker.trackEvery(10).seconds(); 17 | }); -------------------------------------------------------------------------------- /lib/position.js: -------------------------------------------------------------------------------- 1 | /* 2 | * from some weird format in degrees to decimal :) 3 | */ 4 | function convertPoint(point){ 5 | var integerPart = ~~(Math.round(point)/100), 6 | decimalPart = (point - (integerPart * 100)) / 60; 7 | return (integerPart + decimalPart).toFixed(6); 8 | } 9 | 10 | /* 11 | * convert the sign. 12 | * West and South are negative coordinates. 13 | */ 14 | function toSign(c){ 15 | return c === "S" || c === "W" ? -1 : 1; 16 | } 17 | 18 | function Position(message){ 19 | var parts = message.split(","); 20 | this.lat = toSign(parts[8]) * convertPoint(parseFloat(parts[7])), 21 | this.lng = toSign(parts[10]) * convertPoint(parseFloat(parts[9])); 22 | this.date = new Date( 23 | parseInt("20" + parts[2].substr(0, 2), 10), 24 | parseInt(parts[2].substr(2,2),10), 25 | parseInt(parts[2].substr(4,2),10), 26 | parseInt(parts[2].substr(6,2),10), 27 | parseInt(parts[2].substr(8,2),10)); 28 | 29 | this.imei = parts[0].split(":")[1]; 30 | this.speed = parseInt(parts[11], 10) * 1.85; 31 | } 32 | 33 | module.exports = Position; -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var server = module.exports, 2 | net = require("net"), 3 | EventEmitter = require("events").EventEmitter, 4 | Tracker = require("./tracker"), 5 | Position = require("./position"); 6 | 7 | 8 | server.create = function(){ 9 | var server = net.createServer(function(client){ 10 | var data = "", 11 | tracker; 12 | client.on("data", function(chunk){ 13 | if(client.tracker && client.tracker.imei === chunk.toString()){ 14 | //it is a heartbeat message, respond inmediately. 15 | client.write(new Buffer("ON")); 16 | return; 17 | } 18 | 19 | data += chunk.toString(); 20 | if(data.indexOf(";") === -1) return; 21 | var messagesToProcess = data.split(";"); 22 | for (var i = 0; i < messagesToProcess.length -1; i++) { 23 | processData(server, client, messagesToProcess[i]); 24 | } 25 | data = data.slice(data.lastIndexOf(";")+1); 26 | 27 | 28 | if(client.tracker && client.tracker.imei === data){ 29 | //remaining is a heartbeat message, respond inmediately. 30 | client.write(new Buffer("ON")); 31 | data = ""; 32 | return; 33 | } 34 | }); 35 | }); 36 | server.trackers = new EventEmitter(); 37 | return server; 38 | }; 39 | 40 | function processData(server, client, data){ 41 | var messageParts = parseMessage(data.trim()); 42 | 43 | if(messageParts && messageParts[2] == "A"){ 44 | client.write(new Buffer("LOAD")); 45 | var tracker = new Tracker(client, extractImei(data)); 46 | server.trackers[tracker.imei] = tracker; 47 | server.trackers.emit("connected", tracker); 48 | client.tracker = tracker; 49 | return; 50 | } 51 | 52 | if(messageParts && messageParts[4] && messageParts[4] === "F"){ 53 | var imei = extractImei(data), 54 | thisTracker = server.trackers[imei]; 55 | if(!thisTracker){ 56 | server.trackers.emit("error", new Error("position receive from unknown imei")); 57 | return; 58 | } 59 | thisTracker.emit("position", new Position(data)); 60 | } 61 | 62 | if(messageParts && messageParts[1] === "help me"){ 63 | var imei = extractImei(data), 64 | thisTracker = server.trackers[imei]; 65 | 66 | if(!thisTracker){ 67 | server.trackers.emit("error", new Error("position receive from unknown imei")); 68 | return; 69 | } 70 | 71 | thisTracker.emit("help me", null); 72 | } 73 | } 74 | 75 | /* 76 | * extract the IMEI of a message 77 | */ 78 | function extractImei(message){ 79 | return (/imei\:([0-9]*)/).exec(message)[1]; 80 | } 81 | 82 | function parseMessage(message){ 83 | return message.split(","); 84 | } -------------------------------------------------------------------------------- /lib/tracker.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require("events").EventEmitter; 2 | 3 | function Tracker(client, imei){ 4 | EventEmitter.call(this); 5 | 6 | this.imei = imei; 7 | this.client = client; 8 | } 9 | 10 | Tracker.prototype = Object.create(EventEmitter.prototype); 11 | 12 | /* 13 | * usage: tracker.trackEvery(10).seconds(); 14 | * tracker.trackEvery(1).hours(); 15 | * tracker.trackEvery(10).meters(); 16 | */ 17 | Tracker.prototype.trackEvery = function(value){ 18 | var result = {}, 19 | thisTracker = this, 20 | multiTrackFormat = { 21 | "seconds": [2,"s"], 22 | "minutes": [2,"m"], 23 | "hours": [2,"h"], 24 | "meters": [4,"m"] 25 | }; 26 | Object.keys(multiTrackFormat) 27 | .forEach(function(k){ 28 | result[k] = function(){ 29 | var format = multiTrackFormat[k], 30 | interval = Array(format[0] - String(value).length + 1).join('0')+ value + format[1], 31 | message = "**,imei:" + thisTracker.imei + ",C," + interval; 32 | thisTracker.client.write(new Buffer(message)); 33 | }; 34 | }); 35 | 36 | return result; 37 | }; 38 | 39 | Tracker.prototype.getPosition = function(){ 40 | this.client.write(new Buffer("**,imei:" + this.imei + ",B")); 41 | }; 42 | 43 | 44 | module.exports = Tracker; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "José F. Romaniello (http://joseoncode.com)", 3 | "name": "gpstracker", 4 | "keywords": [ 5 | "gps", 6 | "tracker", 7 | "gis", 8 | "tk102", 9 | "tk102B", 10 | "tk103", 11 | "tk103B", 12 | "tk104", 13 | "tk106", 14 | "tk106B" 15 | ], 16 | "description": "A server for the 'GPS/GPRS Tracker' tk102, tk103, tk104 and 106", 17 | "version": "0.0.3", 18 | "dependencies": {}, 19 | "devDependencies": { 20 | "mocha": "~1.3.2", 21 | "should": "~1.1.0", 22 | "comandante": "0.0.0" 23 | }, 24 | "optionalDependencies": {}, 25 | "scripts": { 26 | "test": "mocha" 27 | }, 28 | "main": "./lib/server" 29 | } 30 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | I found this device on the internetz: 2 | 3 | ![GPS/GPRS Tracker](http://www.zhyichina.com/en/GPSTracking/TK102.jpg) 4 | 5 | suitable for an application I was writing, bought one and I did some reverse engineering with telnet to discover its protocol. The result is this small library that you can use in your projects to write your servers and listen to positioning events. 6 | 7 | After that I [found an excel sheet](https://github.com/jfromaniello/node-gpstracker/blob/master/docs/GPRS-Data-Protocol.xls?raw=true) with the protocol very bad documented. The model name of this device seems "TK102", but I also found that the protocol is the same for various devices: 8 | 9 | * TK102/TK102B 10 | * TK103/TK103B 11 | * TK104 12 | * TK106/TK106B 13 | 14 | Install 15 | ======= 16 | 17 | npm install gpstracker 18 | 19 | 20 | Usage 21 | ===== 22 | 23 | A very basic example will be this 24 | 25 | ```javascript 26 | var gpstracker = require("gpstracker"); 27 | var server = gpstracker.create().listen(8000, function(){ 28 | console.log('listening your gps trackers on port', 8000); 29 | }); 30 | 31 | server.trackers.on("connected", function(tracker){ 32 | 33 | console.log("tracker connected with imei:", tracker.imei); 34 | 35 | tracker.on("help me", function(){ 36 | console.log(tracker.imei + " pressed the help button!!".red); 37 | }); 38 | 39 | tracker.on("position", function(position){ 40 | console.log("tracker {" + tracker.imei + "}: lat", 41 | position.lat, "lng", position.lng); 42 | }); 43 | 44 | tracker.trackEvery(10).seconds(); 45 | }); 46 | ``` 47 | 48 | Licence 49 | ======= 50 | 51 | I don't even know if this is legal, if not just let me know. This code is MIT licensed. -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter min 2 | --require should -------------------------------------------------------------------------------- /test/server.tests.js: -------------------------------------------------------------------------------- 1 | var trackerServer = require("../lib/server"), 2 | Socket = require("net").Socket; 3 | 4 | describe("gps tracker server", function() { 5 | beforeEach(function(done) { 6 | this.server = trackerServer.create().listen(7000, done); 7 | }); 8 | 9 | afterEach(function(done) { 10 | this.server.on("close", function(){ 11 | done(); 12 | }); 13 | this.server.close(); 14 | }); 15 | 16 | it("should send LOAD when connecting", function(done) { 17 | new Socket().connect(7000, function(){ 18 | this.write("##,imei:787878,A;"); 19 | }).on("data", function(data){ 20 | data.toString().should.eql("LOAD"); 21 | this.end(); 22 | }).on("end", function(){ 23 | done(); 24 | }); 25 | }); 26 | 27 | it("should send LOAD when connecting with aditional crap chars", function(done) { 28 | new Socket().connect(7000, function(){ 29 | this.write("\n##,imei:787878,A;"); 30 | }).on("data", function(data){ 31 | data.toString().should.eql("LOAD"); 32 | this.end(); 33 | }).on("end", function(){ 34 | done(); 35 | }); 36 | }); 37 | 38 | it("should send LOAD when connecting with aditional crap chars 2", function(done) { 39 | new Socket().connect(7000, function(){ 40 | this.write("\n!opid##,imei:787878,A;"); 41 | }).on("data", function(data){ 42 | data.toString().should.eql("LOAD"); 43 | this.end(); 44 | }).on("end", function(){ 45 | done(); 46 | }); 47 | }); 48 | 49 | it("should emit the connected event on the trackers object", function(done) { 50 | var s = new Socket().connect(7000, function(){ 51 | this.write("##,imei:787878,A;"); 52 | }).on("end", function(){ 53 | done(); 54 | }); 55 | this.server.trackers.on("connected", function(tracker){ 56 | tracker.imei.should.eql("787878"); 57 | s.end(); 58 | }); 59 | }); 60 | 61 | [ 62 | ["seconds", 1, "01s"], 63 | ["minutes", 2, "02m"], 64 | ["hours", 20, "20h"], 65 | ["meters", 4, "0004m"] 66 | ].forEach(function(tuple){ 67 | it("should be able to do multi tracking in " + tuple[0], function(done) { 68 | var s = new Socket().connect(7000, function(){ 69 | this.write("##,imei:787878,A;"); 70 | }).on("data", function(data){ 71 | if(data.toString() === "LOAD") return; 72 | data.toString().should.include("**,imei:787878,C," + tuple[2]); 73 | s.end(); 74 | }).on("end", function(){ 75 | done(); 76 | }); 77 | 78 | this.server.trackers.on("connected", function(tracker){ 79 | tracker.trackEvery(tuple[1])[tuple[0]](); 80 | }); 81 | }); 82 | }); 83 | 84 | it("should be able to request the position once", function(done) { 85 | var s = new Socket().connect(7000, function(){ 86 | this.write("##,imei:787878,A;"); 87 | }).on("data", function(data){ 88 | if(data.toString() === "LOAD") return; 89 | data.toString().should.include("**,imei:787878,B"); 90 | s.end(); 91 | }).on("end", function(){ 92 | done(); 93 | }); 94 | 95 | this.server.trackers.on("connected", function(tracker){ 96 | tracker.getPosition(); 97 | }); 98 | }); 99 | 100 | it("should emit the position event", function(done) { 101 | var s = new Socket().connect(7000, function(){ 102 | this.write("##,imei:787878,A;"); 103 | this.write(new Buffer("imei:787878,tracker,1208080907,,F,120721.000,A,3123.1244,S,06409.8181,W,100.00,0;")); 104 | }).on("end", function(){ 105 | done(); 106 | }); 107 | 108 | this.server.trackers.on("connected", function(tracker){ 109 | tracker.on("position", function(position){ 110 | position.imei.should.eql("787878"); 111 | position.lat.should.eql(-31.385407); 112 | position.lng.should.eql(-64.163635); 113 | position.date.getTime().should.eql(new Date(2012, 8, 8, 9, 7).getTime()); 114 | position.speed.should.eql(185); 115 | s.end(); 116 | }); 117 | }); 118 | 119 | }); 120 | 121 | it("should emit the position event with aditional crap characters", function(done) { 122 | var s = new Socket().connect(7000, function(){ 123 | this.write("##,imei:787878,A;"); 124 | this.write(new Buffer("!\nrsimei:787878,tracker,1208080907,,F,120721.000,A,3123.1244,S,06409.8181,W,100.00,0;")); 125 | }).on("end", function(){ 126 | done(); 127 | }); 128 | 129 | this.server.trackers.on("connected", function(tracker){ 130 | tracker.on("position", function(position){ 131 | position.lat.should.eql(-31.385407); 132 | position.lng.should.eql(-64.163635); 133 | position.date.getTime().should.eql(new Date(2012, 8, 8, 9, 7).getTime()); 134 | position.speed.should.eql(185); 135 | s.end(); 136 | }); 137 | }); 138 | 139 | }); 140 | 141 | it("should respond to the heartbeat with ON", function(done) { 142 | var s = new Socket().connect(7000, function(){ 143 | this.write("##,imei:787878,A;"); 144 | this.write(new Buffer("787878")); 145 | }).on("data", function(chunk){ 146 | if(chunk.toString() === "ON" || chunk.toString().slice(-2) === "ON"){ 147 | s.end(); 148 | } 149 | }).on("end", function(){ 150 | done(); 151 | }); 152 | }); 153 | 154 | it("should emit 'help me' without position when receiving just the help me command", function(done) { 155 | var emited; 156 | var s = new Socket().connect(7000, function(){ 157 | this.write("##,imei:787878,A;"); 158 | this.write("imei:787878,help me,000000000,13554900601,L,;"); 159 | this.write("787878"); 160 | }).on("end", function(){ 161 | emited.should.be.true; 162 | done(); 163 | }); 164 | this.server.trackers.on("connected", function(tracker){ 165 | tracker.on("help me", function(){ 166 | emited = true; 167 | s.end(); 168 | }); 169 | }); 170 | }); 171 | 172 | it("should emit 'help me' and 'position' when receiving help me command", function(done) { 173 | var emited; 174 | var s = new Socket().connect(7000, function(){ 175 | this.write("##,imei:787878,A;"); 176 | this.write("imei:787878,help me,1208080907,,F,120721.000,A,3123.1244,S,06409.8181,W,100.00,0;"); 177 | this.write("787878"); 178 | }).on("end", function(){ 179 | emited.should.be.true; 180 | done(); 181 | }); 182 | this.server.trackers.on("connected", function(tracker){ 183 | tracker.on("position", function(position){ 184 | position.imei.should.eql("787878"); 185 | position.lat.should.eql(-31.385407); 186 | position.lng.should.eql(-64.163635); 187 | position.date.getTime().should.eql(new Date(2012, 8, 8, 9, 7).getTime()); 188 | position.speed.should.eql(185); 189 | emited = true; 190 | }).on("help me", function(){ 191 | s.end(); 192 | }); 193 | }); 194 | }); 195 | 196 | }); 197 | 198 | 199 | 200 | //TODO more commands to implement: 201 | //**,imei:359710040229297,G movement alarm 202 | //**,imei:359710040229297,H,060 speed alarm in km/hs 203 | //**,imei:359710040229297,E stop alarm 204 | //**,imei:359710040229297,O,-31.379971,-064.177948;-31.388771,-064.159718 // (square) virtual fence 205 | //**,imei:359710040229297,P cancel virtual fenge 206 | //I dont know yet the response for the alarms. --------------------------------------------------------------------------------