Skip to content

Instantly share code, notes, and snippets.

@kralo
Created May 11, 2022 07:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kralo/920a13de9c34594bb3c8657b052a8ef0 to your computer and use it in GitHub Desktop.
Save kralo/920a13de9c34594bb3c8657b052a8ef0 to your computer and use it in GitHub Desktop.
Live Callmonitor for Tetracontrol - Tetra Funkgespräche mit Details (in kleinem always-on-top Windows Fenster anzeigen geht mit Electron)

Live Tetra (Group-)Callmonitor für Tetracontrol

screenshot2

Hiermit lassen sich aktive Gespräche, die von TetraControl erfasst werden, live über Webtechnologien anzeigen.

Sehr hilfreich bei mehreren Funkkreisen und Arbeitsplätzen mit viel Bildschirmplatz, die ein kleines weiteres Fenster anzeigen können.

Es werden bis zu 4 Funkkreise unterstütz, mit Klick auf den blauen Text lassen die sich einfach verstecken/anzeigen.

Installation

Die einzelnen Dateien in den Ordner c:\Programme(x86)\TetraControl\html entpacken. index_ muss nach html/, app_callmon.js muss nach html/js/ und live_callmon muss nach html/views/. Github Gist erlaubt keine Pfade vor den Dateien.

TetraControl Webserver einschalten, Aufruf über ://ip:port/index_callmon.html

Hier ist ein Beispiel für ein Electron-Fiddle, das solche kleinen Fenster erzeugt, die bei windows/linux immer ganz oben sind.

Lizenz

So nah wie möglich an "ich erhebe keine Lizenz". Javascript ist zum großen Teil von Tetracontrol kopiert. Wenn die Autoren es in ihr Programm zurück integrieren, freut mich. Und freue mich über Erwähnung...

/* SPDX-License-Identifier: Unlicense or whatever TetraControl is*/
// modified from TetraControl, possibly (c) there
/*
!!!!!
You might have to change this file's encoding to ANSI/ISO-8859-1
!!!!!
*/
var wsUri = "ws://" + window.location.hostname + ":" + window.location.port + "/live.json";
function getConfig() {
var seconds = localStorage.getItem('seconds');
if (seconds == null) {
return {
status: 86400,
gps: 86400,
age: 18600
};
} else {
seconds = JSON.parse(seconds || '');
return {
status: seconds.status,
gps: seconds.gps,
age: seconds.age
};
}
}
function ToJavaScriptDate(value) {
var pattern = /Date\(([^)]+)\)/;
var results = pattern.exec(value);
if (parseFloat(results[1]) < 0) {
return null;
}
var dt = new Date(parseFloat(results[1]));
//dt.setMonth(dt.getMonth() + 1);
return dt;
}
var app = angular.module('tetraCallmon', ['ngRoute']);
app.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/clive', {
templateUrl: 'views/live_callmon.html',
controller: 'TetraCallmonLive'
}).otherwise({
redirectTo: '/clive'
});
}]);
app.filter('bytypes', function() {
return function(datas, types) {
var items = {
types: types,
out: []
};
if (types === undefined) {
return;
}
angular.forEach(datas, function(value, key) {
if (this.types[value.type] === true) {
this.out.push(value);
}
}, items);
return items.out;
}
});
app.filter('bygeraet', function() {
return function(datas, radioName) {
var items = {
out: []
};
if (radioName === undefined) {
return;
}
angular.forEach(datas, function(value, key) {
if (value.radioName === radioName) {
this.out.push(value);
}
}, items);
return items.out;
}
});
app.controller('TetraCallmonLive', function($scope) {
$scope.typefilter = {
sds: false,
status: false,
call: true
};
$scope.datas = [];
$scope.radios = [{
radioName: "Gerät 1",
radioDesc: '1- Fuk1'
}, {
radioName: "Gerät 2",
radioDesc: '2- Fuk2'
}];
$scope.radios.push({
radioName: "Gerät 3",
radioDesc: '3- Fun3'
});
$scope.radios.push({
radioName: "Gerät 4",
radioDesc: '4- Fuk4'
});
var config = getConfig();
var websocket = new WebSocket(wsUri + "?MaxAlter=" + config.age);
websocket.addEventListener("open", function(event) {
onOpen(event);
});
websocket.addEventListener("close", function(event) {
onClose(event);
});
websocket.addEventListener("message", function(event) {
onMessage(event);
});
websocket.addEventListener("error", function(event) {
onError(event);
});
function onOpen(evt) {
console.log("Connected. (" + evt + ")");
}
function onClose(evt) {
console.log("Disconnected. (" + evt + ")");
}
function onError(evt) {
console.log("Error. (" + evt + ")");
}
function onMessage(evt) {
var data = JSON.parse(evt.data);
switch (data.type) {
case 'call':
parseCall(data);
break;
default:
console.error("Unexpected message recieved");
break;
}
}
function parseCall(data) {
data.timestamp = window.ToJavaScriptDate(data.tsStart);
data.end = window.ToJavaScriptDate(data.tsEnd);
if (data.end != null)
data.dur = data.end - data.timestamp;
switch (data.callType) {
default:
data.readableType = "Gespräch";
data.message = data.srcOPTA;
break;
}
$scope.$apply(function() {
for (var i = 0, len = $scope.datas.length; i < len; i++) {
var entry = $scope.datas[i];
if (entry.type == 'call' && entry.ID == data.ID) {
$scope.datas[i] = data;
return;
}
}
$scope.datas.unshift(data);
});
}
}
);
<!-- SPDX-License-Identifier: Unlicense -->
<html ng-app="tetraCallmon">
<head>
<meta charset="UTF-8">
<title>Callmonitor live</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular-route.min.js"></script>
<!-- JavaScript Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<style>
.grid-radios {
border: 0px solid black;
}
.grid-radio>div {
border-bottom: 2px solid grey;
}
.grid-radio {
display: grid;
grid-template-columns: minmax(max-content, 2fr) minmax(max-content, 17fr);
column-gap: 0%;
}
.speaking {
background-color: #fd7e14bd;
}
.radiodesc {
font-size: 80%;
}
.grid-calls {
display: grid;
grid-template-columns: minmax(max-content, 2.2fr) minmax(max-content, 2.3fr) minmax(max-content, 1fr) minmax(max-content, 4fr) minmax(max-content, 6fr);
column-gap: 2%;
font-family: monospace;
}
.timestamp,
.calldest {
font-size: 90%
}
.opta {
font-size: 90%
}
.opta,
.rufname {
text-align: left;
font-family: monospace;
}
#geraeterow {
max-width: 50em;
display: grid;
grid-template-columns: 1fr;
grid-row-gap: 2%;
font-size: 90%;
}
#geraetebuttons {
padding-right: 1em;
place-self: center;
gap: 10%;
display: grid;
grid-template-columns: 1fr 1fr;
}
</style>
</head>
<body>
<div ng-view>
</div>
<script src="js/app_callmon.js"></script>
</body>
</html>
<div class="container-fluid no-gutters p-1">
<div class="grid-radios" id="geraeterow" id="data-ger">
<div ng-repeat="radio in radios">
<div class="collapse show" id="calls-{{ radio.radioName.split(' ').join('') }}">
<div class="grid-radio">
<div class="radiodesc">
<div class="dropdown ">
<button class="btn btn-link btn-sm" type="button" data-bs-toggle="dropdown" aria-expanded="false">
{{radio.radioDesc}}
</button>
<ul class="dropdown-menu" aria-labelledby="adropdownMenu2">
<li ng-if="radio.radioName.split(' ').join('') != 'Gerät1'"><button class="dropdown-item" data-bs-toggle="collapse" data-bs-target="#calls-Gerät1" role="button" aria-expanded="false" type="button">Toggle 1</button></li>
<li ng-if="radio.radioName.split(' ').join('') != 'Gerät2'"><button class="dropdown-item" data-bs-toggle="collapse" data-bs-target="#calls-Gerät2" role="button" aria-expanded="false" type="button">Toggle 2</button></li>
<li ng-if="radio.radioName.split(' ').join('') != 'Gerät3'"><button class="dropdown-item" data-bs-toggle="collapse" data-bs-target="#calls-Gerät3" role="button" aria-expanded="false" type="button">Toggle 3</button></li>
<li ng-if="radio.radioName.split(' ').join('') != 'Gerät4'"><button class="dropdown-item" data-bs-toggle="collapse" data-bs-target="#calls-Gerät4" role="button" aria-expanded="false" type="button">Toggle 4</button></li>
</ul>
</div>
</div>
<div class="grid-calls">
<div ng-if="0" ng-repeat-start="data in datas | bytypes: {sds: false, status: false, call:true } | bygeraet: radio.radioName | limitTo: 2"></div>
<div class="calldest text-secondary">
{{data.destName}}
</div>
<div class="timestamp">
{{data.timestamp | date:'HH:mm:ss'}}
</div>
<div class="duration text-secondary {{data.dataType != 0 && data.type == 'call' && data.dur== null ? 'speaking':'' }}" ng-if="data.dataType != 0 && data.type == 'call' && !(data.dur == null)">
{{ (data.dur / 1000)| number:'0.0' }}s
</div>
<div ng-if="data.dataType == 0 || data.type != 'call'">
{{data.dur}}
</div>
<div class="" ng-if="data.dataType != 0 && data.type == 'call' && data.dur== null">
&nbsp;
</div>
<div class="rufname {{data.dataType != 0 && data.type == 'call' && data.dur== null ? 'speaking':'' }}">
{{data.srcName}}
</div>
<div class="opta">
{{data.message}}
</div>
<div ng-repeat-end ng-if="0"></div>
</div>
</div>
</div>
</div>
</div>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment