├── .gitattributes ├── .gitignore ├── LICENSE ├── README.NOW ├── README.md ├── index.js ├── package.json ├── public └── index.html └── screenshot.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Luke Berndt 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. -------------------------------------------------------------------------------- /README.NOW: -------------------------------------------------------------------------------- 1 | 2 | 1. Configure the "statusServer" entry in your trunk-recorder config file. i.e.: 3 | 4 | "statusServer": "ws://xxx.xxx.xxx.xxx:3010/server" 5 | 6 | where xxx... is the IP address of the system where this server is to be run. 7 | 8 | 2. `node.js` and `npm` must be installed on the system where this server is to be run. 9 | 3. From within this directory, run `npm install` to download required modules 10 | 4. Launch the server with `node index.js` 11 | 5. From a web browser, connect to the interface at: 12 | 13 | http://xxx.xxx.xxx.xxx:3010 14 | 15 | where xxx... is the IP address of the system where this package is running. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # trunk-recorder-status-server 2 | Lets you monitor the status of recordings 3 | ## Install 4 | 1) Make sure you have `node` and `npm` installed. 5 | 2) clone the repository 6 | 3) `cd trunk-recorder-status-server` 7 | 4) `npm install` 8 | 9 | ## Trunk-Recorder config 10 | You need to add `statusServer` to your `config.json` file. 11 | ```json 12 | { 13 | "ver": 2, 14 | "sources": [{ 15 | }], 16 | "systems": [{ 17 | }], 18 | "broadcastifyCallsServer": "abc123", 19 | ... 20 | "statusServer": "ws://{ip-address}:3010/server" 21 | } 22 | ``` 23 | `ws://` indicates a websocket. You will need to replace ip-address of your status-server. It could be `localhost` or the IP address of the machine. 24 | 25 | ## Running 26 | To run the webserver, change into the directory and run the node app. 27 | ``` 28 | node index.js 29 | ``` 30 | 31 | ### Running in background 32 | To run in the background, use `tmux` 33 | ``` 34 | tmux 35 | 36 | cd {trunk-recorder directory} 37 | node index.js 38 | 39 | # control+b 40 | # d #this disconnect tmux and puts you back into you session 41 | ``` 42 | To get back into that tmux session run `tmux list-sessions` to get a list of your sessions. Then you can run `tmux attach-session {session number}` to get back into your session. 43 |

44 | 45 | # Using 46 | To use the webui, go to `http://{ip-address}:3010`. 47 |

48 | ![Screenshot](screenshot.png) 49 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var util = require("util"); 3 | 4 | var app = express(), 5 | http = require('http'), 6 | server = http.createServer(app); 7 | 8 | const WebSocket = require('ws'); 9 | const clientWss = new WebSocket.Server({ 10 | noServer: true 11 | }); 12 | const serverWss = new WebSocket.Server({ 13 | noServer: true 14 | }); 15 | 16 | const url = require('url'); 17 | 18 | var csv = require('csv'); 19 | var fs = require('fs'); 20 | var path = require('path'); 21 | 22 | var router = express.Router(); 23 | 24 | var bodyParser = require('body-parser'); 25 | 26 | var channels = {}; 27 | var clients = []; 28 | var srv = null; 29 | var rec = null; 30 | var stats = {}; 31 | 32 | server.on('upgrade', (request, socket, head) => { 33 | const pathname = url.parse(request.url).pathname; 34 | 35 | if (pathname === '/client') { 36 | console.log("Upgrading Client Connection"); 37 | clientWss.handleUpgrade(request, socket, head, (ws) => { 38 | clientWss.emit('connection', ws); 39 | }); 40 | } else if (pathname === '/server') { 41 | console.log("Upgrading Server Connection"); 42 | serverWss.handleUpgrade(request, socket, head, (ws) => { 43 | serverWss.emit('connection', ws); 44 | }); 45 | } else { 46 | socket.destroy(); 47 | } 48 | }); 49 | 50 | app.use(bodyParser()); 51 | app.use(express.static(__dirname + '/public')); 52 | 53 | function get_clients(req, res) { 54 | if (req.params.shortName) { 55 | var short_name = req.params.shortName.toLowerCase(); 56 | } else { 57 | var short_name = null; 58 | } 59 | for (var i = 0; i < clients.length; i++) { 60 | var response = []; 61 | //console.log(util.inspect(clients[i].socket)); 62 | if (!short_name || (clients[i].shortName == short_name)) { 63 | var age = (Date.now() - clients[i].timestamp) / 1000; 64 | var obj = { 65 | shortName: clients[i].shortName, 66 | filterCode: clients[i].filterCode, 67 | filterName: clients[i].filterName, 68 | filterType: clients[i].filterType, 69 | talkgroupNums: clients[i].talkgroupNums, 70 | type: clients[i].type, 71 | timestamp: age 72 | } 73 | response.push(obj); 74 | } 75 | 76 | } 77 | res.contentType('json'); 78 | res.send(JSON.stringify(response)); 79 | 80 | } 81 | app.get('/', function(req, res) { 82 | res.sendFile(path.join(__dirname + '/public/index.html')); 83 | }); 84 | 85 | 86 | app.get('/:shortName/clients', get_clients); 87 | app.get('/clients', get_clients); 88 | 89 | function notify_clients(call) { 90 | call.type = "calls"; 91 | var sent = 0; 92 | 93 | for (var i = 0; i < clients.length; i++) { 94 | //console.log(util.inspect(clients[i].socket)); 95 | if (clients[i].shortName == call.shortName.toLowerCase()) { 96 | if (clients[i].filterCode == "") { 97 | //console.log("Call TG # is set to All"); 98 | sent++; 99 | clients[i].socket.send(JSON.stringify(call)); 100 | } else if (clients[i].filterType == "unit") { 101 | var codeArray = clients[i].filterCode.split(','); 102 | var success = false; 103 | for (var j = 0; j < codeArray.length; ++j) { 104 | for (var k = 0; k < call.srcList.length; k++) { 105 | if (codeArray[j] == call.srcList[k]) { 106 | sent++; 107 | clients[i].socket.send(JSON.stringify(call)); 108 | success = true; 109 | break; 110 | } 111 | } 112 | if (success) { 113 | break; 114 | } 115 | } 116 | 117 | 118 | } else { 119 | var codeArray = clients[i].talkgroupNums; 120 | //console.log("Group Client: " + i + "\tCodes: " + codeArray + "\tTalkgroupNum: " + call.talkgroupNum); 121 | for (var j = 0; j < codeArray.length; ++j) { 122 | if (codeArray[j] == call.talkgroupNum) { 123 | console.log("[ " + i + " ] - Sending one filtered call"); 124 | clients[i].socket.send(JSON.stringify(call)); 125 | sent++ 126 | break; 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | if (sent > 0) { 134 | console.log("Sent calls to " + sent + " clients, System: " + call.shortName.toLowerCase()); 135 | } 136 | } 137 | 138 | function heartbeat() { 139 | this.isAlive = true; 140 | } 141 | 142 | clientWss.on('connection', function connection(ws, req) { 143 | var client = { 144 | socket: ws 145 | }; 146 | clients.push(client); 147 | 148 | ws.isAlive = true; 149 | ws.on('pong', heartbeat); 150 | console.log((new Date()) + ' WebSocket Connection accepted.'); 151 | ws.on('message', function incoming(message) { 152 | console.log("Got message: " + message); 153 | try { 154 | var data = JSON.parse(message); 155 | if (typeof data.type !== "undefined") { 156 | if (data.type == 'add') { 157 | 158 | var client = { 159 | socket: ws, 160 | code: null 161 | }; 162 | clients.push(client); 163 | console.log("[ " + data.type + " ] Client added"); 164 | if (srv){ 165 | console.log("Sending Srv config: " + srv.config); 166 | ws.send(JSON.stringify(srv.config)); 167 | console.log("Sent"); 168 | } 169 | if (rec){ 170 | console.log("Sending Recorders config: " + rec.config); 171 | ws.send(JSON.stringify(rec.config)); 172 | console.log("Sent"); 173 | } 174 | } 175 | var index = clients.indexOf(client); 176 | if (index != -1) { 177 | 178 | clients[index].timestamp = new Date(); 179 | console.log("[ " + data.type + " ] Client updated: " + index); 180 | } else { 181 | console.log("Error - WebSocket: Client not Found!"); 182 | } 183 | 184 | } 185 | 186 | } catch (err) { 187 | console.log("JSON PArsing Error: " + err); 188 | } 189 | console.log('Received Message: ' + message); 190 | }); 191 | ws.on('close', function(reasonCode, description) { 192 | console.log((new Date()) + ' Client ' + connection.remoteAddress + ' disconnected.'); 193 | console.log("code: " + reasonCode + " description: " + description); 194 | for (var i = 0; i < clients.length; i++) { 195 | // # Remove from our connections list so we don't send 196 | // # to a dead socket 197 | if (clients[i].socket == ws) { 198 | clients.splice(i); 199 | break; 200 | } 201 | } 202 | }); 203 | 204 | }); 205 | 206 | serverWss.on('connection', function connection(ws, req) { 207 | 208 | 209 | ws.isAlive = true; 210 | ws.on('pong', heartbeat); 211 | console.log((new Date()) + ' WebSocket Connection accepted.'); 212 | ws.on('message', function incoming(message) { 213 | try { var data = JSON.parse(message); 214 | if (typeof data.type !== "undefined") { 215 | 216 | if (data.type == 'config') { 217 | 218 | srv = { 219 | socket: ws, 220 | config: data, 221 | timestamp: new Date() 222 | }; 223 | 224 | console.log("[ " + data.type + " ] Server Live - config rcv'd"); 225 | 226 | } else if (data.type == 'calls_active') { 227 | console.log("[ " + data.type + " ] Server - calls_active message "); 228 | } else if (data.type == 'rates') { 229 | console.log("[ " + data.type + " ] Server - rates message "); 230 | } else if (data.type == 'recorders') { 231 | 232 | rec = { 233 | socket: ws, 234 | config: data, 235 | timestamp: new Date() 236 | }; 237 | 238 | console.log("[ " + data.type + " ] Server - recorders message "); 239 | } else if (data.type == 'recorder') { 240 | console.log("[ " + data.type + " ] Server - recorder message "); 241 | } else { 242 | console.log("[ " + data.type + " ] Server - Unknown message type"); 243 | } 244 | } else { 245 | console.log("Server - Message type not defined"); 246 | } 247 | clientWss.clients.forEach(function each(client) { 248 | if (client.readyState === WebSocket.OPEN) { 249 | client.send(message); 250 | } 251 | }); 252 | } catch (err) { 253 | console.log("JSON Parsing Error: " + err); 254 | } 255 | console.log('Received Message: ' + message); 256 | }); 257 | ws.on('close', function(reasonCode, description) { 258 | console.log((new Date()) + ' Server ' + connection.remoteAddress + ' disconnected.'); 259 | console.log("code: " + reasonCode + " description: " + description); 260 | srv = null; 261 | }); 262 | 263 | }); 264 | 265 | 266 | server.listen(3010, function() { 267 | console.log('Web interface is available at: ' + server.address().port + '...'); 268 | console.log('status socket address is probably: ws://localhost:' + server.address().port + '/server'); 269 | console.log(process.env) 270 | }); 271 | 272 | 273 | module.exports = server; 274 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socket-server", 3 | "version": "1.0.0", 4 | "description": "Sample for working with the Status Websocket with trunk-recorder", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Luke Berndt", 10 | "license": "ISC", 11 | "dependencies": { 12 | "body-parser": "1.17.2", 13 | "csv": "1.1.1", 14 | "express": "4.15.4", 15 | "ws": "3.1.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | Recorders 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
IDTypeSrcNumRecNumCountDurationStateErrorSpike
29 |
30 | 31 |
32 | Active Calls 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
ShortnameSysNumFreqTGTG TagLen (s)ConvP25P2Emergency
49 |
50 | 51 |
52 | SysNum: Decode rate/s 53 |
54 |
55 |
56 |
57 |
58 | 59 | 60 | 287 | 288 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrunkRecorder/trunk-recorder-status-server/034a6c4057111725ef6e1a856b978f9005dc2ec7/screenshot.png --------------------------------------------------------------------------------