ggserver
Version:
GeoGate is an opensource GPS tracking server framework
487 lines (436 loc) • 22.3 kB
JavaScript
/*
* Copyright 2014 Fulup Ar Foll.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Telnet-Adapter is a dummy adapter for debug purdev.stampe.
* it waits for a telnet connect and provide very few basic commands
* - lst [list all active devices]
* - snd devid command [arg1, ...]
* - quit
*/
'use strict';
var Jison = require ("jison").Parser;
var Debug = require ("../lib/_Debug");
var TrackerCmd= require ("../lib/_TrackerCmd");
var util = require("util");
// Adapter is an object own by a given device controller that handle data connection
function DevAdapter (controller) {
// Define or LEX/Bison grammar to handle device packet
var grammar = {
"lex": {
"rules" : [ ["\\s+" , "/* skip whitespace */"]
// Lex rules==> smallest token after big/generic ones ex: 'b,'/'b', [A,C]/[a-Z], etc ...
,['help\\b' , "return 'HELP';"]
,['quit\\b' , "return 'QUIT';"]
,['ctrl\\b' , "return 'CTRL';"]
,['back\\b' , "return 'BACK';"]
,['evt\\b' , "return 'EVTS';"]
,['start\\b' , "return 'STAR';"]
,['on\\b' , "return 'STAR';"]
,['stop\\b' , "return 'STOP';"]
,['off\\b' , "return 'STOP';"]
,['dev\\b' , "return 'DEV';"]
,['list\\b' , "return 'LIS';"]
,['login\\b' , "return 'LOG';" ]
,['logout\\b' , "return 'OUT';" ]
,['track\\b' , "return 'TRK';" ]
,['info\\b' , "return 'INF';" ]
,['track\\b' , "return 'TRK';" ]
,['snd\\b' , "return 'SEND';"]
,['all\\b' , "return 'ALL';" ]
,['db\\b' , "return 'BASE';"]
,['create\\b' , "return 'CREA';"]
,['remove\\b' , "return 'DROP';"]
,['search\\b' , "return 'SEAR';"]
,['show\\b' , "return 'SEAR';"]
,['init\\b' , "return 'INIT';"]
,['[\',\"]' , "/* ignored */" ]
,['[^ ]+\\b' , "return 'TXT';" ]
,['\\n' , "return 'EOL';" ]
,[';' , "return 'EOL';" ]
,['$' , "return 'EOL';" ]
]
}, // end Lex rules
"bnf": { // WARNING: only one space in between TOKEN ex: "STOP EOF"
'data': [["EOL" ,"this.cmd='EMPTY'; return (this);"]
,["QUIT EOL" ,"this.cmd='QUIT' ; return (this);"]
,["HELP EOL" ,"this.cmd='HELP' ; return (this);"]
,["CTRL EOL" ,"this.cmd='CONTROL'; return (this);"]
,["BACK EOL" ,"this.cmd='BACKEND'; return (this);"]
,["COMMD EOL" ,"return (this);"]
,["DEV LIS EOL" ,"this.cmd='DEVLIST' ; return (this);"]
,["DEV ALL EOL" ,"this.cmd='DEVALL' ; return (this);"]
,["DEV LOG TXT EOL" ,"this.cmd='DEVIN' ; this.devid=$3; return (this);"]
,["DEV OUT TXT EOL" ,"this.cmd='DEVOUT' ; this.devid=$3; return (this);"]
,["DEV OUT ALL EOL" ,"this.cmd='DEVOUT' ; this.devid=0; return (this);"]
,["DEV TRK TXT EOL" ,"this.cmd='DEVTRCK'; this.devid=$3; return (this);"]
,["DEV TRK ALL EOL" ,"this.cmd='DEVTRCK'; this.devid=0; return (this);"]
,["DEV INF TXT EOL" ,"this.cmd='DEVINFO'; this.devid=$3; return (this);"]
,["BASE INIT EOL" ,"this.cmd='DBINIT' ;return (this);"]
,["BASE DROP TXT EOL" ,"this.cmd='DBDROP' ; this.devid=$3;return (this);"]
,["BASE SEAR TXT EOL" ,"this.cmd='DBSEAR' ; this.devid=$3; this.args=5; return (this);"]
,["BASE SEAR TXT TXT EOL" ,"this.cmd='DBSEAR' ; this.devid=$3; this.limit=parseInt($4); return (this);"]
,["BASE CREA TXT TXT TXT TXT TXT TXT TXT ARGS EOL","this.cmd='DBCREA' ; this.devid=$3; this.mmsi=$4; this.callsign=$5; this.cargo=$6; this.length=$7; this.width=$8; this.model=$9; return (this);"]
,["EVTS STAR EOL" ,"this.cmd='EVTSTART' ; return (this);"]
,["EVTS STOP EOL" ,"this.cmd='EVTSTOP' ; return (this);"]
]
,'COMMD':[
,["SEND TXT ALL ARGS" , "this.cmd='SEND'; this.action=$2; this.devid=0;"]
,["SEND TXT TXT ARGS" , "this.cmd='SEND'; this.action=$2; this.devid=$3;"]
,["SEND TXT ALL" , "this.cmd='SEND'; this.action=$2; this.devid=0;"]
,["SEND TXT TXT" , "this.cmd='SEND'; this.action=$2; this.devid=$3;"]
,["SEND HELP TXT" , "this.cmd='SEND'; this.devid=$3; this.action='help';"]
]
,'ARGS':[
["TXT" , "this.args=[$1];"]
,["ARGS TXT" , "this.args.push ($2);"]
]
}};
this.id = controller.svc;
this.uid = "//" + controller.svcopts.adapter + "/" + controller.svc + ":" + controller.svcopts.port;;
this.control = "tcpsock";
this.info = "Telnet";
this.debug = controller.svcopts.debug; // inherit debug from controller
this.Debug (1,"uid=%s", this.uid);
this.controller= controller; // keep a link to device controller and TCP socket
this.parser = new Jison(grammar);
this.request = 0; // job request number for gateway queue
try {
this.prompt= controller.gateway.opts.name +"> ";
} catch (err) {
this.prompt= "GatewayTracker> ";
}
};
// Import debug method
DevAdapter.prototype.Debug = Debug;
// hook user event handler to receive a copy of messages
DevAdapter.prototype.HookEventHandler = function(socket, gateway) {
var count = 0;
var message;
var EventHandlerQueue = function (status, job){
message=util.format ("#-%d Queue Status=%s DevId=%s Command=%s JobReq=%d Retry=%d\n", count++, status, job.devId, job.command, job.request, job.retry);
socket.write (message);
};
// Events successful process by tracker adapter
var EventHandlerAccept = function (device, data){
message=util.format ("#-%d Action Devid:[%s] Name:[%s] Cmd:[%s] Lat:%d Lon:%d Speed=%d\n", count++, device.devid, device.name, data.cmd, data.lat, data.lon, data.sog);
socket.write (message);
};
// Events on action refused by tracker adapter
var EventHandlerError = function(status, info, adapter, client){
message=util.format ("#-%d Notice Info=%s Data=%s Adapter=%s Client=%s\n", count++, status, info, adapter, client );
socket.write (message);
};
// socket closed let's clear event
if (socket === null) {
gateway.Debug(7, "Remove Telnet gateway event listener [%s]", gateway.uid);
gateway.event.removeListener("queue" ,EventHandlerQueue);
gateway.event.removeListener("accept",EventHandlerAccept);
gateway.event.removeListener("error" ,EventHandlerError);
} else {
// Events from queued jobs
message=util.format ("> Hook On [Listening for gateway [queue|accept|notice] events\n");
socket.write (message);
// note: in order to make removal of listener dev.stampsible function should have a static name
gateway.event.on("queue" ,EventHandlerQueue);
gateway.event.on("accept",EventHandlerAccept);
gateway.event.on("notice",EventHandlerError);
}
};
// Method is called each time a new client connect
DevAdapter.prototype.ClientConnect = function (socket) {
socket.write ("> type: help for support [evt to receive events]\n");
socket.write (this.prompt);
};
// Method is called when a client quit a TcpClient adapter
DevAdapter.prototype.ClientQuit = function (socket) {
};
// Command received from TCP server
DevAdapter.prototype.SendCommand = function(socket, action, arg1) {
var gateway = this.controller.gateway;
switch (action) {
case "LOGOUT": // warning at this point socket is not valid !!!
this.HookEventHandler (null, gateway);
break;
case "HELP": // return supported commands by this adapter
var listcmd=["try: [help] command directly"];
// push a notice HELP action event to gateway
device.controller.gateway.event.emit ("notice", "HELP", listcmd, this.uid, socket.uid);
break;
default:
this.Debug (1,"Telnet ignored Command=[%s]", action);
return (-1);
}
return (0);
};
// This routine is call from DevClient each time a new line arrive on socket
DevAdapter.prototype.ParseBuffer = function(socket, buffer) {
var prompt=this.prompt; // make prompt avaliable in timer :(
var data, status;
var JobCallback = function (job) {
var msg = util.format (" --> [job:%s] command=%s devid=%s [sent]\n", job.request, job.command, job.devId);
socket.write (msg);
};
// make our life simpler
var gateway = socket.controller.gateway;
var adapter= socket.adapter;
var line = buffer.toString('utf8'); // socket buffer are not string
try {
data=this.parser.parse(line); // call jison parser
} catch (err) {
socket.write ("??? (Hoops) Unknown Command [help ???]\n");
// socket.write (err + "\n");
socket.write (prompt);
return (255); // special ignore status return code
}
// final processing of data return from parser
switch (data.cmd) {
case "EMPTY": // ignore empty lines
break;
case "LOGIN": // simulate a real login [parsed by DevClient]
device.data=data;
socket.write (prompt);
return (0);
case "HELP": // better than no documentation :)
socket.write ("> ---- help ----\n");
socket.write ("> dev list [list logged devices]\n");
socket.write ("> dev all [list every connected devices]\n");
socket.write ("> dev track xxxx [send track request to device devid=xxxx]\n");
socket.write ("> dev info xxxx [display avaliable last info from activeClient devid=xxxx]\n");
socket.write ("> dev login xxxx [simulate devid=xxxx login]\n");
socket.write ("> dev logout xxxx [close client socket & force a full reconnect]\n");
socket.write (">\n");
socket.write ("> db init [if not exist create table in database]\n");
socket.write ("> db create devid mmsi callsign cargo length/cm width/cm model name....name \n");
socket.write ("> db remove xxxx [delete devices in database devid=xxx]\n");
socket.write ("> db search xxxx [search last devices dev.stampitions in database devid=xxx]\n");
socket.write (">\n");
socket.write ("> snd cmd xxxx|all [arg1..argn] [send command=cmd to devid=xxxx]\n");
socket.write ("> snd help xxxx [check avaliable commands for devid=xxxx]\n");
socket.write (">\n");
socket.write ("> evt start [register a listener to receive event from gateway as user application does\n");
socket.write ("> evt stop [stop event listener\n");
socket.write (">\n");
socket.write ("> ctrl [list controllers]\n");
socket.write ("> back [display backend]\n");
socket.write ("> quit [close connection]\n");
break;
case "DEVLIST": // list devices from gateway active list
var count = 0;
socket.write ("> List logged active devices \n");
for (var devId in gateway.activeClients) {
var dev= gateway.activeClients[devId];
if (dev.logged) {
count ++;
var elapse= parseInt((new Date().getTime()- dev.lastshow)/1000);
var info= util.format ("> -%d- devid/mmsi= %s Name= '%s' LastShow: %ds Adapter: %s\n"
, count, devId, dev.name, elapse, dev.adapter.info);
socket.write (info);
}
}
if (count === 0) socket.write ("> - no active devices [try 'dev all']\n");
break;
case "DEVALL": // list devices from gateway active list
var count = 0;
socket.write ("> List all active devices \n");
for (var devId in gateway.activeClients) {
count ++;
dev= gateway.activeClients[devId];
var elapse= parseInt((new Date().getTime()- dev.lastshow)/1000);
var info= util.format ("> -%d- devid/mmsi= %s Name= '%s' Logged=%s LastShow: %ss Adapter: %s\n"
, count, devId, dev.name, dev.logged, elapse, dev.adapter.info);
socket.write (info);
}
if (count === 0) socket.write ("> - no active devices [retry later]\n");
break;
case "EVTSTART": // register to listen gateway application events
this.HookEventHandler (socket, gateway);
break;
case "EVTSTOP": // register to listen gateway application events
socket.write ("> stop event listen\r\n");
this.HookEventHandler (null, gateway);
break;
case "CONTROL": // list active controller for this gateway
socket.write ("> List active device controller\n");
for (var svc in gateway.controllers) {
var ctrl= gateway.controllers[svc];
socket.write ("> - uid=" + ctrl.uid + "\n");
}
break;
case "BACKEND": // list active backend for this gateway
socket.write ("> Current Backend: " + gateway.backend.uid + "\n");
break;
case 'DBSEAR':
try {
// Ask DB backend to display on telnet socket last X position for devid=yyyy
var DBcallback = function (dbresult) {
if (dbresult === null || dbresult === undefined) {
this.Debug (1,"Hoops: no DB info for %s", data.devid);
return;
}
for (var idx = 0; (idx < dbresult.length); idx ++) {
var posi= dbresult[idx];
posi.lon = posi.lon.toFixed (4);
posi.lat = posi.lat.toFixed (4);
posi.sog = posi.sog.toFixed (2);
posi.cog = posi.cog.toFixed (2);
var info=util.format ("> -%d- Lat:%s Lon:%s Sog:%s Cog:%s Alt:%s Acquired:%s\n"
, idx, posi.lat, posi.lon, posi.sog, posi.cog, posi.alt, posi.acquired_at);
socket.write (info);
}
};
var lastpos = gateway.backend.LookupDev (DBcallback, data.devid, data.limit || 10);
} catch(err) {
this.Debug (0,"Error: DBsearch devid:%s err=%s", data.devid, err);
socket.write ("> - devid: " + data.devid + "error requesting DBsearch backend\n");
}
break;
case "DEVTRCK": // track one or all active devices
var job={command: TrackerCmd.SendTo.GET_TRACK
,gateway: gateway
,devId : data.devid
,request: this.request++
};
gateway.queue.push (job, JobCallback); // push to queue
socket.write ("--> queue:" + job.request);
break;
case "DEVINFO": // print info avaliable from gateway activeClient array
try {
var dev=gateway.activeClients[data.devid];
var stamp=dev.stamp;
var elapse= parseInt((new Date().getTime()- dev.lastshow)/1000);
var posi = {
lon: stamp.lon.toFixed (4),
lat: stamp.lat.toFixed (4),
sog: stamp.sog.toFixed (2),
cog: stamp.cog.toFixed (2),
alt: stamp.alt.toFixed (2)
};
var info= util.format ("> --- devid/mmsi= %s Name= '%s' LastShow: %ss Adapter: %s\n"
, data.devid, dev.name, elapse, dev.adapter.info);
socket.write (info);
info=util.format ("> Lat:%s Lon:%s Speed:%s Alt:%s Crs:%s Time:%s\n"
, posi.lat, posi.lon, posi.sog, posi.alt, posi.cog, stamp.gpsdate);
socket.write (info);
} catch(err) {
this.Debug (1,"Error: parsing Devid: %s Msg: %s", data.devid, err);
socket.write ("> - devid: " + data.devid + "No Stamp Info [try dev track]\n");
}
break;
case "DEVOUT": // force a device to close tcp socket
var job={command: TrackerCmd.SendTo.LOGOUT
,gateway: gateway
,devId : data.devid
,request: this.request++
};
gateway.queue.push (job, JobCallback); // push to queue
socket.write ("--> queue:" + job.request);
break;
case "DBINIT": // Create table in database
status= gateway.backend.CheckTablesExits ();
socket.write ("--> dbinit: done \n");
break;
case "DBCREA": // create a device within database backend
var devname;
var error=false;
if (data.args.length > 0 ) {
devname = data.args.join(" ");
} else {
devname = data.args;
}
var cmdcreate = {
devname : devname
,callsign: data.callsign
,model : data.model
,mmsi : data.mmsi
,cargo : data.cargo
,length : data.length
,width : data.width
};
for (var slot in cmdcreate) {
if (cmdcreate [slot].length === 0) {
socket.write ("--> dbcreat: " + slot + "should be defined\n");
error=true;
}
}
if (!error) {
gateway.backend.CreateDev(data.devid, cmdcreate);
socket.write("--> create:" + data.devid + "\n");
}
break;
case "DBDROP": // create a device within database backend
gateway.backend.RemoveDev (data.devid);
socket.write ("--> drop:" + data.devid + "\n");
break;
case "SEND": // request action from device
try {
var job = {
command: TrackerCmd.SendTo[data.action.toUpperCase()],
gateway: gateway,
devId : data.devid,
args : data.args,
request: this.request++
};
gateway.queue.push (job, JobCallback); // push to queue
socket.write ("--> queue:" + job.request);
} catch (e) {
socket.write ('--> unknown command: ' + data.action.toUpperCase() + '\n');
}
break;
case "QUIT": // force closing of tcp connection
socket.end();
return (255);
break;
default:
socket.write (prompt);
break;
};
// wait 1/4s before rewriting prompt [most command will be finished]
setTimeout(function () {socket.write (prompt);}, 250);
// Telnet adapter alway return special 255 status code to DevClient
return (255);
};
// if started as a main and not as module, then process test.
if (process.argv[1] === __filename) {
// Add here any paquet you would like to test
var testParser = { Empty: ""
,"Start " : "ctrl"
,"Quit " : "quit"
,"List2 " : "dev list"
,"Login " : "dev login 123456"
,"Logout1 " : "dev logout 123456"
,"Logout2 " : "dev logout all"
,"Send2 " : "snd help 123456789"
,"Send4 " : "snd command 1234567 arg1"
,"Send5 " : "snd command 1234567 arg1 arg2"
,"DBinit " : "db init"
,"DBCreate " : "db create devid mmsi callsign length width My Friendly Name"
,"DBRemove " : "db remove 123456"
,"DBSearch1 " : "db search 123456"
,"DBSearch2 " : "db search 123456 10"
};
var dummy = []; // dummy object for test
dummy.debug = 9;
var devAdapter = new DevAdapter (dummy);
// Jison is quite picky, heavy testing is more than recommended
for (var test in testParser) {
var line= testParser[test];
console.log ("### %s = [%s]", test, line);
var data=devAdapter.parser.parse(line);
console.log ("--> cmd=%s devid=%s subaction=%s args=%j", data.cmd, data.devid, data.action, data.args);
}
console.log ("**** Telnet Parser Test Done ****");
};
module.exports = DevAdapter; // http://openmymind.net/2012/2/3/Node-Require-and-Exports/