Last active
July 1, 2024 11:47
-
-
Save tcha-tcho/6b04511fd170dddd8ec5df822525840f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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