├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json ├── scripts ├── startgps.py └── stopgps.py ├── signal-logr-rtl-sdr.png ├── signal-logr.png └── www ├── dashboard.html ├── dashboard.js ├── gmap.html ├── img ├── focus.png ├── gpio.png ├── gps.png ├── restart.png ├── rtlsdr.jpeg └── wifi.png └── jquery-ui-timepicker-addon.js /.env.example: -------------------------------------------------------------------------------- 1 | apiKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | #Project data files 40 | data/wifi/*.json 41 | data/gps/*.json 42 | www/dump1090/*.json 43 | .env 44 | # fuck you npm 45 | package-lock.json 46 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 tobozo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Air logger for Raspberry Pi** 2 | =========== 3 | 4 | This is an attempt to build a GPS + Wifi logger for war walking, with the addition of ADSB tracking. 5 | This build requires at least a Pi2 + Serial GPS + Wifi USB Dongle + RTL-SDR Dongle. 6 | The collected data can be rendered later or observed in realtime through the HTML5 GUI. 7 | 8 |

9 | 10 |

11 |

12 | 13 |

14 | 15 | **OS/System Requirements:** 16 | ---- 17 | 18 | - Raspbian Stretch (works with Jessie too) 19 | - NodeJS 20 | - ForeverJS (https://github.com/foreverjs/forever) 21 | - gpsd (http://catb.org/gpsd/) 22 | - A GPS device connected to the Pi 23 | - A Wifi device connected to the Pi 24 | - A valid Google Maps Api key (https://developers.google.com/maps/documentation/javascript/get-api-key) 25 | - A GSM/GPRS device (such as [this one](https://www.waveshare.com/wiki/GSM/GPRS/GNSS_HAT)) 26 | - A RTL-SDR dongle (see [this page](https://www.rtl-sdr.com/buy-rtl-sdr-dvb-t-dongles/)) 27 | 28 | **Installation:** 29 | ---- 30 | 31 | **Prerequisites** GPS / Wifi / RTL-SDR devices are plugged in 32 | 33 | Choose your veersion and [install NodeJS](https://github.com/nodesource/distributions#debinstall) 34 | 35 | Install RTL-SDR software 36 | 37 | cd ~ 38 | git clone https://github.com/MalcolmRobb/dump1090 39 | cd dump1090 40 | make 41 | 42 | If using GSM/GPRS/GNSS hat instead of external GPS module, start by [installing GSM/GPRS/GNSS software from WaveShare](https://www.waveshare.com/wiki/GSM/GPRS/GNSS_HAT) first. 43 | Any other GPS will do as long as it is supported by gpsd and detected by the Pi. 44 | 45 | Install gpsd 46 | 47 | sudo apt-get install gpsd 48 | 49 | Edit your `/etc/defaults/gpsd` 50 | 51 | sudo nano /etc/defaults/gpsd 52 | 53 | Verify the tty of your GPS device, change if necessary 54 | 55 | DEVICES="/dev/ttyUSB0" 56 | 57 | Other values to check 58 | 59 | START_DAEMON="true" 60 | USBAUTO="false" 61 | 62 | Then restart gpsd 63 | 64 | sudo service gpsd restart 65 | 66 | You need *forever.js* installed globally to run this project headless 67 | 68 | sudo npm install forever -g 69 | 70 | Clone the repository and install 71 | 72 | cd ~ 73 | git clone https://github.com/tobozo/signal-logr.git 74 | cd signal-logr 75 | npm install 76 | 77 | Put your Google Maps Api key in the `.env` file 78 | 79 | cp .env.example .env 80 | nano .env 81 | 82 | Test the app 83 | 84 | node index.js 85 | 86 | You can add to `/etc/rc.local` and reboot 87 | 88 | cd /home/pi/signal-logr/ && /opt/nodejs/bin/forever start index.js 89 | 90 | 91 | **** 92 | **Useful links** 93 | ---- 94 | * http://www.catb.org/gpsd/troubleshooting.html 95 | * http://www.xarg.org/2016/06/neo6mv2-gps-module-with-raspberry-pi/ 96 | * https://bigdanzblog.wordpress.com/2015/01/18/connecting-u-blox-neo-6m-gps-to-raspberry-pi/ 97 | * http://www.bashpi.org/?page_id=459 98 | 99 | **** 100 | **Roadmap** 101 | ---- 102 | * Enable GSM/GPRS data collection using sim900 / sim800 103 | * Enable rogue mode + sms/ppp notifications 104 | * Compliance with more advanced wifi tools and platforms (e.g. Kali) 105 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | RPi-GPS-Wifi logger 4 | (c+) tobozo 2016-11-13 5 | 6 | */ 7 | 8 | require('dotenv').config(); 9 | 10 | // load app stack 11 | 12 | const express = require('express') 13 | , app = express() 14 | , http = require('http').Server(app) 15 | , request = require('request') 16 | , io = require('socket.io')(http) 17 | , gpsd = require("node-gpsd") 18 | , GPS = require('gps') 19 | , exec = require('child_process').exec 20 | , spawn = require('child_process').spawn 21 | , execSync = require("child_process").execSync 22 | , jsonfile = require('jsonfile') 23 | , fs = require('fs') 24 | , Wireless = require('wireless') 25 | , os = require('os') 26 | , path = require('path') 27 | ; 28 | 29 | // throw some vars 30 | let connected = false 31 | , killing_app = false 32 | , oldalias = 0 33 | , secondsSinceLastPoll = 0 34 | , fixPoll = [] 35 | , lastFix 36 | , currentFix 37 | , secondsSinceLastFix = 0 38 | , dataDir = __dirname + '/data' 39 | , geoDataDir = dataDir + '/gps/' 40 | , wifiDataDir = dataDir + '/wifi/' 41 | , htmlDir = __dirname + '/www' 42 | , rtlsdrDataDir = htmlDir + '/dump1090' 43 | , dump1090Dir = '/home/pi/dump1090/' 44 | , dump1090HTMLDir = dump1090Dir + 'public_html/' 45 | , dump1090ConfigFile = dump1090HTMLDir + 'config.js' 46 | , dump1090timer 47 | , pollFiles = [] 48 | , wifiFiles = [] 49 | , wifiMaxHistoryItems = 100 50 | , googleMapsApiKey = process.env.apiKey 51 | , wifiCache = { } 52 | , gpstime = new Date() 53 | , gpsdaemonisrunning = true 54 | , gpshatisrunning = false 55 | , wirelessisrunning = false 56 | , rtlsdrisrunning = false 57 | ; 58 | 59 | 60 | if(googleMapsApiKey===undefined) console.log("[WARN] Missing apiKey in .env file, GUI may suffer"); 61 | 62 | const wireless = new Wireless({ 63 | iface: 'wlan0', 64 | updateFrequency: 30, // Optional, seconds to scan for networks 65 | connectionSpyFrequency: 2, // Optional, seconds to scan if connected 66 | vanishThreshold: 5 // Optional, how many scans before network considered gone 67 | }); 68 | 69 | 70 | const gpsWarn = function(msg) { 71 | console.log('[WARN] GPSD - ', msg); 72 | } 73 | const gpsErr = function(msg) { 74 | console.log('[ERROR] GPSD - ', msg); 75 | gpsdaemonisrunning = false; 76 | } 77 | 78 | 79 | const gps = new GPS; 80 | 81 | 82 | const listener = new gpsd.Listener({ 83 | port: 2947, 84 | hostname: 'localhost', 85 | emitraw: true, 86 | parsejson: false, 87 | logger: { 88 | info: function() {}, 89 | warn: gpsWarn, 90 | error: gpsErr 91 | }, 92 | parse: false 93 | }); 94 | 95 | 96 | const mkdirSync = function (path) { 97 | try { 98 | fs.mkdirSync(path); 99 | } catch(e) { 100 | if ( e.code != 'EEXIST' ) throw e; 101 | } 102 | } 103 | 104 | 105 | const checkInterfaces = function() { 106 | if(wirelessisrunning===false) return; 107 | const ifaces = os.networkInterfaces(); 108 | let alias = 0; 109 | Object.keys(ifaces).forEach(function (ifname) { 110 | alias = 0; 111 | 112 | ifaces[ifname].forEach(function (iface) { 113 | 114 | if ('IPv4' !== iface.family || iface.internal !== false) { 115 | // skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses 116 | return; 117 | } 118 | 119 | if (alias >= 1) { 120 | // this single interface has multiple ipv4 addresses 121 | //console.log(ifname + ':' + alias, iface.address); 122 | } else { 123 | // this interface has only one ipv4 adress 124 | //console.log(ifname, iface.address); 125 | } 126 | ++alias; 127 | }); 128 | }); 129 | 130 | if(alias==0) { 131 | console.log('no eth0: headless mode'); 132 | } else { 133 | if(oldalias!==0 && oldalias!==alias) { 134 | console.log('network changed, restarting nodejs app'); 135 | process.exit(0); 136 | } 137 | } 138 | oldalias = alias; 139 | }; 140 | 141 | 142 | const setPoll = function() { 143 | secondsSinceLastFix++; 144 | secondsSinceLastPoll++; 145 | if(fixPoll.length === 0) { 146 | return; 147 | } 148 | if(fixPoll.length>=100 || secondsSinceLastPoll>=60) { 149 | savePoll(); 150 | secondsSinceLastPoll = 0; 151 | } 152 | }; 153 | 154 | 155 | const setFix = function() { 156 | if(gpsdaemonisrunning===false) return; 157 | if(lastFix===undefined) { 158 | // not started yet 159 | return; 160 | } 161 | if(currentFix===undefined) { 162 | // setting initial currentFix 163 | currentFix = lastFix; 164 | fixPoll.push(lastFix); 165 | return; 166 | } 167 | if(currentFix.time === lastFix.time) return; // don't create duplicates 168 | fixPoll.push(lastFix); 169 | currentFix = lastFix; 170 | } 171 | 172 | 173 | const savePoll = function() { 174 | if(fixPoll.length===0) { 175 | // can't save empty poll! 176 | return; 177 | } 178 | 179 | const fileName = geoDataDir + fixPoll[0].time.replace(/[^a-z0-9-]+/gi, '_') + '.json'; 180 | let wifilist = {}; 181 | try { 182 | wifilist = wireless.list(); 183 | } catch(e) { 184 | console.log('cannot save wifilist', e); 185 | } 186 | 187 | jsonfile.writeFile(fileName, fixPoll, {spaces: 2}, function(err) { 188 | if(err) console.error(err); 189 | resetPoll(); 190 | refreshPollInventory(); 191 | }); 192 | 193 | } 194 | 195 | 196 | const resetPoll = function() { 197 | fixPoll = []; 198 | } 199 | 200 | 201 | const refreshPollInventory = function() { 202 | fs.readdir(geoDataDir, function(err, files) { 203 | if(err) { 204 | console.log('failed to get dir', geoDataDir, err); 205 | return; 206 | } 207 | if(files.length===0) { 208 | // geodatadir is empty! 209 | return; 210 | } 211 | pollFiles = JSON.parse(JSON.stringify(files)); 212 | io.emit('pollsize', pollFiles.length); 213 | }); 214 | } 215 | 216 | 217 | const sendPollInventory = function() { 218 | fs.readdir(geoDataDir, function(err, files) { 219 | if(err) { 220 | console.log('failed to get dir', geoDataDir, err); 221 | return; 222 | } 223 | if(files.length===0) { 224 | // geodatadir is empty! 225 | return; 226 | } 227 | pollFiles = JSON.parse(JSON.stringify(files)); 228 | io.emit('pollfiles', files); 229 | }); 230 | } 231 | 232 | const sendPollContent = function(fileName) { 233 | let pollName = false; 234 | // console.log('got poll content request', fileName); 235 | pollFiles.forEach(function(tmpFileName) { 236 | if(fileName === tmpFileName) { 237 | pollName = fileName; 238 | } 239 | }); 240 | if(pollName===false) return; 241 | jsonfile.readFile(geoDataDir + pollName, function(err, obj) { 242 | if(err) { 243 | console.log(err); 244 | io.emit('pollfile', {filename:pollName, error: JSON.stringify(err)}); 245 | return; 246 | } 247 | io.emit('pollfile', {filename:pollName, content: obj}); 248 | }); 249 | } 250 | 251 | 252 | const sendWifiCache = function() { 253 | io.emit('wificache', wifiCache); 254 | } 255 | 256 | const setWifiCache = function() { 257 | if(wirelessisrunning===false) return; 258 | if(Object.keys(wifiCache).length ===0) return; 259 | sendWifiCache(); 260 | } 261 | 262 | 263 | const saveWifi = function(wifi, event) { 264 | const fileName = wifiDataDir + wifi.address.replace(/[^a-z0-9-]+/gi, '_') + '.json'; 265 | 266 | jsonfile.readFile(fileName, function(err, obj) { 267 | 268 | if(obj===undefined || (err!==null && err.code==="ENOENT")) { 269 | // console.log('new wifi device'); 270 | obj = {}; 271 | obj.iface = wifi; 272 | obj.events = []; 273 | } 274 | 275 | // prevent database explosion 276 | if(obj.events.length > wifiMaxHistoryItems) { 277 | while(obj.events.length > wifiMaxHistoryItems) { 278 | obj.events.pop(); 279 | } 280 | } 281 | 282 | obj.events.unshift([gpstime, event, wifi.channel, wifi.quality, wifi.strength]); 283 | 284 | jsonfile.writeFile(fileName, obj, {spaces:0}, function(err, obj) { 285 | if(err) console.error(err); 286 | }); 287 | 288 | }); 289 | } 290 | 291 | 292 | 293 | const sendWifiInventory = function() { 294 | fs.readdir(wifiDataDir, function(err, files) { 295 | if(err) { 296 | console.log('failed to get dir', geoDataDir, err); 297 | return; 298 | } 299 | if(files.length===0) { 300 | // wifidatadir is empty! 301 | return; 302 | } 303 | wifiFiles = JSON.parse(JSON.stringify(files)); 304 | io.emit('wififiles', files); 305 | }); 306 | } 307 | 308 | 309 | const sendWifiContent = function(fileName) { 310 | let wifiName = false; 311 | // console.log('got wifi content request', fileName); 312 | wifiFiles.forEach(function(tmpFileName) { 313 | if(fileName === tmpFileName) { 314 | wifiName = fileName; 315 | } 316 | }); 317 | if(wifiName===false) return; 318 | jsonfile.readFile(wifiDataDir + wifiName, function(err, obj) { 319 | if(err) { 320 | console.log(err); 321 | io.emit('wififile', {filename:wifiName, error: JSON.stringify(err)}); 322 | return; 323 | } 324 | io.emit('wififile', {filename:wifiName, content: obj}); 325 | }); 326 | } 327 | 328 | 329 | const wirelessStart = function() { 330 | if(wirelessisrunning===true) { 331 | console.log("[INFO] Wireless already started"); 332 | return; 333 | } 334 | wireless.enable(function(err) { 335 | if (err) { 336 | console.log("[FAILURE] Unable to enable wireless card. Quitting..."); 337 | return; 338 | } 339 | console.log("[INFO] Wireless card enabled."); 340 | console.log("[INFO] Starting wireless scan..."); 341 | wireless.start(); 342 | wirelessisrunning = true; 343 | }); 344 | } 345 | 346 | 347 | const wirelessStop = function() { 348 | if(wirelessisrunning===false) { 349 | console.log("[INFO] Wireless already stopped"); 350 | return; 351 | } 352 | wireless.disable(function() { 353 | console.log("[INFO] Stopping Wifi"); 354 | wireless.stop(); 355 | wirelessisrunning = false; 356 | }); 357 | } 358 | 359 | 360 | 361 | const onChildStdOut = function(data) { 362 | console.log('[STDOUT]', data.toString()); 363 | } 364 | const onChildStdErr = function(data) { 365 | console.log('[STDOUT]', data.toString()); 366 | } 367 | const onChildClose = function(code) { 368 | console.log("[EOL] Finished with code " + code); 369 | } 370 | 371 | const spawnChild = function(cmd, args, opts, onstdout, onstderr, onclose) { 372 | let child = spawn(cmd, args, opts); 373 | child.stdout.on('data', onstdout); 374 | child.stderr.on('data', onstderr); 375 | child.on('close', onclose); 376 | } 377 | 378 | const dump1090JSON = function() { 379 | // get http://localhost:8080/dump1090/data.json 380 | // write locally 381 | //request('http://localhost:8080/dump1090/data.json').pipe(fs.createWriteStream(htmlDir + '/dump1090/data.json')) 382 | request('http://localhost:8080/dump1090/data.json', function (error, response, body) { 383 | if(response && response.statusCode && response.statusCode==200) { 384 | io.emit('rtlsdr', body); 385 | } 386 | });//.pipe(io.emit('rtlsdr')) 387 | } 388 | 389 | 390 | const startDump1090 = function() { 391 | //execSync(dump1090Dir + 'dump1090 --net --net-http-port 8080 --quiet &'); 392 | stopDump1090(); 393 | spawnChild(dump1090Dir + 'dump1090', ['--net', '--net-http-port', '8080', '--quiet'], {cwd:dump1090Dir}, onChildStdOut, onChildStdErr, function() { 394 | console.log('[INFO] dump1090 exited Successfully!'); 395 | rtlsdrisrunning = false; 396 | }); 397 | 398 | rtlsdrisrunning = true; 399 | clearInterval( dump1090timer ); 400 | dump1090timer = setInterval( dump1090JSON, 5000); 401 | 402 | console.log('[INFO] RTLSDR Device Started Successfully!'); 403 | } 404 | const stopDump1090 = function() { 405 | try { 406 | execSync('sudo killall dump1090'); 407 | } catch(e) { ; } 408 | rtlsdrisrunning = false; 409 | clearInterval( dump1090timer ); 410 | console.log('[INFO] RTLSDR Device Killed Successfully!'); 411 | } 412 | 413 | 414 | const startGPSDaemon = function() { 415 | execSync('sudo service gpsd restart'); 416 | console.log('[INFO] GPS Daemon Started Successfully, will restart server'); 417 | gpsdaemonisrunning = true; 418 | process.exit(0); 419 | } 420 | const stopGPSDaemon = function() { 421 | execSync('sudo service gpsd stop'); 422 | console.log('[INFO] GPS Daemon Stopped Successfully'); 423 | gpsdaemonisrunning = false; 424 | //process.exit(0); 425 | } 426 | 427 | const startGPSDevice = function() { 428 | spawnChild('python', ['scripts/startgps.py'], {}, onChildStdOut, onChildStdErr, function() { 429 | gpsdaemonisrunning = true; 430 | console.log('[INFO] GPS Device Started Successfully!'); 431 | startGPSDaemon(); 432 | }); 433 | } 434 | const stopGPSDevice = function() { 435 | spawnChild('python', ['scripts/stopgps.py'], {}, onChildStdOut, onChildStdErr, function() { 436 | gpsdaemonisrunning = true; 437 | console.log('[INFO] GPS Device Stopped Successfully!'); 438 | stopGPSDaemon(); 439 | }); 440 | } 441 | 442 | const sendDeviceStatus = function() { 443 | io.emit('device-status', { 444 | wirelessisrunning:wirelessisrunning, 445 | gpsdaemonisrunning:gpsdaemonisrunning, 446 | gpshatisrunning:gpshatisrunning, 447 | rtlsdrisrunning:rtlsdrisrunning 448 | }); 449 | } 450 | 451 | 452 | mkdirSync( dataDir ); 453 | mkdirSync( geoDataDir ); 454 | mkdirSync( wifiDataDir); 455 | mkdirSync( rtlsdrDataDir ); 456 | 457 | setInterval(checkInterfaces, 20000); // check for network change every 20 sec 458 | setInterval(setPoll, 1000); 459 | setInterval(setFix, 1000); 460 | setInterval(setWifiCache, 61000); // force wifi cache reload every minute 461 | setInterval(sendDeviceStatus, 1000); // send device status every second 462 | 463 | 464 | listener.connect(function() { 465 | console.log('[INFO] Connected and Listening to GPSD'); 466 | gpsdaemonisrunning = true; 467 | }); 468 | 469 | 470 | listener.watch({class: 'WATCH', nmea: true}); 471 | 472 | // tell express to use ejs for rendering HTML files: 473 | app.set('views', htmlDir); 474 | //app.set('views', htmlDir+'/dump1090'); 475 | app.engine('html', require('ejs').renderFile); 476 | 477 | // feed the dashboard with the apiKey 478 | app.get('/', function(req, res) { 479 | res.render('dashboard.html', { 480 | apiKey: googleMapsApiKey 481 | }); 482 | }); 483 | 484 | app.get('/jquery-ui-timepicker-addon.js', function(req, res) { 485 | res.sendFile(htmlDir + '/jquery-ui-timepicker-addon.js'); 486 | }); 487 | app.get('/gmap.html', function(req, res) { 488 | res.render('gmap.html', { 489 | host: req.headers.host.replace('3000', '8080'), 490 | lat: lastFix.lat, 491 | lon: lastFix.lon 492 | }); 493 | //res.sendFile(htmlDir + '/gmap.html'); 494 | }); 495 | 496 | 497 | app.get('/dump1090/data.json', function(req, res) { 498 | res.sendFile(htmlDir + '/dump1090/data.json'); 499 | }); 500 | 501 | app.get('/dashboard.js', function(req, res) { 502 | res.sendFile(htmlDir + '/dashboard.js'); 503 | }); 504 | app.use(express.static(path.join(__dirname, 'www/img'))); 505 | 506 | http.listen(3000, function() { 507 | console.log('[INFO] Web Server GUI listening on *:3000'); 508 | 509 | gps.on('data', function() { 510 | io.emit('state', gps.state); 511 | //console.log('[state]', gps.state); 512 | if(gps.state.fix && gps.state.fix==='3D') { 513 | if(gps.state.lat===0 || gps.state.lat===null) return; 514 | if(gps.state.lon===0 || gps.state.lon===null) return; 515 | lastFix = JSON.parse(JSON.stringify(gps.state)); 516 | secondsSinceLastFix = 0; 517 | } 518 | if(gps.time) { 519 | gpstime = new Date(gps.time); 520 | } 521 | }); 522 | 523 | 524 | listener.on('raw', function(data) { 525 | gpshatisrunning = true; 526 | try { 527 | gps.update(data); 528 | //console.log('[RAW]', data); 529 | } catch(e) { 530 | console.log('invalid data'); 531 | console.log(data) 532 | console.dir(e); 533 | } 534 | }); 535 | 536 | 537 | io.sockets.on('connection', function (socket) { 538 | 539 | socket.on('gpsenable', function(data) { 540 | console.log('[INFO] Will enable GPS'); 541 | startGPSDevice(); 542 | }); 543 | socket.on('gpsdisable', function(data) { 544 | console.log('[INFO] Will disable GPS'); 545 | stopGPSDevice(); 546 | }); 547 | socket.on('gpsdaemonstart', function(data) { 548 | startGPSDaemon(); 549 | }); 550 | socket.on('gpsdaemonstop', function(data) { 551 | stopGPSDaemon(); 552 | }); 553 | socket.on('rtlsdrenable', function(data) { 554 | wirelessStop(); 555 | startDump1090(); 556 | rtlsdrisrunning = true; 557 | }); 558 | socket.on('rtlsdrdisable', function(data) { 559 | stopDump1090(); 560 | }); 561 | socket.on('wifienable', function(data) { 562 | stopDump1090(); 563 | wirelessStart(); 564 | rtlsdrisrunning = false; 565 | }); 566 | socket.on('wifidisable', function(data) { 567 | wirelessStop(); 568 | }); 569 | 570 | 571 | //socket.on('sms', function(data) { 572 | // console.log('received event sms', data); 573 | // var gammu = "/usr/bin/sudo"; 574 | // var destination = "0606060606"; 575 | // var cmdargs = ''; 576 | // cmdargs = ' /usr/bin/gammu-smsd-inject TEXT ' + destination + ' -text "'+data.msg+'"'; 577 | // console.log(gammu+cmdargs); 578 | // execSync([gammu+cmdargs]); 579 | //}); 580 | 581 | socket.on('reload', function(data) { 582 | // foreferjs 583 | process.exit(0); 584 | }); 585 | 586 | socket.on('poll-files', sendPollInventory); 587 | socket.on('poll-content', sendPollContent); 588 | socket.on('wifi-cache', sendWifiCache); 589 | socket.on('device-status', sendDeviceStatus); 590 | 591 | }); 592 | 593 | setTimeout(function() { 594 | console.log('will reload web UI'); 595 | io.emit('reload', {go:true}); 596 | }, 2000); 597 | 598 | 599 | wirelessStart(); 600 | 601 | }); 602 | 603 | 604 | // Found a new network 605 | wireless.on('appear', function(network) { 606 | const quality = Math.floor(network.quality / 70 * 100); 607 | 608 | network.ssid = network.ssid || '[HIDDEN]'; 609 | 610 | network.encryption_type = 'NONE'; 611 | if (network.encryption_wep) { 612 | network.encryption_type = 'WEP'; 613 | } else if (network.encryption_wpa && network.encryption_wpa2) { 614 | network.encryption_type = 'WPA-WPA2'; 615 | } else if (network.encryption_wpa) { 616 | network.encryption_type = 'WPA'; 617 | } else if (network.encryption_wpa2) { 618 | network.encryption_type = 'WPA2'; 619 | } 620 | io.emit('wifi', {appear:network}); 621 | wifiCache[network.address] = network; 622 | //console.log("[ APPEAR] " + network.ssid + " [" + network.address + "] " + quality + "% " + network.strength + "dBm " + network.encryption_type); 623 | saveWifi(network, 'appear'); 624 | }); 625 | 626 | wireless.on('change', function(network) { 627 | const quality = Math.floor(network.quality / 70 * 100); 628 | 629 | network.ssid = network.ssid || '[HIDDEN]'; 630 | 631 | network.encryption_type = 'NONE'; 632 | if (network.encryption_wep) { 633 | network.encryption_type = 'WEP'; 634 | } else if (network.encryption_wpa && network.encryption_wpa2) { 635 | network.encryption_type = 'WPA-WPA2'; 636 | } else if (network.encryption_wpa) { 637 | network.encryption_type = 'WPA'; 638 | } else if (network.encryption_wpa2) { 639 | network.encryption_type = 'WPA2'; 640 | } 641 | io.emit('wifi', {appear:network}); 642 | wifiCache[network.address] = network; 643 | //console.log("[ APPEAR] " + network.ssid + " [" + network.address + "] " + quality + "% " + network.strength + "dBm " + network.encryption_type); 644 | saveWifi(network, 'change'); 645 | }); 646 | 647 | 648 | // A network disappeared (after the specified threshold) 649 | wireless.on('vanish', function(network) { 650 | io.emit('wifi', {vanish:network}); 651 | if(wifiCache[network.address]!==undefined) { 652 | delete(wifiCache[network.address]); 653 | } 654 | //console.log("[ VANISH] " + network.ssid + " [" + network.address + "] "); 655 | saveWifi(network, 'vanish'); 656 | }); 657 | 658 | wireless.on('error', function(message) { 659 | // io.emit('wifi', {error:network}); 660 | console.log("[ERROR] Wifi / " + message); 661 | wirelessStop(); 662 | /* 663 | wireless.disable(function() { 664 | console.log("[INFO] Stopping Wifi"); 665 | wireless.stop(); 666 | }); 667 | */ 668 | }); 669 | 670 | 671 | // User hit Ctrl + C 672 | process.on('SIGINT', function() { 673 | console.log("\n"); 674 | 675 | if (killing_app) { 676 | console.log("[INFO] Double SIGINT, Killing without cleanup!"); 677 | process.exit(); 678 | } 679 | 680 | killing_app = true; 681 | console.log("[INFO] Gracefully shutting down from SIGINT (Ctrl+C)"); 682 | console.log("[INFO] Disabling RTL-SDR Dongle"); 683 | stopDump1090(); 684 | console.log("[INFO] Disabling Wifi Adapter..."); 685 | wireless.disable(function() { 686 | console.log("[INFO] Stopping Wifi and Exiting..."); 687 | wireless.stop(); 688 | }); 689 | 690 | }); 691 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "signal-logr", 3 | "version": "1.0.0", 4 | "description": "GPS & Wifi Signal Logger + GUI for Raspberry Pi", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "tobozo", 10 | "license": "MIT", 11 | "dependencies": { 12 | "dotenv": "^2.0.0", 13 | "ejs": "^2.5.2", 14 | "express": "^4.14.0", 15 | "gps": "^0.2.0", 16 | "jsonfile": "^2.4.0", 17 | "node-gpsd": "^0.2.6", 18 | "request": "^2.83.0", 19 | "socket.io": "^1.5.1", 20 | "wireless": "^0.3.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scripts/startgps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # gps.py 4 | # Script to start GPS on the WaveShare GSM/GPRS/GNSS hat https://www.waveshare.com/wiki/GSM/GPRS/GNSS_HAT 5 | # 6 | import wiringpi 7 | import serial 8 | import time, sys 9 | import datetime 10 | import os, signal 11 | import argparse 12 | 13 | ##############Begin process command line ########### 14 | parser = argparse.ArgumentParser(description='Enable GPS and display output') 15 | 16 | parser.add_argument('--port', '-p', 17 | help='Serial port', 18 | type=str, 19 | default='/dev/ttyAMA0') 20 | 21 | args = parser.parse_args() 22 | serial_port = args.port 23 | ##############End process command line ########### 24 | 25 | # Wait for the nadhat answer 26 | def wait_Answer(code): 27 | time.sleep(0.3) 28 | rep = ser.read(ser.inWaiting()) # Check if the board answers 29 | if rep != "": 30 | if code in rep: 31 | print "Answers : "+code 32 | else : 33 | print code+" not received : No communication with the board" 34 | sys.exit(0) 35 | else : 36 | print "No response from the board" 37 | sys.exit(1) 38 | 39 | # Serial port init 40 | ser = serial.Serial( 41 | port = serial_port, 42 | baudrate = 115200, 43 | parity = serial.PARITY_NONE, 44 | stopbits = serial.STOPBITS_ONE, 45 | bytesize = serial.EIGHTBITS 46 | ) 47 | 48 | # Check the communication with the nadhat board 49 | ser.write("AT\r") # Send AT command 50 | print "AT\r" 51 | wait_Answer("OK") 52 | 53 | print "AT+CGNSTST=0\r" # turn off data sending (otherwise wait_Answer might raise false positives) 54 | 55 | print "AT+CGNSPWR=1\r" # Turn on the power of GPS 56 | ser.write("AT+CGNSPWR=1\r") 57 | wait_Answer("OK") 58 | 59 | #print "AT+CGNSSEQ=\"RMC\"\r" # RMC mode, Recommended Minimum Specific GNSS Data 60 | #ser.write("AT+CGNSSEQ=\"RMC\"\r") 61 | #wait_Answer("OK") 62 | 63 | #print "AT+CGNSINF" # GNSS navigation information parsed from NMEA sentences. 64 | #ser.write("AT+CGNSINF\r") 65 | #wait_Answer("OK") 66 | 67 | #print "AT+CGNSURC=2" # set URC reporting every 2 seconds 68 | #ser.write("AT+CGNSURC=2\r") 69 | ser.write("AT+CGNSURC=0\r") 70 | #wait_Answer("OK") 71 | 72 | print "AT+CGNSTST=1\r" # Send data received to UART 73 | ser.write("AT+CGNSTST=1\r") 74 | 75 | print "END" 76 | 77 | -------------------------------------------------------------------------------- /scripts/stopgps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # stopgps.py 4 | # Script to stop GPS from spamming the serial 5 | # 6 | import wiringpi 7 | import serial 8 | import time, sys 9 | import datetime 10 | import os, signal 11 | import argparse 12 | 13 | ##############Begin process command line ########### 14 | parser = argparse.ArgumentParser(description='Enable GPS and display output') 15 | 16 | parser.add_argument('--port', '-p', 17 | help='Serial port', 18 | type=str, 19 | default='/dev/ttyAMA0') 20 | 21 | args = parser.parse_args() 22 | serial_port = args.port 23 | ##############End process command line ########### 24 | 25 | # Wait for the nadhat answer 26 | def wait_Answer(code): 27 | time.sleep(0.3) 28 | rep = ser.read(ser.inWaiting()) # Check if the board answers 29 | if rep != "": 30 | if code in rep: 31 | print "Answers : "+code 32 | else : 33 | print code+" not received : No communication with the board" 34 | sys.exit(0) 35 | else : 36 | print "No response from the board" 37 | sys.exit(1) 38 | 39 | # Serial port init 40 | ser = serial.Serial( 41 | port = serial_port, 42 | baudrate = 115200, 43 | parity = serial.PARITY_NONE, 44 | stopbits = serial.STOPBITS_ONE, 45 | bytesize = serial.EIGHTBITS 46 | ) 47 | 48 | # Check the communication with the nadhat board 49 | ser.write("AT\r") # Send AT command 50 | print "AT\r" 51 | wait_Answer("OK") 52 | 53 | print "AT+CGNSTST=0\r" # turn off data sending (otherwise wait_Answer might raise false positives) 54 | 55 | print "END" 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /signal-logr-rtl-sdr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobozo/signal-logr/5228b0f6c50e7d698a4ed0bef006ea793c1e5ce0/signal-logr-rtl-sdr.png -------------------------------------------------------------------------------- /signal-logr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobozo/signal-logr/5228b0f6c50e7d698a4ed0bef006ea793c1e5ce0/signal-logr.png -------------------------------------------------------------------------------- /www/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GPS/Wifi Dashboard 5 | 6 | 7 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 |
253 |
254 |
GPS Satellites
255 |
256 |
Wifi Devices
257 |
258 |
259 | 260 |
261 |
GPS Info
262 | 263 | 264 | 265 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 296 | 297 |
DOP 266 | P: - 267 | V: - 268 | H: 269 | Date
LatLng
Alt.Speed
FIX total (0)Sats in Use /
293 | 294 | 0 since last fix 295 |
298 |
299 | 300 |
301 |
302 |
303 | 304 |
305 |
306 |
307 |
308 |
309 | 310 | 311 |
312 |
313 | Wifi 314 | 318 | sorted by 319 | 323 |
324 |
Loading...
325 |
326 |
wpa+wpa2
327 |
wpa2
328 |
wpa
329 |
wep
330 |
none
331 |
332 |
333 |
334 |
335 |
336 |
337 | GPS Heat Map Options 338 |
339 | Min Radius: 340 | After: 341 | Before 342 | 343 | 344 | 345 |
346 | Layers: 347 | 348 | 349 | 350 | 351 |
352 |
353 | 354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 | 366 | 367 | 368 | 369 | 370 | -------------------------------------------------------------------------------- /www/dashboard.js: -------------------------------------------------------------------------------- 1 | var mapElement, 2 | parent, 3 | mapOptions, 4 | map, 5 | marker, 6 | circle, 7 | bounds, 8 | latLng, 9 | openMarker, 10 | lastFix, 11 | lastState, 12 | nmeaInfo, 13 | rawData, 14 | secondsSinceLastFix = 0, 15 | totalFixes = 0, 16 | updateFreq = 1000, 17 | updateTimer = false, 18 | timeSinceLastPoll = 0, 19 | fixPoll = [], 20 | heatmap, 21 | heatmapData = [], 22 | heatmapFiles = {}, 23 | heatmapCount = 0, 24 | pollReceiving = false, 25 | lastPoint = false, 26 | radius = 15, 27 | filterBefore, 28 | filterAfter, 29 | wifiCache = {}, 30 | wifiTimeline = [], 31 | wifiSort = 'quality', 32 | wifiName = 'ssid', 33 | $wifilist = $('.wifi-list'), 34 | deviceStatus = { 35 | wifi:null, 36 | rtlsdr:null, 37 | gpshat:null, 38 | gpsdaemon:null 39 | }, 40 | listKMLType = ['Approch', 'Departure', 'Transit', 'Custom1', 'Custom2'], 41 | listKMLs = localStorage['listKMLs'] || [], 42 | Planes = {}, 43 | PlanesOnMap = 0, 44 | PlanesOnTable = 0, 45 | PlanesToReap = 0, 46 | SelectedPlane = null, 47 | SpecialSquawk = false, 48 | iSortCol=-1, 49 | bSortASC=true, 50 | bDefaultSortASC=true, 51 | iDefaultSortCol=3, 52 | Metric = false, 53 | MarkerColor = "rgb(127, 127, 127)", 54 | SelectedColor = "rgb(225, 225, 225)", 55 | StaleColor = "rgb(190, 190, 190)", 56 | SiteShow = false, 57 | SiteCircles = true, // true or false (Only shown if SiteShow is true) 58 | // In nautical miles or km (depending settings value 'Metric') 59 | SiteCirclesDistances = new Array(100,150,200) 60 | ; 61 | 62 | 63 | var socket = io(); 64 | 65 | var rad = function(x) { 66 | return x * Math.PI / 180; 67 | }; 68 | 69 | var getDistance = function(p1, p2) { 70 | var R = 6378137; // Earth ^ ^ s mean radius in meter 71 | var dLat = rad(p2.lat() - p1.lat()); 72 | var dLong = rad(p2.lng() - p1.lng()); 73 | var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + 74 | Math.cos(rad(p1.lat())) * Math.cos(rad(p2.lat())) * 75 | Math.sin(dLong / 2) * Math.sin(dLong / 2); 76 | var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 77 | var d = R * c; 78 | return d; // returns the distance in meter 79 | }; 80 | 81 | 82 | String.prototype.toHHMMSS = function () { 83 | var sec_num = parseInt(this, 10); // don't forget the second param 84 | var hours = Math.floor(sec_num / 3600); 85 | var minutes = Math.floor((sec_num - (hours * 3600)) / 60); 86 | var seconds = sec_num - (hours * 3600) - (minutes * 60); 87 | 88 | if (10 > hours) {hours = "0"+hours;} 89 | if (10 > minutes) {minutes = "0"+minutes;} 90 | if (10 > seconds) {seconds = "0"+seconds;} 91 | return hours+':'+minutes+':'+seconds; 92 | } 93 | 94 | String.prototype.replaceAll = function(search, replacement) { 95 | var target = this; 96 | return target.replace(new RegExp(search, 'g'), replacement); 97 | }; 98 | 99 | 100 | google.maps.event.addDomListener(window, 'load', initializeMap); 101 | 102 | var renderIface = function(iface) { 103 | var wifibar = '
'; 104 | var $ifacename = $('
'+iface[wifiName]+'
'); 105 | var $ifacebox = $('
') 106 | var $signalbox = $('
'); 107 | var clearfix = '
'; 108 | var signalstrength = Math.floor( iface.quality / 20 ); 109 | var encryption_type = 'none'; 110 | 111 | $ifacename.appendTo($ifacebox); 112 | 113 | for(var i=5;i>0;i--) { 114 | if(signalstrength>=i) { 115 | $(wifibar).appendTo($signalbox); 116 | } else { 117 | $(wifibar).addClass('off').appendTo($signalbox); 118 | } 119 | } 120 | 121 | $signalbox.prependTo($ifacebox); 122 | 123 | $ifacebox.css({ 124 | "background-image": "linear-gradient(to right, lightgreen, rgba(125,0,0,0.5) "+ ( iface.quality - 1) + "%, transparent " + iface.quality + "%)" 125 | }); 126 | 127 | if (iface.encryption_wep) { 128 | encryption_type = 'wep'; 129 | } else if (iface.encryption_wpa && iface.encryption_wpa2) { 130 | encryption_type = 'wpa-wpa2'; 131 | } else if (iface.encryption_wpa) { 132 | encryption_type = 'wpa'; 133 | } else if (iface.encryption_wpa2) { 134 | encryption_type = 'wpa2'; 135 | } 136 | 137 | $ifacename.attr('data-encryption-type', encryption_type); 138 | $ifacebox.attr('data-iface-addr', iface.address); 139 | $ifacebox.appendTo($wifilist); 140 | $(clearfix).appendTo($ifacebox); 141 | } 142 | 143 | 144 | var renderWifiCache = function(data) { 145 | var ifaceList = Object.keys(data).sort(function(a,b){return data[b][wifiSort]-data[a][wifiSort]}); 146 | wifiCache = data; 147 | $wifilist.empty(); 148 | // console.log(ifaceList); 149 | ifaceList.forEach(function(iface) { 150 | renderIface(wifiCache[iface]); 151 | }); 152 | $wifilist.attr('data-iface-size', ifaceList.length); 153 | updateWifi(wifiCache); 154 | } 155 | 156 | 157 | socket.on('rtlsdr', function(data) { 158 | data = JSON.parse(data); 159 | console.log(data); 160 | PlanesOnMap = 0 161 | SpecialSquawk = false; 162 | 163 | // Loop through all the planes in the data packet 164 | for (var j=0; j < data.length; j++) { 165 | // Do we already have this plane object in Planes? 166 | // If not make it. 167 | if (Planes[data[j].hex]) { 168 | var plane = Planes[data[j].hex]; 169 | } else { 170 | var plane = jQuery.extend(true, {}, planeObject); 171 | } 172 | 173 | /* For special squawk tests 174 | if (data[j].hex == '48413x') { 175 | data[j].squawk = '7700'; 176 | } //*/ 177 | 178 | // Set SpecialSquawk-value 179 | if (data[j].squawk == '7500' || data[j].squawk == '7600' || data[j].squawk == '7700') { 180 | SpecialSquawk = true; 181 | } 182 | 183 | // Call the function update 184 | plane.funcUpdateData(data[j]); 185 | 186 | // Copy the plane into Planes 187 | Planes[plane.icao] = plane; 188 | } 189 | 190 | PlanesOnTable = data.length; 191 | 192 | refreshTableInfo(); 193 | refreshSelected(); 194 | reaper(); 195 | 196 | }); 197 | 198 | 199 | socket.on('reload', function() { location.reload(); }); 200 | 201 | 202 | socket.on('device-status', function(data) { 203 | 204 | if(data.wirelessisrunning != deviceStatus.wifi) { 205 | deviceStatus.wifi = data.wirelessisrunning; 206 | if(deviceStatus.wifi) { 207 | $('.device-wifi').removeClass('disabled') 208 | $('.wifi-item').show(); 209 | $('.rtlsdr-wrapper').hide(); 210 | marker.setVisible(true); 211 | } else { 212 | $('.device-wifi').addClass('disabled'); 213 | $('.wifi-item').hide(); 214 | } 215 | } 216 | if(data.gpsdaemonisrunning != deviceStatus.gpsdaemon) { 217 | deviceStatus.gpsdaemon = data.gpsdaemonisrunning; 218 | if(deviceStatus.gpsdaemon) 219 | $('.device-gpsdaemon').removeClass('disabled'); 220 | else 221 | $('.device-gpsdaemon').addClass('disabled'); 222 | } 223 | if(data.rtlsdrisrunning != deviceStatus.rtlsdr) { 224 | deviceStatus.rtlsdr = data.rtlsdrisrunning; 225 | if(deviceStatus.rtlsdr) { 226 | $('.device-rtlsdr').removeClass('disabled') 227 | $('.wifi-item').hide(); 228 | $('.rtlsdr-wrapper').show(); 229 | marker.setVisible(false); 230 | } else { 231 | $('.device-rtlsdr').addClass('disabled'); 232 | $('.wifi-item').show(); 233 | $('.rtlsdr-wrapper').hide(); 234 | marker.setVisible(true); 235 | } 236 | } 237 | if(data.gpshatisrunning != deviceStatus.gpshat) { 238 | deviceStatus.gpshat = data.gpshatisrunning; 239 | if(deviceStatus.gpshat) 240 | $('.device-gpshat').removeClass('disabled') 241 | else 242 | $('.device-gpshat').addClass('disabled'); 243 | } 244 | 245 | }); 246 | 247 | socket.on('wificache', renderWifiCache); 248 | 249 | socket.on('wifi', function(data) { 250 | var event = Object.keys(data)[0]; 251 | var wifi = data[event]; 252 | switch(event) { 253 | case 'vanish': 254 | if(wifiCache[wifi.address]!==undefined) { 255 | delete(wifiCache[wifi.address]); 256 | } 257 | break; 258 | case 'appear': 259 | case 'change': 260 | wifiCache[wifi.address] = wifi; 261 | break; 262 | } 263 | renderWifiCache(wifiCache); 264 | //console.log('wifi', event, wifi.ssid, wifi.address, Object.keys(wifiCache).length); 265 | }); 266 | 267 | 268 | socket.on('state', function(state) { 269 | //console.log('state', state); 270 | updateSatellite(state); 271 | updateTable(state); 272 | updateMap(state); 273 | }); 274 | 275 | 276 | socket.on('pollsize', function(data) { 277 | $('#pollsize').attr("data-poll-currentsize", data); 278 | }); 279 | 280 | 281 | socket.on('pollfiles', function(data) { 282 | data.forEach(function(file) { 283 | if(heatmapFiles[file]!==undefined) return; 284 | 285 | stringDate = file.replaceAll("_", ":").replace(":000Z.json", ".000Z"); 286 | propDate = new Date( stringDate ).getTime(); 287 | if(!isNaN(filterBefore) && propDate > filterBefore ) return; 288 | if(!isNaN(filterAfter) && filterAfter > propDate ) return; 289 | 290 | heatmapFiles[file] = []; 291 | }); 292 | setOnePoll(); 293 | }); 294 | 295 | 296 | socket.on('pollfile', function(data) { 297 | var progress = 0; 298 | 299 | if(heatmapFiles[data.filename]!==undefined && heatmapFiles[data.filename].length>0) return; 300 | 301 | if(data.content===undefined) { 302 | data.content = []; 303 | } 304 | 305 | data.content.forEach(function(obj, index) { 306 | if(obj.wifilist!==undefined) { 307 | //console.log(obj.wifilist); 308 | //throw('bah'); 309 | return; // skip wifilist 310 | } 311 | if(obj.lat===null || obj.lon===null) return; // exclude null coords 312 | if(0- -(obj.lat).toFixed(4)===0 || 0- -(obj.lon).toFixed(4)===0) return; // exclude zero coords 313 | var thisPoint = new google.maps.LatLng(obj.lat, obj.lon); 314 | var distance = 0; 315 | var objTime = new Date(obj.time).getTime(); 316 | if(lastPoint!==false) { 317 | lastObject = obj; 318 | distance = google.maps.geometry.spherical.computeDistanceBetween( thisPoint, lastPoint ); 319 | if( radius > distance ) { 320 | return; 321 | } 322 | if(!isNaN(filterBefore) && objTime > filterBefore ) return; 323 | if(!isNaN(filterAfter) && filterAfter > objTime ) return; 324 | } 325 | lastPoint = thisPoint; 326 | heatmapData.push(new google.maps.LatLng(obj.lat, obj.lon)); 327 | bounds.extend(thisPoint); 328 | }); 329 | 330 | heatmapFiles[data.filename] = data.content; 331 | heatmapCount++; 332 | 333 | if(data.error) { 334 | console.log('ignoring invalid data for', data.filename); 335 | delete(heatmapFiles[data.filename]); 336 | } 337 | 338 | progress = Math.floor( (heatmapCount / Object.keys(heatmapFiles).length) * 100 ); 339 | 340 | $('#pollsize').css('background-image', 'linear-gradient(to right, lightgray, rgba(0,0,0,0.5) '+(progress-1)+'%, transparent '+progress+'%)') 341 | .attr('data-pollsize', heatmapData.length); 342 | 343 | if(progress===100) { 344 | if(heatmapData.length>0) { 345 | $('.button-control-heatmap').prop("disabled", false); 346 | $('#pollsize').prop('disabled', true).css('background-image', ''); 347 | map.fitBounds(bounds); 348 | initHeatmap(); 349 | } 350 | } else { 351 | setOnePoll(); 352 | } 353 | }); 354 | 355 | //Width and height 356 | var width = 500; 357 | var barHeight = 100; 358 | var padding = 1; 359 | var paddingGPS = 4; 360 | 361 | var dataset = []; 362 | 363 | //Create SVG element for satellites 364 | var svgSatellite = d3.select(".dataviz .satellites") 365 | .append("svg") 366 | .attr("width", width) 367 | .attr("height", barHeight + 50) 368 | .append("g"); 369 | 370 | 371 | var svgWifi = d3.select(".dataviz .wifi") 372 | .append("svg") 373 | .attr("width", width) 374 | .attr("height", barHeight) 375 | .append("g"); 376 | 377 | function updateWifi(obj) { 378 | var data = []; 379 | var keys = Object.keys(obj); 380 | var rect; 381 | 382 | // turn incoming object into an array 383 | keys.forEach(function(iface){ data.push(obj[iface]); }); 384 | 385 | rect = svgWifi.selectAll("rect").data(data); 386 | 387 | rect.enter().append("rect"); 388 | rect.attr("x", function(d, i) { 389 | return i * (width / data.length); 390 | }).attr("y", function(d) { 391 | var v = d.quality || 10; 392 | return barHeight - (v * 1); 393 | }).attr("width", width / data.length - padding).attr("height", function(d) { 394 | var v = d.quality || 10; 395 | return v * 4; 396 | }).attr("fill", function(d) { 397 | var v = 255 + d.strength*2 || 0; 398 | if (d.strength<-67) { 399 | return "rgb(0, 0, " + (v * 1 | 0) + ")"; 400 | } 401 | return "rgb(" + (v * 1 | 0) + ", 0, 0)"; 402 | }); 403 | rect.exit().remove(); 404 | } 405 | 406 | function updateSatellite(data) { 407 | 408 | var rect = svgSatellite.selectAll("rect").data(data.satsVisible); 409 | var text = svgSatellite.selectAll("text").data(data.satsVisible); 410 | rect.enter().append("rect"); 411 | rect.enter().append("text"); 412 | rect.attr("x", function(d, i) { 413 | return i * (width / data.satsVisible.length); 414 | }).attr("y", function(d) { 415 | var v = d.snr || 10; 416 | return barHeight - (v * 2); 417 | }).attr("width", width / data.satsVisible.length - paddingGPS).attr("height", function(d) { 418 | var v = d.snr || 10; 419 | return v * 2; 420 | }).attr("fill", function(d) { 421 | var v = d.snr || 10; 422 | if (-1 !== data.satsActive.indexOf(d.prn)) { 423 | return "rgb(0, 0, " + (v * 10 | 0) + ")"; 424 | } 425 | return "rgb(" + (v * 10 | 0) + ", 0, 0)"; 426 | }); 427 | text.attr("x", function(d, i) { 428 | return 15 + i * (width / data.satsVisible.length); 429 | }).attr("y", barHeight + 20).text(function(d) { 430 | return d.prn; 431 | }).attr("fill", "black"); 432 | rect.exit().remove(); 433 | text.exit().remove(); 434 | } 435 | 436 | 437 | function updateTable(state) { 438 | lastState = state; 439 | $("#date").text(state.time); 440 | $("#lat").text(state.lat); 441 | $("#lon").text(state.lon); 442 | $("#alt").text(state.alt); 443 | $("#speed").text(state.speed); 444 | $("#status").text(state.fix); 445 | $("#pdop").text(state.pdop); 446 | $("#vdop").text(state.vdop); 447 | $("#hdop").text(state.hdop); 448 | $("#active").text(state.satsActive.length); 449 | $("#view").text(state.satsVisible.length); 450 | } 451 | 452 | 453 | function updateMap(state) { 454 | if(state.lat===undefined || state.lon===undefined) return; 455 | if(state.lat===null || state.lon===null) return; 456 | if(google===undefined) return; 457 | if(google.maps===undefined) return; 458 | if(map==undefined) return; 459 | if(state.fix=='2D' || state.fix=='3D') { 460 | // do not overload UI + localStorage 461 | if(updateTimer) return; 462 | updateTimer = setTimeout(function() { 463 | updateTimer = false; 464 | }, updateFreq); 465 | 466 | if(state.hdop!== undefined && state.pdop!==undefined) { 467 | if(state.hdop>50 || state.pdop>50) return; 468 | if(2>state.hdop || 2>state.pdop) return; 469 | circle.setRadius( Math.sqrt(state.hdop*state.pdop) *10 ); 470 | } 471 | 472 | localStorage.setItem('lastFix', JSON.stringify(state)); 473 | 474 | setPoll(lastFix); 475 | 476 | lastFix = state; 477 | totalFixes++; 478 | 479 | $('#fix-progress').trigger('reset'); 480 | 481 | latLng = new google.maps.LatLng(state.lat, state.lon); 482 | 483 | localStorage['CenterLat'] = state.lat; 484 | localStorage['CenterLon'] = state.lon; 485 | 486 | marker.setPosition(latLng); 487 | circle.setCenter(latLng); 488 | map.setCenter(latLng); 489 | } 490 | } 491 | 492 | 493 | function initializeMap() { 494 | 495 | $fixProgress = $('#fix-progress'); 496 | $secondsSinceLastFix = $('#secondsSinceLastFix'); 497 | $totalFixes = $('#totalFixes'); 498 | $pollisze = $('#pollsize'); 499 | 500 | lastFix = JSON.parse(localStorage.getItem('lastFix')); 501 | 502 | if(lastFix===null) { 503 | latLng = new google.maps.LatLng(48.8, 2.3); 504 | } else { 505 | latLng = new google.maps.LatLng(lastFix.lat, lastFix.lon); 506 | } 507 | 508 | mapElement = document.getElementById('mapid'); 509 | 510 | /* 511 | mapOptions = { 512 | center: latLng, 513 | zoom: 14, 514 | mapTypeId: google.maps.MapTypeId.ROADMAP 515 | }; 516 | */ 517 | 518 | 519 | // Get current map settings 520 | //CenterLat = Number(localStorage['CenterLat']) || latL; 521 | //CenterLon = Number(localStorage['CenterLon']) || CONST_CENTERLON; 522 | ZoomLvl = Number(localStorage['ZoomLvl']) || 5; 523 | // Make a list of all the available map IDs 524 | var mapTypeIds = []; 525 | for(var type in google.maps.MapTypeId) { 526 | mapTypeIds.push(google.maps.MapTypeId[type]); 527 | } 528 | // Push OSM on to the end 529 | mapTypeIds.push("OSM"); 530 | mapTypeIds.push("dark_map"); 531 | 532 | // Styled Map to outline airports and highways 533 | var styles = [ 534 | { 535 | "featureType": "administrative", 536 | "stylers": [ 537 | { "visibility": "off" } 538 | ] 539 | },{ 540 | "featureType": "landscape", 541 | "stylers": [ 542 | { "visibility": "off" } 543 | ] 544 | },{ 545 | "featureType": "poi", 546 | "stylers": [ 547 | { "visibility": "off" } 548 | ] 549 | },{ 550 | "featureType": "road", 551 | "stylers": [ 552 | { "visibility": "off" } 553 | ] 554 | },{ 555 | "featureType": "transit", 556 | "stylers": [ 557 | { "visibility": "off" } 558 | ] 559 | },{ 560 | "featureType": "landscape", 561 | "stylers": [ 562 | { "visibility": "on" }, 563 | { "weight": 8 }, 564 | { "color": "#000000" } 565 | ] 566 | },{ 567 | "featureType": "water", 568 | "stylers": [ 569 | { "lightness": -74 } 570 | ] 571 | },{ 572 | "featureType": "transit.station.airport", 573 | "stylers": [ 574 | { "visibility": "on" }, 575 | { "weight": 8 }, 576 | { "invert_lightness": true }, 577 | { "lightness": 27 } 578 | ] 579 | },{ 580 | "featureType": "road.highway", 581 | "stylers": [ 582 | { "visibility": "simplified" }, 583 | { "invert_lightness": true }, 584 | { "gamma": 0.3 } 585 | ] 586 | },{ 587 | "featureType": "road", 588 | "elementType": "labels", 589 | "stylers": [ 590 | { "visibility": "off" } 591 | ] 592 | } 593 | ] 594 | 595 | // Add our styled map 596 | var styledMap = new google.maps.StyledMapType(styles, {name: "Dark Map"}); 597 | 598 | // Define the Google Map 599 | var mapOptions = { 600 | center: latLng, // new google.maps.LatLng(CenterLat, CenterLon), 601 | zoom: ZoomLvl, 602 | mapTypeId: google.maps.MapTypeId.ROADMAP, 603 | mapTypeControl: true, 604 | streetViewControl: false, 605 | mapTypeControlOptions: { 606 | mapTypeIds: mapTypeIds, 607 | position: google.maps.ControlPosition.TOP_LEFT, 608 | style: google.maps.MapTypeControlStyle.DROPDOWN_MENU 609 | } 610 | }; 611 | 612 | 613 | 614 | 615 | map = new google.maps.Map(mapElement, mapOptions) 616 | bounds = new google.maps.LatLngBounds(); 617 | marker = new google.maps.Marker({position: latLng, map: map}); 618 | circle = new google.maps.Circle({ 619 | strokeColor: '#FF0000', 620 | strokeOpacity: 0.8, 621 | strokeWeight: 2, 622 | fillColor: '#FF0000', 623 | fillOpacity: 0.35, 624 | map: map, 625 | center: latLng, 626 | radius: 100 627 | }); 628 | 629 | 630 | //Define OSM map type pointing at the OpenStreetMap tile server 631 | map.mapTypes.set("OSM", new google.maps.ImageMapType({ 632 | getTileUrl: function(coord, zoom) { 633 | return "http://tile.openstreetmap.org/" + zoom + "/" + coord.x + "/" + coord.y + ".png"; 634 | }, 635 | tileSize: new google.maps.Size(256, 256), 636 | name: "OpenStreetMap", 637 | maxZoom: 18 638 | })); 639 | 640 | map.mapTypes.set("dark_map", styledMap); 641 | 642 | // Listeners for newly created Map 643 | google.maps.event.addListener(map, 'center_changed', function() { 644 | localStorage['CenterLat'] = map.getCenter().lat(); 645 | localStorage['CenterLon'] = map.getCenter().lng(); 646 | }); 647 | 648 | google.maps.event.addListener(map, 'zoom_changed', function() { 649 | localStorage['ZoomLvl'] = map.getZoom(); 650 | }); 651 | 652 | 653 | $('#focus-lastfix').on('click', function() { 654 | latLng = new google.maps.LatLng( document.getElementById('lat').innerHTML, document.getElementById('lon').innerHTML ); 655 | map.setCenter(latLng); 656 | marker.setPosition(latLng); 657 | circle.setCenter(latLng); 658 | }); 659 | 660 | $('#reload-ws').on('click', function() { 661 | $(this).attr('disabled', true); 662 | socket.emit('reload', "blah"); 663 | setTimeout(function() { 664 | top.location = top.location 665 | }, 10000); 666 | }); 667 | 668 | $('.device-gpsdaemon').on('click', function() { 669 | if(deviceStatus.gpsdaemon===true) { 670 | socket.emit('gpsdaemonstop'); 671 | } else { 672 | socket.emit('gpsdaemonstart'); 673 | } 674 | }); 675 | $('.device-gpshat').on('click', function() { 676 | if(deviceStatus.gpshat===true) { 677 | socket.emit('gpsdisable'); 678 | } else { 679 | socket.emit('gpsenable'); 680 | } 681 | }); 682 | $('.device-rtlsdr').on('click', function() { 683 | if(deviceStatus.rtlsdr===true) { 684 | socket.emit('rtlsdrdisable'); 685 | } else { 686 | socket.emit('rtlsdrenable'); 687 | } 688 | }); 689 | $('.device-wifi').on('click', function() { 690 | if(deviceStatus.wifi===true) { 691 | socket.emit('wifidisable'); 692 | } else { 693 | socket.emit('wifienable'); 694 | } 695 | }); 696 | 697 | $fixProgress.on('reset', function() { 698 | $fixProgress.val(60); 699 | $totalFixes.text(totalFixes); 700 | secondsSinceLastFix = 0; 701 | $secondsSinceLastFix.text('00:00:01'); 702 | }); 703 | 704 | setInterval(function() { 705 | var fixVal = 0- -$fixProgress.val(); 706 | secondsSinceLastFix++; 707 | timeSinceLastPoll++; 708 | $secondsSinceLastFix.text( (""+secondsSinceLastFix).toHHMMSS() ); 709 | if(fixVal==0) return; 710 | $fixProgress.val(fixVal-1); 711 | }, 1000); 712 | 713 | setWifiSort(); 714 | } 715 | 716 | 717 | function setPoll() { 718 | fixPoll.push(lastFix); 719 | // save every minute or every 100 records 720 | if(fixPoll.length>100 || timeSinceLastPoll > 60) { 721 | // save 722 | socket.emit('setpoll', fixPoll); 723 | // purge 724 | fixPoll = []; 725 | timeSinceLastPoll = 0; 726 | return; 727 | } 728 | } 729 | 730 | 731 | function togglePollFiles() { 732 | if(pollReceiving===true) { 733 | stopPollFiles(); 734 | } else { 735 | getPollFiles(); 736 | } 737 | } 738 | 739 | 740 | function getPollFiles() { 741 | pollReceiving = true; 742 | $('#pollsize').attr('data-label', 'Stop retrieving files'); 743 | radius = 0- -$('#radius').val(); 744 | filterBeforeString = $('#date-filter-before').val().replace(" ", "") + ".000Z"; 745 | filterAfterString = $('#date-filter-after').val().replace(" ", "") + ".000Z"; 746 | 747 | filterBefore = new Date( filterBeforeString ).getTime(); 748 | filterAfter = new Date( filterAfterString ).getTime(); 749 | $('.button-control-heatmap').prop("disabled", true); 750 | socket.emit('poll-files', {blah:true}); 751 | } 752 | 753 | 754 | function stopPollFiles() { 755 | pollReceiving = false; 756 | $('.button-control-heatmap').prop("disabled", false); 757 | $('#pollsize').attr('data-label', 'Start retrieving files'); 758 | if(heatmapData.length>0) { 759 | $('#initheatmap-button').css('display', 'inline-block'); 760 | } else { 761 | $('#initheatmap-button').css('display', 'none'); 762 | } 763 | } 764 | 765 | 766 | function setOnePoll() { 767 | //var propDate; 768 | //var stringDate; 769 | if(pollReceiving!==true) return; 770 | for(prop in heatmapFiles) { 771 | if(!prop.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2}/)) continue; 772 | if(heatmapFiles[prop].length===0) { 773 | socket.emit('poll-content', prop); 774 | return; 775 | } 776 | } 777 | } 778 | 779 | function toggleHeatmap() { 780 | heatmap.setMap(heatmap.getMap() ? null : map); 781 | } 782 | 783 | function changeGradient() { 784 | var gradient = [ 785 | 'rgba(0, 255, 255, 0)', 786 | 'rgba(0, 255, 255, 1)', 787 | 'rgba(0, 191, 255, 1)', 788 | 'rgba(0, 127, 255, 1)', 789 | 'rgba(0, 63, 255, 1)', 790 | 'rgba(0, 0, 255, 1)', 791 | 'rgba(0, 0, 223, 1)', 792 | 'rgba(0, 0, 191, 1)', 793 | 'rgba(0, 0, 159, 1)', 794 | 'rgba(0, 0, 127, 1)', 795 | 'rgba(63, 0, 91, 1)', 796 | 'rgba(127, 0, 63, 1)', 797 | 'rgba(191, 0, 31, 1)', 798 | 'rgba(255, 0, 0, 1)' 799 | ]; 800 | heatmap.set('gradient', heatmap.get('gradient') ? null : gradient); 801 | } 802 | 803 | 804 | function changeRadius() { 805 | heatmap.set('radius', heatmap.get('radius') ? null : 20); 806 | } 807 | 808 | 809 | function changeHeatmapOpacity() { 810 | heatmap.set('opacity', heatmap.get('opacity') ? null : 0.2); 811 | } 812 | 813 | 814 | function changecircleOpacity() { 815 | circle.setVisible(!circle.getVisible()); 816 | } 817 | 818 | 819 | function getPoints() { 820 | return heatmapData; 821 | } 822 | 823 | function setWifiSort() { 824 | wifiSort = $('#wifi-sort-by').val(); 825 | socket.emit('wifi-cache', "blah"); 826 | } 827 | 828 | function setWifiName() { 829 | wifiName = $('#wifi-named-by').val(); 830 | socket.emit('wifi-cache', 'blah'); 831 | } 832 | 833 | function initHeatmap() { 834 | heatmap = new google.maps.visualization.HeatmapLayer({ 835 | data: getPoints(), 836 | map: map 837 | }); 838 | $('#initheatmap-button').prop('disabled', true); 839 | circle.setVisible(false); 840 | } 841 | 842 | $(function() { 843 | $( 'input[type="datetime"]').datetimepicker({dateFormat:"yy-mm-ddT", timeFormat:"HH:mm:ss"}); 844 | }); 845 | 846 | 847 | 848 | // This looks for planes to reap out of the master Planes variable 849 | function reaper() { 850 | PlanesToReap = 0; 851 | // When did the reaper start? 852 | reaptime = new Date().getTime(); 853 | // Loop the planes 854 | for (var reap in Planes) { 855 | // Is this plane possibly reapable? 856 | if (Planes[reap].reapable == true) { 857 | // Has it not been seen for 5 minutes? 858 | // This way we still have it if it returns before then 859 | // Due to loss of signal or other reasons 860 | if ((reaptime - Planes[reap].updated) > 300000) { 861 | // Reap it. 862 | delete Planes[reap]; 863 | } 864 | PlanesToReap++; 865 | } 866 | }; 867 | } 868 | 869 | // Refresh the detail window about the plane 870 | function refreshSelected() { 871 | var selected = false; 872 | if (typeof SelectedPlane !== 'undefined' && SelectedPlane != "ICAO" && SelectedPlane != null) { 873 | selected = Planes[SelectedPlane]; 874 | } 875 | 876 | var columns = 2; 877 | var html = ''; 878 | 879 | if (selected) { 880 | html += ''; 881 | } else { 882 | html += '
'; 883 | } 884 | 885 | // Flight header line including squawk if needed 886 | if (selected && selected.flight == "") { 887 | html += ''; 909 | 910 | if (selected) { 911 | if (Metric) { 912 | html += ''; 913 | } else { 914 | html += ''; 915 | } 916 | } else { 917 | html += ''; 918 | } 919 | 920 | if (selected && selected.squawk != '0000') { 921 | html += ''; 922 | } else { 923 | html += ''; 924 | } 925 | 926 | html += ''; 937 | 938 | if (selected) { 939 | html += ''; 940 | } else { 941 | html += ''; // Something is wrong if we are here 942 | } 943 | 944 | html += ''; 951 | 952 | html += ''; 955 | 956 | // Let's show some extra data if we have site coordinates 957 | if (SiteShow) { 958 | //var siteLatLon = new google.maps.LatLng(SiteLat, SiteLon); 959 | var planeLatLon = new google.maps.LatLng(selected.latitude, selected.longitude); 960 | var dist = google.maps.geometry.spherical.computeDistanceBetween (latLng, planeLatLon); 961 | 962 | if (Metric) { 963 | dist /= 1000; 964 | } else { 965 | dist /= 1852; 966 | } 967 | dist = (Math.round((dist)*10)/10).toFixed(1); 968 | html += ''; 970 | } // End of SiteShow 971 | } else { 972 | if (SiteShow) { 973 | html += ''; 975 | } else { 976 | html += 'n/a'; 977 | } 978 | } 979 | 980 | html += '
N/A (' + 888 | selected.icao + ')'; 889 | } else if (selected && selected.flight != "") { 890 | html += '
' + 891 | selected.flight + ''; 892 | } else { 893 | html += '
DUMP1090'; 894 | } 895 | 896 | if (selected && selected.squawk == 7500) { // Lets hope we never see this... Aircraft Hijacking 897 | html += '  Squawking: Aircraft Hijacking '; 898 | } else if (selected && selected.squawk == 7600) { // Radio Failure 899 | html += '  Squawking: Radio Failure '; 900 | } else if (selected && selected.squawk == 7700) { // General Emergency 901 | html += '  Squawking: General Emergency '; 902 | } else if (selected && selected.flight != '') { 903 | html += ' [FR24]'; 904 | html += ' [FlightStats]'; 906 | html += ' [FlightAware]'; 907 | } 908 | html += '
Altitude: ' + Math.round(selected.altitude / 3.2828) + ' m
Altitude: ' + selected.altitude + ' ft
Altitude: n/aSquawk: ' + selected.squawk + '
Squawk: n/a
Speed: ' 927 | if (selected) { 928 | if (Metric) { 929 | html += Math.round(selected.speed * 1.852) + ' km/h'; 930 | } else { 931 | html += selected.speed + ' kt'; 932 | } 933 | } else { 934 | html += 'n/a'; 935 | } 936 | html += 'ICAO (hex): ' + selected.icao + '
ICAO (hex): n/a
Track: ' 945 | if (selected && selected.vTrack) { 946 | html += selected.track + '°' + ' (' + normalizeTrack(selected.track, selected.vTrack)[1] +')'; 947 | } else { 948 | html += 'n/a'; 949 | } 950 | html += ' 
Lat/Long: '; 953 | if (selected && selected.vPosition) { 954 | html += selected.latitude + ', ' + selected.longitude + '
Distance from Site: ' + dist + 969 | (Metric ? ' km' : ' NM') + '
Distance from Site: n/a ' + 974 | (Metric ? ' km' : ' NM') + '
'; 981 | 982 | document.getElementById('plane_detail').innerHTML = html; 983 | } 984 | 985 | // Right now we have no means to validate the speed is good 986 | // Want to return (n/a) when we dont have it 987 | // TODO: Edit C code to add a valid speed flag 988 | // TODO: Edit js code to use said flag 989 | function normalizeSpeed(speed, valid) { 990 | return speed 991 | } 992 | 993 | // Returns back a long string, short string, and the track if we have a vaild track path 994 | function normalizeTrack(track, valid){ 995 | x = [] 996 | if ((track > -1) && (track < 22.5)) { 997 | x = ["North", "N", track] 998 | } 999 | if ((track > 22.5) && (track < 67.5)) { 1000 | x = ["North East", "NE", track] 1001 | } 1002 | if ((track > 67.5) && (track < 112.5)) { 1003 | x = ["East", "E", track] 1004 | } 1005 | if ((track > 112.5) && (track < 157.5)) { 1006 | x = ["South East", "SE", track] 1007 | } 1008 | if ((track > 157.5) && (track < 202.5)) { 1009 | x = ["South", "S", track] 1010 | } 1011 | if ((track > 202.5) && (track < 247.5)) { 1012 | x = ["South West", "SW", track] 1013 | } 1014 | if ((track > 247.5) && (track < 292.5)) { 1015 | x = ["West", "W", track] 1016 | } 1017 | if ((track > 292.5) && (track < 337.5)) { 1018 | x = ["North West", "NW", track] 1019 | } 1020 | if ((track > 337.5) && (track < 361)) { 1021 | x = ["North", "N", track] 1022 | } 1023 | if (!valid) { 1024 | x = [" ", "n/a", ""] 1025 | } 1026 | return x 1027 | } 1028 | 1029 | // Refeshes the larger table of all the planes 1030 | function refreshTableInfo() { 1031 | var html = ''; 1032 | html += ''; 1033 | html += ''; 1034 | html += ''; 1035 | html += ''; 1037 | html += ''; 1039 | html += ''; 1041 | // Add distance column header to table if site coordinates are provided 1042 | if (SiteShow && (typeof SiteLat !== 'undefined' || typeof SiteLon !== 'undefined')) { 1043 | html += ''; 1045 | } 1046 | html += ''; 1048 | html += ''; 1050 | html += ''; 1052 | for (var tablep in Planes) { 1053 | var tableplane = Planes[tablep] 1054 | if (!tableplane.reapable) { 1055 | var specialStyle = ""; 1056 | // Is this the plane we selected? 1057 | if (tableplane.icao == SelectedPlane) { 1058 | specialStyle += " selected"; 1059 | } 1060 | // Lets hope we never see this... Aircraft Hijacking 1061 | if (tableplane.squawk == 7500) { 1062 | specialStyle += " squawk7500"; 1063 | } 1064 | // Radio Failure 1065 | if (tableplane.squawk == 7600) { 1066 | specialStyle += " squawk7600"; 1067 | } 1068 | // Emergancy 1069 | if (tableplane.squawk == 7700) { 1070 | specialStyle += " squawk7700"; 1071 | } 1072 | 1073 | if (tableplane.vPosition == true) { 1074 | html += ''; 1075 | } else { 1076 | html += ''; 1077 | } 1078 | 1079 | html += ''; 1080 | html += ''; 1081 | if (tableplane.squawk != '0000' ) { 1082 | html += ''; 1083 | } else { 1084 | html += ''; 1085 | } 1086 | 1087 | if (Metric) { 1088 | html += ''; 1089 | html += ''; 1090 | } else { 1091 | html += ''; 1092 | html += ''; 1093 | } 1094 | // Add distance column to table if site coordinates are provided 1095 | if (SiteShow && (typeof SiteLat !== 'undefined' || typeof SiteLon !== 'undefined')) { 1096 | html += ''; 1112 | } 1113 | 1114 | html += ''; 1122 | html += ''; 1123 | html += ''; 1124 | html += ''; 1125 | } 1126 | } 1127 | html += '
ICAOFlightSquawkAltitudeSpeedDistanceTrackMsgsSeen
' + tableplane.icao + '' + tableplane.flight + '' + tableplane.squawk + ' ' + Math.round(tableplane.altitude / 3.2828) + '' + Math.round(tableplane.speed * 1.852) + '' + tableplane.altitude + '' + tableplane.speed + ''; 1097 | if (tableplane.vPosition) { 1098 | //var siteLatLon = new google.maps.LatLng(SiteLat, SiteLon); 1099 | var planeLatLon = new google.maps.LatLng(tableplane.latitude, tableplane.longitude); 1100 | var dist = google.maps.geometry.spherical.computeDistanceBetween (latLng, planeLatLon); 1101 | if (Metric) { 1102 | dist /= 1000; 1103 | } else { 1104 | dist /= 1852; 1105 | } 1106 | dist = (Math.round((dist)*10)/10).toFixed(1); 1107 | html += dist; 1108 | } else { 1109 | html += '0'; 1110 | } 1111 | html += ''; 1115 | if (tableplane.vTrack) { 1116 | html += normalizeTrack(tableplane.track, tableplane.vTrack)[2]; 1117 | // html += ' (' + normalizeTrack(tableplane.track, tableplane.vTrack)[1] + ')'; 1118 | } else { 1119 | html += ' '; 1120 | } 1121 | html += '' + tableplane.messages + '' + tableplane.seen + '
'; 1128 | 1129 | document.getElementById('planes_table').innerHTML = html; 1130 | 1131 | if (SpecialSquawk) { 1132 | $('#SpecialSquawkWarning').css('display', 'inline'); 1133 | } else { 1134 | $('#SpecialSquawkWarning').css('display', 'none'); 1135 | } 1136 | 1137 | // Click event for table 1138 | $('#planes_table').find('tr').click( function(){ 1139 | var hex = $(this).find('td:first').text(); 1140 | if (hex != "ICAO") { 1141 | selectPlaneByHex(hex); 1142 | refreshTableInfo(); 1143 | refreshSelected(); 1144 | } 1145 | }); 1146 | 1147 | sortTable("tableinfo"); 1148 | } 1149 | 1150 | // Credit goes to a co-worker that needed a similar functions for something else 1151 | // we get a copy of it free ;) 1152 | function setASC_DESC(iCol) { 1153 | if(iSortCol==iCol) { 1154 | bSortASC=!bSortASC; 1155 | } else { 1156 | bSortASC=bDefaultSortASC; 1157 | } 1158 | } 1159 | 1160 | function sortTable(szTableID,iCol) { 1161 | //if iCol was not provided, and iSortCol is not set, assign default value 1162 | if (typeof iCol==='undefined'){ 1163 | if(iSortCol!=-1){ 1164 | var iCol=iSortCol; 1165 | } else if (SiteShow && (typeof SiteLat !== 'undefined' || typeof SiteLon !== 'undefined')) { 1166 | var iCol=5; 1167 | } else { 1168 | var iCol=iDefaultSortCol; 1169 | } 1170 | } 1171 | 1172 | //retrieve passed table element 1173 | var oTbl=document.getElementById(szTableID).tBodies[0]; 1174 | var aStore=[]; 1175 | 1176 | //If supplied col # is greater than the actual number of cols, set sel col = to last col 1177 | if (typeof oTbl.rows[0] !== 'undefined' && oTbl.rows[0].cells.length <= iCol) { 1178 | iCol=(oTbl.rows[0].cells.length-1); 1179 | } 1180 | 1181 | //store the col # 1182 | iSortCol=iCol; 1183 | 1184 | //determine if we are delaing with numerical, or alphanumeric content 1185 | var bNumeric = false; 1186 | if ((typeof oTbl.rows[0] !== 'undefined') && 1187 | (!isNaN(parseFloat(oTbl.rows[0].cells[iSortCol].textContent || 1188 | oTbl.rows[0].cells[iSortCol].innerText)))) { 1189 | bNumeric = true; 1190 | } 1191 | 1192 | //loop through the rows, storing each one inro aStore 1193 | for (var i=0,iLen=oTbl.rows.length;i 15) { 1309 | this.markerColor = StaleColor; 1310 | } 1311 | 1312 | // Plane marker 1313 | var baseSvg = { 1314 | planeData : "M 1.9565564,41.694305 C 1.7174505,40.497708 1.6419973,38.448747 " + 1315 | "1.8096508,37.70494 1.8936398,37.332056 2.0796653,36.88191 2.222907,36.70461 " + 1316 | "2.4497603,36.423844 4.087816,35.47248 14.917931,29.331528 l 12.434577," + 1317 | "-7.050718 -0.04295,-7.613412 c -0.03657,-6.4844888 -0.01164,-7.7625804 " + 1318 | "0.168134,-8.6194061 0.276129,-1.3160905 0.762276,-2.5869575 1.347875," + 1319 | "-3.5235502 l 0.472298,-0.7553719 1.083746,-0.6085497 c 1.194146,-0.67053522 " + 1320 | "1.399524,-0.71738842 2.146113,-0.48960552 1.077005,0.3285939 2.06344," + 1321 | "1.41299352 2.797602,3.07543322 0.462378,1.0469993 0.978731,2.7738408 " + 1322 | "1.047635,3.5036272 0.02421,0.2570284 0.06357,3.78334 0.08732,7.836246 0.02375," + 1323 | "4.052905 0.0658,7.409251 0.09345,7.458546 0.02764,0.04929 5.600384,3.561772 " + 1324 | "12.38386,7.805502 l 12.333598,7.715871 0.537584,0.959688 c 0.626485,1.118378 " + 1325 | "0.651686,1.311286 0.459287,3.516442 -0.175469,2.011604 -0.608966,2.863924 " + 1326 | "-1.590344,3.127136 -0.748529,0.200763 -1.293144,0.03637 -10.184829,-3.07436 " + 1327 | "C 48.007733,41.72562 44.793806,40.60197 43.35084,40.098045 l -2.623567," + 1328 | "-0.916227 -1.981212,-0.06614 c -1.089663,-0.03638 -1.985079,-0.05089 -1.989804," + 1329 | "-0.03225 -0.0052,0.01863 -0.02396,2.421278 -0.04267,5.339183 -0.0395,6.147742 " + 1330 | "-0.143635,7.215456 -0.862956,8.845475 l -0.300457,0.680872 2.91906,1.361455 " + 1331 | "c 2.929379,1.366269 3.714195,1.835385 4.04589,2.41841 0.368292,0.647353 " + 1332 | "0.594634,2.901439 0.395779,3.941627 -0.0705,0.368571 -0.106308,0.404853 " + 1333 | "-0.765159,0.773916 L 41.4545,62.83158 39.259237,62.80426 c -6.030106,-0.07507 " + 1334 | "-16.19508,-0.495041 -16.870991,-0.697033 -0.359409,-0.107405 -0.523792," + 1335 | "-0.227482 -0.741884,-0.541926 -0.250591,-0.361297 -0.28386,-0.522402 -0.315075," + 1336 | "-1.52589 -0.06327,-2.03378 0.23288,-3.033615 1.077963,-3.639283 0.307525," + 1337 | "-0.2204 4.818478,-2.133627 6.017853,-2.552345 0.247872,-0.08654 0.247455," + 1338 | "-0.102501 -0.01855,-0.711959 -0.330395,-0.756986 -0.708622,-2.221756 -0.832676," + 1339 | "-3.224748 -0.05031,-0.406952 -0.133825,-3.078805 -0.185533,-5.937448 -0.0517," + 1340 | "-2.858644 -0.145909,-5.208974 -0.209316,-5.222958 -0.06341,-0.01399 -0.974464," + 1341 | "-0.0493 -2.024551,-0.07845 L 23.247235,38.61921 18.831373,39.8906 C 4.9432155," + 1342 | "43.88916 4.2929558,44.057819 3.4954426,43.86823 2.7487826,43.690732 2.2007966," + 1343 | "42.916622 1.9565564,41.694305 z" 1344 | }; 1345 | 1346 | // If the squawk code is one of the international emergency codes, 1347 | // match the info window alert color. 1348 | if (this.squawk == 7500) { 1349 | this.markerColor = "rgb(255, 85, 85)"; 1350 | } 1351 | if (this.squawk == 7600) { 1352 | this.markerColor = "rgb(0, 255, 255)"; 1353 | } 1354 | if (this.squawk == 7700) { 1355 | this.markerColor = "rgb(255, 255, 0)"; 1356 | } 1357 | 1358 | // If we have not overwritten color by now, an extension still could but 1359 | // just keep on trucking. :) 1360 | 1361 | return { 1362 | strokeWeight: (this.is_selected ? 2 : 1), 1363 | path: "M 0,0 "+ baseSvg["planeData"], 1364 | scale: 0.4, 1365 | fillColor: this.markerColor, 1366 | fillOpacity: 0.9, 1367 | anchor: new google.maps.Point(32, 32), // Set anchor to middle of plane. 1368 | rotation: this.track 1369 | }; 1370 | }, 1371 | 1372 | // TODO: Trigger actions of a selecting a plane 1373 | funcSelectPlane : function(selectedPlane){ 1374 | selectPlaneByHex(this.icao); 1375 | }, 1376 | 1377 | // Update our data 1378 | funcUpdateData : function(data){ 1379 | // So we can find out if we moved 1380 | var oldlat = this.latitude; 1381 | var oldlon = this.longitude; 1382 | var oldalt = this.altitude; 1383 | 1384 | // Update all of our data 1385 | this.updated = new Date().getTime(); 1386 | this.altitude = data.altitude; 1387 | this.speed = data.speed; 1388 | this.track = data.track; 1389 | this.latitude = data.lat; 1390 | this.longitude = data.lon; 1391 | this.flight = data.flight; 1392 | this.squawk = data.squawk; 1393 | this.icao = data.hex; 1394 | this.messages = data.messages; 1395 | this.seen = data.seen; 1396 | 1397 | // If no packet in over 58 seconds, consider the plane reapable 1398 | // This way we can hold it, but not show it just in case the plane comes back 1399 | if (this.seen > 58) { 1400 | this.reapable = true; 1401 | if (this.marker) { 1402 | this.marker.setMap(null); 1403 | this.marker = null; 1404 | } 1405 | if (this.line) { 1406 | this.line.setMap(null); 1407 | this.line = null; 1408 | } 1409 | if (SelectedPlane == this.icao) { 1410 | if (this.is_selected) { 1411 | this.is_selected = false; 1412 | } 1413 | SelectedPlane = null; 1414 | } 1415 | } else { 1416 | if (this.reapable == true) { 1417 | } 1418 | this.reapable = false; 1419 | } 1420 | 1421 | // Is the position valid? 1422 | if ((data.validposition == 1) && (this.reapable == false)) { 1423 | this.vPosition = true; 1424 | 1425 | // Detech if the plane has moved 1426 | changeLat = false; 1427 | changeLon = false; 1428 | changeAlt = false; 1429 | if (oldlat != this.latitude) { 1430 | changeLat = true; 1431 | } 1432 | if (oldlon != this.longitude) { 1433 | changeLon = true; 1434 | } 1435 | if (oldalt != this.altitude) { 1436 | changeAlt = true; 1437 | } 1438 | // Right now we only care about lat/long, if alt is updated only, oh well 1439 | if ((changeLat == true) || (changeLon == true)) { 1440 | this.funcAddToTrack(); 1441 | if (this.is_selected) { 1442 | this.line = this.funcUpdateLines(); 1443 | } 1444 | } 1445 | this.marker = this.funcUpdateMarker(); 1446 | PlanesOnMap++; 1447 | } else { 1448 | this.vPosition = false; 1449 | } 1450 | 1451 | // Do we have a valid track for the plane? 1452 | if (data.validtrack == 1) 1453 | this.vTrack = true; 1454 | else 1455 | this.vTrack = false; 1456 | }, 1457 | 1458 | // Update our marker on the map 1459 | funcUpdateMarker: function() { 1460 | if (this.marker) { 1461 | this.marker.setPosition(new google.maps.LatLng(this.latitude, this.longitude)); 1462 | this.marker.setIcon(this.funcGetIcon()); 1463 | } else { 1464 | this.marker = new google.maps.Marker({ 1465 | position: new google.maps.LatLng(this.latitude, this.longitude), 1466 | map: map, 1467 | icon: this.funcGetIcon(), 1468 | visable: true 1469 | }); 1470 | 1471 | // This is so we can match icao address 1472 | this.marker.icao = this.icao; 1473 | 1474 | // Trap clicks for this marker. 1475 | google.maps.event.addListener(this.marker, 'click', this.funcSelectPlane); 1476 | } 1477 | 1478 | // Setting the marker title 1479 | if (this.flight.length == 0) { 1480 | this.marker.setTitle(this.hex); 1481 | } else { 1482 | this.marker.setTitle(this.flight+' ('+this.icao+')'); 1483 | } 1484 | return this.marker; 1485 | }, 1486 | 1487 | // Update our planes tail line, 1488 | // TODO: Make this multi colored based on options 1489 | // altitude (default) or speed 1490 | funcUpdateLines: function() { 1491 | if (this.line) { 1492 | var path = this.line.getPath(); 1493 | path.push(new google.maps.LatLng(this.latitude, this.longitude)); 1494 | } else { 1495 | this.line = new google.maps.Polyline({ 1496 | strokeColor: '#000000', 1497 | strokeOpacity: 1.0, 1498 | strokeWeight: 3, 1499 | map: map, 1500 | path: this.trackline 1501 | }); 1502 | } 1503 | return this.line; 1504 | } 1505 | }; 1506 | -------------------------------------------------------------------------------- /www/gmap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | DUMP1090 22 | 23 | 24 | 27 |
28 |
29 |
30 | 62 |
63 | Squak 7x00 is reported and shown.
64 | This is most likely an error in reciving or decoding.
65 | Please do not call the local authorities, they already know about it if it is valid squak. 66 |
67 |
68 | 69 | 70 | -------------------------------------------------------------------------------- /www/img/focus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobozo/signal-logr/5228b0f6c50e7d698a4ed0bef006ea793c1e5ce0/www/img/focus.png -------------------------------------------------------------------------------- /www/img/gpio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobozo/signal-logr/5228b0f6c50e7d698a4ed0bef006ea793c1e5ce0/www/img/gpio.png -------------------------------------------------------------------------------- /www/img/gps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobozo/signal-logr/5228b0f6c50e7d698a4ed0bef006ea793c1e5ce0/www/img/gps.png -------------------------------------------------------------------------------- /www/img/restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobozo/signal-logr/5228b0f6c50e7d698a4ed0bef006ea793c1e5ce0/www/img/restart.png -------------------------------------------------------------------------------- /www/img/rtlsdr.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobozo/signal-logr/5228b0f6c50e7d698a4ed0bef006ea793c1e5ce0/www/img/rtlsdr.jpeg -------------------------------------------------------------------------------- /www/img/wifi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobozo/signal-logr/5228b0f6c50e7d698a4ed0bef006ea793c1e5ce0/www/img/wifi.png -------------------------------------------------------------------------------- /www/jquery-ui-timepicker-addon.js: -------------------------------------------------------------------------------- 1 | /*! jQuery Timepicker Addon - v1.6.3 - 2016-04-20 2 | * http://trentrichardson.com/examples/timepicker 3 | * Copyright (c) 2016 Trent Richardson; Licensed MIT */ 4 | (function (factory) { 5 | if (typeof define === 'function' && define.amd) { 6 | define(['jquery', 'jquery-ui'], factory); 7 | } else { 8 | factory(jQuery); 9 | } 10 | }(function ($) { 11 | 12 | /* 13 | * Lets not redefine timepicker, Prevent "Uncaught RangeError: Maximum call stack size exceeded" 14 | */ 15 | $.ui.timepicker = $.ui.timepicker || {}; 16 | if ($.ui.timepicker.version) { 17 | return; 18 | } 19 | 20 | /* 21 | * Extend jQueryUI, get it started with our version number 22 | */ 23 | $.extend($.ui, { 24 | timepicker: { 25 | version: "1.6.3" 26 | } 27 | }); 28 | 29 | /* 30 | * Timepicker manager. 31 | * Use the singleton instance of this class, $.timepicker, to interact with the time picker. 32 | * Settings for (groups of) time pickers are maintained in an instance object, 33 | * allowing multiple different settings on the same page. 34 | */ 35 | var Timepicker = function () { 36 | this.regional = []; // Available regional settings, indexed by language code 37 | this.regional[''] = { // Default regional settings 38 | currentText: 'Now', 39 | closeText: 'Done', 40 | amNames: ['AM', 'A'], 41 | pmNames: ['PM', 'P'], 42 | timeFormat: 'HH:mm', 43 | timeSuffix: '', 44 | timeOnlyTitle: 'Choose Time', 45 | timeText: 'Time', 46 | hourText: 'Hour', 47 | minuteText: 'Minute', 48 | secondText: 'Second', 49 | millisecText: 'Millisecond', 50 | microsecText: 'Microsecond', 51 | timezoneText: 'Time Zone', 52 | isRTL: false 53 | }; 54 | this._defaults = { // Global defaults for all the datetime picker instances 55 | showButtonPanel: true, 56 | timeOnly: false, 57 | timeOnlyShowDate: false, 58 | showHour: null, 59 | showMinute: null, 60 | showSecond: null, 61 | showMillisec: null, 62 | showMicrosec: null, 63 | showTimezone: null, 64 | showTime: true, 65 | stepHour: 1, 66 | stepMinute: 1, 67 | stepSecond: 1, 68 | stepMillisec: 1, 69 | stepMicrosec: 1, 70 | hour: 0, 71 | minute: 0, 72 | second: 0, 73 | millisec: 0, 74 | microsec: 0, 75 | timezone: null, 76 | hourMin: 0, 77 | minuteMin: 0, 78 | secondMin: 0, 79 | millisecMin: 0, 80 | microsecMin: 0, 81 | hourMax: 23, 82 | minuteMax: 59, 83 | secondMax: 59, 84 | millisecMax: 999, 85 | microsecMax: 999, 86 | minDateTime: null, 87 | maxDateTime: null, 88 | maxTime: null, 89 | minTime: null, 90 | onSelect: null, 91 | hourGrid: 0, 92 | minuteGrid: 0, 93 | secondGrid: 0, 94 | millisecGrid: 0, 95 | microsecGrid: 0, 96 | alwaysSetTime: true, 97 | separator: ' ', 98 | altFieldTimeOnly: true, 99 | altTimeFormat: null, 100 | altSeparator: null, 101 | altTimeSuffix: null, 102 | altRedirectFocus: true, 103 | pickerTimeFormat: null, 104 | pickerTimeSuffix: null, 105 | showTimepicker: true, 106 | timezoneList: null, 107 | addSliderAccess: false, 108 | sliderAccessArgs: null, 109 | controlType: 'slider', 110 | oneLine: false, 111 | defaultValue: null, 112 | parse: 'strict', 113 | afterInject: null 114 | }; 115 | $.extend(this._defaults, this.regional['']); 116 | }; 117 | 118 | $.extend(Timepicker.prototype, { 119 | $input: null, 120 | $altInput: null, 121 | $timeObj: null, 122 | inst: null, 123 | hour_slider: null, 124 | minute_slider: null, 125 | second_slider: null, 126 | millisec_slider: null, 127 | microsec_slider: null, 128 | timezone_select: null, 129 | maxTime: null, 130 | minTime: null, 131 | hour: 0, 132 | minute: 0, 133 | second: 0, 134 | millisec: 0, 135 | microsec: 0, 136 | timezone: null, 137 | hourMinOriginal: null, 138 | minuteMinOriginal: null, 139 | secondMinOriginal: null, 140 | millisecMinOriginal: null, 141 | microsecMinOriginal: null, 142 | hourMaxOriginal: null, 143 | minuteMaxOriginal: null, 144 | secondMaxOriginal: null, 145 | millisecMaxOriginal: null, 146 | microsecMaxOriginal: null, 147 | ampm: '', 148 | formattedDate: '', 149 | formattedTime: '', 150 | formattedDateTime: '', 151 | timezoneList: null, 152 | units: ['hour', 'minute', 'second', 'millisec', 'microsec'], 153 | support: {}, 154 | control: null, 155 | 156 | /* 157 | * Override the default settings for all instances of the time picker. 158 | * @param {Object} settings object - the new settings to use as defaults (anonymous object) 159 | * @return {Object} the manager object 160 | */ 161 | setDefaults: function (settings) { 162 | extendRemove(this._defaults, settings || {}); 163 | return this; 164 | }, 165 | 166 | /* 167 | * Create a new Timepicker instance 168 | */ 169 | _newInst: function ($input, opts) { 170 | var tp_inst = new Timepicker(), 171 | inlineSettings = {}, 172 | fns = {}, 173 | overrides, i; 174 | 175 | for (var attrName in this._defaults) { 176 | if (this._defaults.hasOwnProperty(attrName)) { 177 | var attrValue = $input.attr('time:' + attrName); 178 | if (attrValue) { 179 | try { 180 | inlineSettings[attrName] = eval(attrValue); 181 | } catch (err) { 182 | inlineSettings[attrName] = attrValue; 183 | } 184 | } 185 | } 186 | } 187 | 188 | overrides = { 189 | beforeShow: function (input, dp_inst) { 190 | if ($.isFunction(tp_inst._defaults.evnts.beforeShow)) { 191 | return tp_inst._defaults.evnts.beforeShow.call($input[0], input, dp_inst, tp_inst); 192 | } 193 | }, 194 | onChangeMonthYear: function (year, month, dp_inst) { 195 | // Update the time as well : this prevents the time from disappearing from the $input field. 196 | // tp_inst._updateDateTime(dp_inst); 197 | if ($.isFunction(tp_inst._defaults.evnts.onChangeMonthYear)) { 198 | tp_inst._defaults.evnts.onChangeMonthYear.call($input[0], year, month, dp_inst, tp_inst); 199 | } 200 | }, 201 | onClose: function (dateText, dp_inst) { 202 | if (tp_inst.timeDefined === true && $input.val() !== '') { 203 | tp_inst._updateDateTime(dp_inst); 204 | } 205 | if ($.isFunction(tp_inst._defaults.evnts.onClose)) { 206 | tp_inst._defaults.evnts.onClose.call($input[0], dateText, dp_inst, tp_inst); 207 | } 208 | } 209 | }; 210 | for (i in overrides) { 211 | if (overrides.hasOwnProperty(i)) { 212 | fns[i] = opts[i] || this._defaults[i] || null; 213 | } 214 | } 215 | 216 | tp_inst._defaults = $.extend({}, this._defaults, inlineSettings, opts, overrides, { 217 | evnts: fns, 218 | timepicker: tp_inst // add timepicker as a property of datepicker: $.datepicker._get(dp_inst, 'timepicker'); 219 | }); 220 | tp_inst.amNames = $.map(tp_inst._defaults.amNames, function (val) { 221 | return val.toUpperCase(); 222 | }); 223 | tp_inst.pmNames = $.map(tp_inst._defaults.pmNames, function (val) { 224 | return val.toUpperCase(); 225 | }); 226 | 227 | // detect which units are supported 228 | tp_inst.support = detectSupport( 229 | tp_inst._defaults.timeFormat + 230 | (tp_inst._defaults.pickerTimeFormat ? tp_inst._defaults.pickerTimeFormat : '') + 231 | (tp_inst._defaults.altTimeFormat ? tp_inst._defaults.altTimeFormat : '')); 232 | 233 | // controlType is string - key to our this._controls 234 | if (typeof(tp_inst._defaults.controlType) === 'string') { 235 | if (tp_inst._defaults.controlType === 'slider' && typeof($.ui.slider) === 'undefined') { 236 | tp_inst._defaults.controlType = 'select'; 237 | } 238 | tp_inst.control = tp_inst._controls[tp_inst._defaults.controlType]; 239 | } 240 | // controlType is an object and must implement create, options, value methods 241 | else { 242 | tp_inst.control = tp_inst._defaults.controlType; 243 | } 244 | 245 | // prep the timezone options 246 | var timezoneList = [-720, -660, -600, -570, -540, -480, -420, -360, -300, -270, -240, -210, -180, -120, -60, 247 | 0, 60, 120, 180, 210, 240, 270, 300, 330, 345, 360, 390, 420, 480, 525, 540, 570, 600, 630, 660, 690, 720, 765, 780, 840]; 248 | if (tp_inst._defaults.timezoneList !== null) { 249 | timezoneList = tp_inst._defaults.timezoneList; 250 | } 251 | var tzl = timezoneList.length, tzi = 0, tzv = null; 252 | if (tzl > 0 && typeof timezoneList[0] !== 'object') { 253 | for (; tzi < tzl; tzi++) { 254 | tzv = timezoneList[tzi]; 255 | timezoneList[tzi] = { value: tzv, label: $.timepicker.timezoneOffsetString(tzv, tp_inst.support.iso8601) }; 256 | } 257 | } 258 | tp_inst._defaults.timezoneList = timezoneList; 259 | 260 | // set the default units 261 | tp_inst.timezone = tp_inst._defaults.timezone !== null ? $.timepicker.timezoneOffsetNumber(tp_inst._defaults.timezone) : 262 | ((new Date()).getTimezoneOffset() * -1); 263 | tp_inst.hour = tp_inst._defaults.hour < tp_inst._defaults.hourMin ? tp_inst._defaults.hourMin : 264 | tp_inst._defaults.hour > tp_inst._defaults.hourMax ? tp_inst._defaults.hourMax : tp_inst._defaults.hour; 265 | tp_inst.minute = tp_inst._defaults.minute < tp_inst._defaults.minuteMin ? tp_inst._defaults.minuteMin : 266 | tp_inst._defaults.minute > tp_inst._defaults.minuteMax ? tp_inst._defaults.minuteMax : tp_inst._defaults.minute; 267 | tp_inst.second = tp_inst._defaults.second < tp_inst._defaults.secondMin ? tp_inst._defaults.secondMin : 268 | tp_inst._defaults.second > tp_inst._defaults.secondMax ? tp_inst._defaults.secondMax : tp_inst._defaults.second; 269 | tp_inst.millisec = tp_inst._defaults.millisec < tp_inst._defaults.millisecMin ? tp_inst._defaults.millisecMin : 270 | tp_inst._defaults.millisec > tp_inst._defaults.millisecMax ? tp_inst._defaults.millisecMax : tp_inst._defaults.millisec; 271 | tp_inst.microsec = tp_inst._defaults.microsec < tp_inst._defaults.microsecMin ? tp_inst._defaults.microsecMin : 272 | tp_inst._defaults.microsec > tp_inst._defaults.microsecMax ? tp_inst._defaults.microsecMax : tp_inst._defaults.microsec; 273 | tp_inst.ampm = ''; 274 | tp_inst.$input = $input; 275 | 276 | if (tp_inst._defaults.altField) { 277 | tp_inst.$altInput = $(tp_inst._defaults.altField); 278 | if (tp_inst._defaults.altRedirectFocus === true) { 279 | tp_inst.$altInput.css({ 280 | cursor: 'pointer' 281 | }).focus(function () { 282 | $input.trigger("focus"); 283 | }); 284 | } 285 | } 286 | 287 | if (tp_inst._defaults.minDate === 0 || tp_inst._defaults.minDateTime === 0) { 288 | tp_inst._defaults.minDate = new Date(); 289 | } 290 | if (tp_inst._defaults.maxDate === 0 || tp_inst._defaults.maxDateTime === 0) { 291 | tp_inst._defaults.maxDate = new Date(); 292 | } 293 | 294 | // datepicker needs minDate/maxDate, timepicker needs minDateTime/maxDateTime.. 295 | if (tp_inst._defaults.minDate !== undefined && tp_inst._defaults.minDate instanceof Date) { 296 | tp_inst._defaults.minDateTime = new Date(tp_inst._defaults.minDate.getTime()); 297 | } 298 | if (tp_inst._defaults.minDateTime !== undefined && tp_inst._defaults.minDateTime instanceof Date) { 299 | tp_inst._defaults.minDate = new Date(tp_inst._defaults.minDateTime.getTime()); 300 | } 301 | if (tp_inst._defaults.maxDate !== undefined && tp_inst._defaults.maxDate instanceof Date) { 302 | tp_inst._defaults.maxDateTime = new Date(tp_inst._defaults.maxDate.getTime()); 303 | } 304 | if (tp_inst._defaults.maxDateTime !== undefined && tp_inst._defaults.maxDateTime instanceof Date) { 305 | tp_inst._defaults.maxDate = new Date(tp_inst._defaults.maxDateTime.getTime()); 306 | } 307 | tp_inst.$input.bind('focus', function () { 308 | tp_inst._onFocus(); 309 | }); 310 | 311 | return tp_inst; 312 | }, 313 | 314 | /* 315 | * add our sliders to the calendar 316 | */ 317 | _addTimePicker: function (dp_inst) { 318 | var currDT = $.trim((this.$altInput && this._defaults.altFieldTimeOnly) ? this.$input.val() + ' ' + this.$altInput.val() : this.$input.val()); 319 | 320 | this.timeDefined = this._parseTime(currDT); 321 | this._limitMinMaxDateTime(dp_inst, false); 322 | this._injectTimePicker(); 323 | this._afterInject(); 324 | }, 325 | 326 | /* 327 | * parse the time string from input value or _setTime 328 | */ 329 | _parseTime: function (timeString, withDate) { 330 | if (!this.inst) { 331 | this.inst = $.datepicker._getInst(this.$input[0]); 332 | } 333 | 334 | if (withDate || !this._defaults.timeOnly) { 335 | var dp_dateFormat = $.datepicker._get(this.inst, 'dateFormat'); 336 | try { 337 | var parseRes = parseDateTimeInternal(dp_dateFormat, this._defaults.timeFormat, timeString, $.datepicker._getFormatConfig(this.inst), this._defaults); 338 | if (!parseRes.timeObj) { 339 | return false; 340 | } 341 | $.extend(this, parseRes.timeObj); 342 | } catch (err) { 343 | $.timepicker.log("Error parsing the date/time string: " + err + 344 | "\ndate/time string = " + timeString + 345 | "\ntimeFormat = " + this._defaults.timeFormat + 346 | "\ndateFormat = " + dp_dateFormat); 347 | return false; 348 | } 349 | return true; 350 | } else { 351 | var timeObj = $.datepicker.parseTime(this._defaults.timeFormat, timeString, this._defaults); 352 | if (!timeObj) { 353 | return false; 354 | } 355 | $.extend(this, timeObj); 356 | return true; 357 | } 358 | }, 359 | 360 | /* 361 | * Handle callback option after injecting timepicker 362 | */ 363 | _afterInject: function() { 364 | var o = this.inst.settings; 365 | if ($.isFunction(o.afterInject)) { 366 | o.afterInject.call(this); 367 | } 368 | }, 369 | 370 | /* 371 | * generate and inject html for timepicker into ui datepicker 372 | */ 373 | _injectTimePicker: function () { 374 | var $dp = this.inst.dpDiv, 375 | o = this.inst.settings, 376 | tp_inst = this, 377 | litem = '', 378 | uitem = '', 379 | show = null, 380 | max = {}, 381 | gridSize = {}, 382 | size = null, 383 | i = 0, 384 | l = 0; 385 | 386 | // Prevent displaying twice 387 | if ($dp.find("div.ui-timepicker-div").length === 0 && o.showTimepicker) { 388 | var noDisplay = ' ui_tpicker_unit_hide', 389 | html = '
' + '
' + o.timeText + '
' + 390 | '
'; 391 | 392 | // Create the markup 393 | for (i = 0, l = this.units.length; i < l; i++) { 394 | litem = this.units[i]; 395 | uitem = litem.substr(0, 1).toUpperCase() + litem.substr(1); 396 | show = o['show' + uitem] !== null ? o['show' + uitem] : this.support[litem]; 397 | 398 | // Added by Peter Medeiros: 399 | // - Figure out what the hour/minute/second max should be based on the step values. 400 | // - Example: if stepMinute is 15, then minMax is 45. 401 | max[litem] = parseInt((o[litem + 'Max'] - ((o[litem + 'Max'] - o[litem + 'Min']) % o['step' + uitem])), 10); 402 | gridSize[litem] = 0; 403 | 404 | html += '
' + o[litem + 'Text'] + '
' + 405 | '
'; 406 | 407 | if (show && o[litem + 'Grid'] > 0) { 408 | html += '
'; 409 | 410 | if (litem === 'hour') { 411 | for (var h = o[litem + 'Min']; h <= max[litem]; h += parseInt(o[litem + 'Grid'], 10)) { 412 | gridSize[litem]++; 413 | var tmph = $.datepicker.formatTime(this.support.ampm ? 'hht' : 'HH', {hour: h}, o); 414 | html += ''; 415 | } 416 | } 417 | else { 418 | for (var m = o[litem + 'Min']; m <= max[litem]; m += parseInt(o[litem + 'Grid'], 10)) { 419 | gridSize[litem]++; 420 | html += ''; 421 | } 422 | } 423 | 424 | html += '
' + tmph + '' + ((m < 10) ? '0' : '') + m + '
'; 425 | } 426 | html += '
'; 427 | } 428 | 429 | // Timezone 430 | var showTz = o.showTimezone !== null ? o.showTimezone : this.support.timezone; 431 | html += '
' + o.timezoneText + '
'; 432 | html += '
'; 433 | 434 | // Create the elements from string 435 | html += '
'; 436 | var $tp = $(html); 437 | 438 | // if we only want time picker... 439 | if (o.timeOnly === true) { 440 | $tp.prepend('
' + '
' + o.timeOnlyTitle + '
' + '
'); 441 | $dp.find('.ui-datepicker-header, .ui-datepicker-calendar').hide(); 442 | } 443 | 444 | // add sliders, adjust grids, add events 445 | for (i = 0, l = tp_inst.units.length; i < l; i++) { 446 | litem = tp_inst.units[i]; 447 | uitem = litem.substr(0, 1).toUpperCase() + litem.substr(1); 448 | show = o['show' + uitem] !== null ? o['show' + uitem] : this.support[litem]; 449 | 450 | // add the slider 451 | tp_inst[litem + '_slider'] = tp_inst.control.create(tp_inst, $tp.find('.ui_tpicker_' + litem + '_slider'), litem, tp_inst[litem], o[litem + 'Min'], max[litem], o['step' + uitem]); 452 | 453 | // adjust the grid and add click event 454 | if (show && o[litem + 'Grid'] > 0) { 455 | size = 100 * gridSize[litem] * o[litem + 'Grid'] / (max[litem] - o[litem + 'Min']); 456 | $tp.find('.ui_tpicker_' + litem + ' table').css({ 457 | width: size + "%", 458 | marginLeft: o.isRTL ? '0' : ((size / (-2 * gridSize[litem])) + "%"), 459 | marginRight: o.isRTL ? ((size / (-2 * gridSize[litem])) + "%") : '0', 460 | borderCollapse: 'collapse' 461 | }).find("td").click(function (e) { 462 | var $t = $(this), 463 | h = $t.html(), 464 | n = parseInt(h.replace(/[^0-9]/g), 10), 465 | ap = h.replace(/[^apm]/ig), 466 | f = $t.data('for'); // loses scope, so we use data-for 467 | 468 | if (f === 'hour') { 469 | if (ap.indexOf('p') !== -1 && n < 12) { 470 | n += 12; 471 | } 472 | else { 473 | if (ap.indexOf('a') !== -1 && n === 12) { 474 | n = 0; 475 | } 476 | } 477 | } 478 | 479 | tp_inst.control.value(tp_inst, tp_inst[f + '_slider'], litem, n); 480 | 481 | tp_inst._onTimeChange(); 482 | tp_inst._onSelectHandler(); 483 | }).css({ 484 | cursor: 'pointer', 485 | width: (100 / gridSize[litem]) + '%', 486 | textAlign: 'center', 487 | overflow: 'hidden' 488 | }); 489 | } // end if grid > 0 490 | } // end for loop 491 | 492 | // Add timezone options 493 | this.timezone_select = $tp.find('.ui_tpicker_timezone').append('').find("select"); 494 | $.fn.append.apply(this.timezone_select, 495 | $.map(o.timezoneList, function (val, idx) { 496 | return $("