Skip to content

Instantly share code, notes, and snippets.

@tcha-tcho
Last active May 6, 2024 13:37
Show Gist options
  • Save tcha-tcho/6b04511fd170dddd8ec5df822525840f to your computer and use it in GitHub Desktop.
Save tcha-tcho/6b04511fd170dddd8ec5df822525840f to your computer and use it in GitHub Desktop.
/* UTILS AND CONSTANTS*/
const SPEED_MULTIPLIER = 1.852;
const DISTANCE_MULTIPLIER = 111.045;
const SETTINGS = {
"default_object_online_timeout": 5
,"valid_by_avg_speed": false
,"min_moving_speed": 6
,"addon.device_tracker_app_login": false
,"apply_network_data": true
};
const MAX_SPEED = 300; // ?? 'max_speed' => env('MAX_SPEED_LIMIT', 300)
const SHORTEN_NAMES = {
"deviceId": "imei"
,"uniqueId": "imei"
,"": "ssid"
,"tid": "id"
,"in1": "in1"
,"in2": "in2"
,"in3": "in3"
,"out1": "out1"
,"blocked": "out1" // realocation
,"engine_hours": "eh"
,"engineHours": "eh"
,"out2": "out2"
,"out3": "out3"
,"odometer": "odo" // meters
,"power": "pwr" // vols –> use getPower
,"sat": "sat" // in use
,"distance": "dis" // meters
,"totalDistance": "tdis" // meters ,"type": "tp","versionFw": "vf"
,"versionHw": "vh"
,"latitude": "lat"
,"longitude": "lng"
,"protocol": "prot"
,"speed": "spd" // -> use getSpeed
,"valid": "ok"
,"time": "t"
,"deviceTime": "dt"
,"serverTime": "st"
,"fixTime": "ft"
,"alarm": "alrm"
,"alert": "alrt"
,"angle": "angle"
,"sequence": "seq"
,"outdated": "old"
,"altitude": "alt"
,"course": "cs"
,"address": "adr"
,"accuracy": "accu"
,"network": "net"
,"raw": "raw"
,"index": "idx"
,"hdop": "hdop"
,"vdop": "vdop"
,"pdop": "pdop"
,"satVisible": "satv"
,"rssi": "rssi"
,"gps": "gps"
,"roaming": "roam"
,"event": "evt"
,"status": "stat"
,"serviceOdometer": "srvodo" // meters
,"tripOdometer": "tripodo" // meters
,"hours": "hs"
,"steps": "steps"
,"heartRate": "heart"
,"input": "inpt"
,"output": "outpt"
,"image": "img"
,"video": "vid"
,"audio": "aud"
// The units for the below four KEYs currently vary.
// The preferred units of measure are specified in the comment for each.
,"battery": "batt" // volts
,"batteryLevel": "battlvl" // percentage
,"fuel": "fuel" // liters
,"fuelUsed": "fuelu" // liters
,"fuelConsumption": "fuelc" // liters/hour
,"ignition": "ign" // ign
,"flags": "flags"
,"antenna": "ant"
,"charge": "chg"
,"ip": "ip"
,"archive": "arch"
,"rpm": "rpm"
,"vin": "vin"
,"approximate": "approx"
,"throttle": "throttle"
,"motion": "mot"
,"armed": "armed"
,"geofence": "geofc"
,"acceleration": "acc"
,"deviceTemp": "devtmp" // celsius
,"coolantTemp": "cooltmp" // celsius
,"engineLoad": "engload"
,"operator": "op"
,"command": "comm"
// ,"blocked": "blkd" // out1
,"door": "door"
,"axleWeight": "axlwt"
,"gSensor": "gsnsr"
,"iccid": "iccid"
,"phone": "phone"
,"speedLimit": "spdlmt"
,"dtcs": "dtcs"
,"obdSpeed": "obdspd" // knots
,"obdOdometer": "obdodo" // meters
,"result": "res"
,"driverUniqueId": "drvuid"
// Start with 1 not 0
,"temp": "temp"
,"adc": "adc"
,"io": "io"
,"count": "cnt"
,"in": "in"
,"out": "out"
,"general": "gen"
,"sos": "sos"
,"vibration": "vib"
,"movement": "mov"
,"lowspeed": "lspd"
,"overspeed": "ospd"
,"fallDown": "fdwn"
,"lowPower": "lpwr"
,"lowBattery": "lbatt"
,"fault": "flt"
,"powerOff": "poff"
,"powerOn": "pon"
,"lock": "lck"
,"unlock": "ulck"
,"geofenceEnter": "geoin"
,"geofenceExit": "geoout"
,"gpsAntennaCut": "antcut"
,"accident": "accdnt"
,"tow": "tow"
,"idle": "idle"
,"highRpm": "hrpm"
,"hardAcceleration": "hrdacc"
,"hardBraking": "hrdbrk"
,"hardCornering": "hrdcorn"
,"laneChange": "lanec"
,"fatigueDriving": "ftgue"
,"powerCut": "pcut"
,"powerRestored": "pres"
,"jamming": "jamm"
,"temperature": "temp"
,"parking": "park"
,"bonnet": "bonn"
,"footBrake": "brake"
,"fuelLeak": "fleak"
,"tampering": "tamp"
,"removing": "rem"
,"test": "tst"
};
const FIXES = {
// "cs": null // course is the angle from device, we are using ang
// ,"ft": null
// ,"idx": null
// ,"st": null
// ,"seq": null
// ,"sat": null
};
const STATUS_PERSISTENCE = {
"spd": function(status, last, pos) {
const min_spd = 10;
if (last.spd && pos.spd) {
// atualizar a data da última parada
if (parseInt(last.spd) > min_spd && parseInt(pos.spd) <= min_spd) {
status.stp = new Date().getTime();
status.updateStatus = true;
};
};
}
};
/* AGREGATED METHODS */
function isNumber (n) {
if (n && typeof n == "string") return !isNaN(n);
return Number(n) === n;
};
function empty(d) {
return d === "undefined" || d === undefined || is_null(d);
};
function is_null(d) {
return d === null || d === "null";
};
// Converts from degrees to radians.
function toRadians(degrees) {
return degrees * Math.PI / 180;
};
// Converts from radians to degrees.
function toDegrees(radians) {
return radians * 180 / Math.PI;
};
function getAngle(oldlat, oldlng, newlat, newlng) {
oldlat = toRadians(oldlat);
oldlng = toRadians(oldlng);
newlat = toRadians(newlat);
newlng = toRadians(newlng);
y = Math.sin(newlng - oldlng) * Math.cos(newlat);
x = Math.cos(oldlat) * Math.sin(newlat) -
Math.sin(oldlat) * Math.cos(newlat) * Math.cos(newlng - oldlng);
brng = Math.atan2(y, x);
brng = toDegrees(brng);
brng = (brng + 360) % 360;
return parseInt(brng || 0);
};
// adjust this using geolib as reference
const getDistance = (lat1, lng1, lat2, lng2) => {
if (lat1 === undefined || lng1 === undefined
|| lat2 === undefined || lng2 === undefined) {
return 0;
}
// 6371*ACOS(COS(PI()*(90-D3)/180)*COS((90-D2)*PI()/180)+SEN((90-D3)*PI()/180)*SEN((90-D2)*PI()/180)*COS((E2-E3)*PI()/180))
const DISTANCE_MULTIPLIER = 6371;
return (Math.acos(
Math.cos(Math.PI*(90-lat2)/180) *
Math.cos((90-lat1)*Math.PI/180) + Math.sin((90-lat2)*Math.PI/180) *
Math.sin((90-lat1)*Math.PI/180) *
Math.cos((lng1-lng2)*Math.PI/180)
) * DISTANCE_MULTIPLIER) * 1000;
};
// TODO: Spread for multiple levels recursively
function spread_data(data) {
if (typeof data == "object" && !(data instanceof Array)) {
for (let prop in data) {
if (typeof data[prop] == "object" && !(data[prop] instanceof Array)) {
data = {...data, ...data[prop]};
delete data[prop];
}
};
};
for (let prop in data) {
if (data[prop] instanceof Array) data[prop] = JSON.stringify(data[prop]);
}
let newdata = {}
for (let key in data) { // short names to reduce space on redis.
newdata[SHORTEN_NAMES[key] || key] = data[key];
};
return newdata;
};
const adjustVal = function(ref, useQM, skipString) {
if (isNumber(ref)) return Number(ref);
if (ref === "null" || ref === null) return null;
if (ref === "false" || ref === false) return false;
if (ref === "true" || ref === true) return true;
if (typeof ref === "string" && !skipString) {
if (useQM) {
return '"'+ref+'"';
} else {
return ref.replace(/^['"](.*)['"]$/ig, "$1")
}
};
if (ref === undefined) return null;
return ref;
};
/* CACHE METHODOLOGY */
const cache = {};
cache.sendCommand = (cmd, args, callback) => {
return new Promise(async (resolve, reject) => {
if (cmd instanceof Array) {
cmd.forEach( (c, i) => cmd[i] = String(cmd[i]));
};
global.client.sendCommand(cmd, args).then(resolve).catch(reject);
})
};
cache.pipeCommands = (cmds, args, callback) => {
return new Promise(async (resolve, reject) => {
cmds.forEach(cmd => {
// insert to pipe
})
// execute pipe
resolve(/*piped results*/)
});
};
/* SPECIFIC CODE */
function normalizeData(data, last) {
data = {...{
'ang': 0,
'spd': 0,
'sdis': 0,
'ok': 1,
// 'ack': empty(data['ft']),
't': new Date().getTime()
}, ...data};
if (!data.lat || empty(data.lat)) data.lat = 0;
if (!data.lng || empty(data.lng)) data.lng = 0;
data.lat = Number(data.lat);
data.lng = Number(data.lng);
if (empty(data['imei'])) data['ok'] = 0;
if (isNumber(data['spd'])) data['spd'] = Number(data['spd']);
if (typeof data['spd'] !== 'number') data['spd'] = 0;
if (data['spd'] > 0) data['spd'] = data['spd'] * SPEED_MULTIPLIER;
// Very subtle variation can generate costs without base
["pwr", "batt", "spd"].forEach(key => {
if (isNumber(data[key]) && data[key] !== 0) {
data[key] = parseFloat(Number(data[key]).toFixed(2));
};
});
// basic calculations
if (data && last && data.lat &&
last.lat && data.lng && last.lng) {
data["ang"] = getAngle(
last.lat, last.lng, data.lat, data.lng
);
data["sdis"] = getDistance(
last.lat, last.lng, data.lat, data.lng
);
};
// persist the last angle if the current one is not present
if (data["ang"] === 0) data["ang"] = last["ang"] || 0;
// I will not include this because all times are set in GMT on server
// if ((data['dt'] - new Date().getTime()) > 60) {
// data['ack'] = true;
// };
// if (isSkipableOsmand(data)) {
// console.log('Osmand skipable');
// return false;
// }
// if ( ! data['ack']) {
// //Outdated check for 90 days
// if (new Date().getTime() - data['dt'] > 7776000) {
// console.log('Bad date - outdated: ' . data['dt']);
// data['ok'] = 0;
// }
// //Future check for 1 day
// if (data['dt'] - new Date().getTime() > 86400) {
// console.log('Bad date - future: ' . data['t']);
// data['ok'] = 0;
// }
// }
// if (getProtocolConfig(data['prot'], 'bypass_invalid')) {
// data['ok'] = 1;
// };
return data;
};
function processPosition(data, last/*, resolve, reject*/) {
if (typeof last !== "object") last = {};
data = spread_data(data || {});
// for (let key in last) { // replicate last props and keep them
// if (data[key] === undefined) data[key] = last[key];
// };
data.t = new Date().getTime();
for (let key in data) {
if (typeof FIXES[key] === "string" &&
data[key] && data[key][FIXES[key]] &&
typeof data[key][FIXES[key]] === "function") {
try {
data[key] = data[key][FIXES[key]]();
} catch(e) {
console.error("Error trying to run fix key: "+e);
};
} else if (FIXES[key] === null) {
data[key] = FIXES[key];
};
};
data.dt = data.dt || data.ft || data.t; // make sure that we always have a dt
data = normalizeData(data, last);
if (!data || data.ok < 1) {
return data; // only keep the valid ones
};
data['stdis'] = last['stdis'] || 0;
data['ok'] = 1;
if (data.spd > MAX_SPEED) {
data.spd = last ? last.spd : MAX_SPEED;
};
skipProtocols = ['upro'];
if (
SETTINGS.valid_by_avg_speed &&
skipProtocols.indexOf(data.prot) === -1 &&
data.sdis > 10
) {
let time = data.t - last.t;
if (time > 0) {
let avg_speed = data.sdis / (time / 3600);
if (avg_speed > MAX_SPEED) {
data.ok = 0;
}
} else {
data.ok = 0;
}
}
data['ok'] = !!adjustVal(data['ok']);
return data; // only keep the valid ones
};
const initialLoadings = async (data) => {
return new Promise(async (resolve, reject) => {
const now = (new Date().getTime())+"";
const dt = Number( data.dt||data.deviceTime||data.device_time||now );
data.imei = (data.imei || data.uniqueId || "")
.replace(NAME_ESCAPE, "");
let commands = []
// realtime
commands.push([ "HGET", "last_updates", data.imei ]);
// will get the legacy server name
commands.push([
"HGET"
,"legacyServerName"
,data.imei
]);
// persisted data – Ex. stopped time
commands.push([ "HGET", "persisted_status", data.imei ]);
commands.push([ "HGET", "imei's_drivers", data.imei ]);
cache.pipeCommands(commands).then(results => {
const last = result[0] ? YAML.parse(result[0]) : {};
data.legacyServerName = results[1];
let status = results[2];
status = status ? YAML.parse(status) : {};
for (let prop in data) {
if (STATUS_PERSISTENCE[prop]) {
STATUS_PERSISTENCE[prop](status, last, data);
};
};
data = Object.assign(data, status);
const found_drvuid = result[3]
resolve(data, last, found_drvuid, status);
})
});
};
const finalRecords = async (data, last, found_drvuid, status) => {
return new Promise(async (resolve, reject) => {
let commands = []
if (data.updateStatus) {
delete status.updateStatus;
commands.push(
[ "HSET", "persisted_status", data.imei, YAML.stringify(status) ]
);
};
// realtime
let yamldata = {
t: data.t
,dt: data.dt
,ft: data.ft
,spd: data.spd
,lat: data.lat
,lng: data.lng
,ang: data.ang
,stp: data.stp
,ign: data.ign
,imei: data.imei
};
const curr_dt = (yamldata.dt || yamldata.ft || 0);
const found_dt = (last.dt || last.ft || 0);
const offline_pos = curr_dt < found_dt;
const maxdate = (new Date().getTime() + 86400000 /*24h ahead*/);
const in_the_future = data.ft ?
Number(data.ft) > maxdate :
data.dt && Number(data.dt) > maxdate;
if (in_the_future || offline_pos) {
// we will not record in the future in realtime anymore
} else {
if (!data.ok || data.lat == 0) {
yamldata.lat = last.lat;
yamldata.lng = last.lng;
};
yamldata = YAML.stringify(yamldata);
commands.push( [ "HSET", "last_updates", data.imei, yamldata ] );
commands.push( [ 'SET', "exp_"+data.imei, yamldata, "EX", "10"] );
}
const drvuid = (data.driverUniqueId || data.drvuid || "").toUpperCase();
if (drvuid && drvuid.length > 5) {
// cache([ "HSET", data.imei+"_drivers", drvuid, now ]);
// cache([ "HSET", drvuid+"_imeis", data.imei, now ]);
const _date = new Date();
if (found_drvuid !== drvuid) {
commands.push([ "HSET", "imei's_drivers", data.imei, drvuid ]);
data.drvchange = true;
// const collection = global.admin.firestore().collection("driverLogs");
// collection.doc(imei+"_"+_date.getTime()).set({
// at: _date
// ,imei: imei
// ,driver: drvuid
// });
};
};
// last communications
commands.push([
'ZADD'
,"last_connection"
,(new Date().getTime())
,data.imei+"_"+(data.prot||"unknown_prot")
]);
// record a cache with imeis on servers
if (data.serverNode) {
commands.push([
"HSET"
,"serverNode"
,data.imei
,data.serverNode +","+ (data.protocol||data.prot) +","+ (new Date().getTime())
]);
};
const score = parseInt( data.dt||data.ft||data.t||new Date().getTime() );
commands.push(
[ 'ZADD', data.imei+"_pos", score, YAML.stringify(data) ]
);
cache.pipeCommands(commands).then(resolve);
});
};
module.exports = (data) => {
initialLoading(data)
.then((data, last, found_drvuid, status) => {
processPosition(data, last/*, resolve, reject*/).then((data, last) => {
finalRecords(data, last, found_drvuid, status);
});
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment