1
0
Fork 0

First functional codebase

First codebase with readme for successful installation... maybe
main
Kim - DG9VH 4 years ago
parent 390f319f22
commit a748ea5ccf

@ -1 +1,29 @@
# MMDVMHost-Websocketboard
# MMDVMHost-Websocketboard
## Introduction
This is a very first development version of my new MMDVMDash using websockets-technology to get rid of high load on Raspberry Pi and others and keep those temperature down.
Also this should improve user experience.
## Installation
You'll need to install several python3 modules. A concrete list will follow here later.
### Installation steps
* clone this repository to your home-directory
* create directory with `sudo mkdir /opt/MMDVMDash`
* change ownership to your user for example with `sudo chown -R pi /opt/MMDVMDash`
* copy all files from repository into this folder
* modify *logtailer.ini* to fit your needs
* copy files in */opt/MMDVMDash/systemd* to */etc/systemd/system* or similar corresponding to your system
* modify both scripts to fit your needs
* enable services with following commmands, this results in starting both automatically after reboot:
* `sudo systemctl enable http.server.service`
* `sudo systemctl enable logtailer.service`
* start services with following commmands:
* `sudo systemctl enable http.server.service`
* `sudo systemctl enable logtailer.service`
Finally you should be able to get the new Dashboard calling the hostname of your hotspot
## Screenshots
![Screenshot of MMDVMDash Websocketboard](img/Screenshot.png "Screenshot of MMDVMDash Websocketboard")

Binary file not shown.

@ -0,0 +1,160 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="MMDVM-Dashboard by DG9VH">
<meta name="author" content="DG9VH">
<!-- So refresh works every time -->
<meta http-equiv="expires" content="0">
<title>MMDVM-Dashboard by DG9VH</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<!-- Bootstrap core JavaScript -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<!-- Datatables -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.10.21/css/jquery.dataTables.min.css">
<script type="text/javascript" src="https://cdn.datatables.net/1.10.21/js/jquery.dataTables.min.js"></script>
<script type="text/javascript" src="js/functions.js"></script>
<style>
nowrap {
white-space:nowrap
}
</style>
<title>DG9VH - MMDVM-Dashboard by DG9VH</title>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top">
<div class="container-fluid">
<span class="float:left">
<a class="navbar-brand" href="#">MMDVM-Dashboard by DG9VH</a>
</span>
<span class="navbar-brand float:right">Websocket-Based</span>
</div>
</nav>
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="lastheard-tab" data-toggle="tab" href="#lastheard" role="tab" aria-controls="lastheard" aria-selected="true">Last Heard</a>
</li>
<li class="nav-item">
<a class="nav-link" id="localheard-tab" data-toggle="tab" href="#localheard" role="tab" aria-controls="localheard" aria-selected="true">Local Heard</a>
</li>
<li class="nav-item">
<a class="nav-link" id="qso-tab" data-toggle="tab" href="#qso" role="tab" aria-controls="qso" aria-selected="false">In QSO</a>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="lastheard" role="tabpanel" aria-labelledby="lastheard-tab">
<div class="panel panel-default">
<!-- Standard-Panel-Inhalt -->
<div class="panel-heading">Last Heard List of today's callsigns.<span class="pull-right clickable"><i class="glyphicon glyphicon-chevron-up"></i></span></div>
<div class="panel-body">
<!-- Tabelle -->
<div class="table-responsive">
<table id="lastHeard" class="table lastHeard table-condensed table-striped table-hover">
<thead>
<tr>
<th>Time (UTC)</th>
<th>Mode</th>
<th>Callsign</th>
<th>Target</th>
<th>Source</th>
<th>Dur (s)</th>
<th>Loss</th>
<th>BER</th>
<th>QSO</th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="localheard" role="tabpanel" aria-labelledby="localheard-tab">
<div class="panel panel-default">
<!-- Standard-Panel-Inhalt -->
<div class="panel-heading">Local Heard List of today<span class="pull-right clickable"><i class="glyphicon glyphicon-chevron-up"></i></span></div>
<div class="panel-body">
<!-- Tabelle -->
<div class="table-responsive">
<table id="localHeard" class="table localHeard table-condensed table-striped table-hover">
<thead>
<tr>
<th>Time (UTC)</th>
<th>Mode</th>
<th>Callsign</th>
<th>Target</th>
<th>Source</th>
<th>Dur (s)</th>
<th>BER</th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="qso" role="tabpanel" aria-labelledby="qso-tab">
<div class="panel panel-default">
<!-- Standard-Panel-Inhalt -->
<div class="panel-heading">Currently in QSO<span class="pull-right clickable"><i class="glyphicon glyphicon-chevron-up"></i></span></div>
<div class="panel-body">
<!-- Tabelle -->
<div class="table-responsive">
<button id="button">Delete selected row</button>
<table id="inQSO" class="table inQSO table-condensed table-striped table-hover">
<thead>
<tr>
<th>Callsign</th>
<th>Added at</th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
var t_lh = $('#lastHeard').DataTable( {
"order": [[ 0, "desc" ]]
} );
var t_localh = $('#localHeard').DataTable( {
"order": [[ 0, "desc" ]]
} );
var ws = new WebSocket("ws://" + window.location.hostname + ":5678");
ws.onmessage = function (event) {
getLastHeard(document, event);
getLocalHeard(document, event);
};
t_lh.order( [ 0, 'desc' ] ).draw();
t_localh.order( [ 0, 'desc' ] ).draw();
var t_qso = $('#inQSO').DataTable( {
"order": [[ 0, "asc" ]]
} );
$('#inQSO tbody').on( 'click', 'tr', function () {
if ( $(this).hasClass('selected') ) {
$(this).removeClass('selected');
} else {
t_qso.$('tr.selected').removeClass('selected');
$(this).addClass('selected');
}
} );
$('#button').click( function () {
t_qso.row('.selected').remove().draw( false );
} );
</script>
</body>
</html>

@ -0,0 +1,159 @@
// 00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333
// 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
// M: 2020-11-01 21:33:27.454 YSF, received network data from DG2MAS to DG-ID 0 at DG2MAS
// M: 2020-11-01 21:33:35.025 YSF, received network end of transmission from DG2MAS to DG-ID 0, 7.7 seconds, 0% packet loss, BER: 0.0%
function getTimestamp(logline) {
return logline.substring(3,22);
}
function getMode(logline) {
return logline.substring(27, logline.indexOf(","));
}
function getCallsign(logline) {
return logline.substring(logline.indexOf("from") + 5, logline.indexOf("to"));
}
function getTarget(logline) {
if(logline.indexOf("at") > 0) {
return logline.substring(logline.indexOf("to") + 3, logline.lastIndexOf("at"));
} else {
return logline.substring(logline.indexOf("to") + 3, logline.substring(logline.indexOf("to") + 3).indexOf(",") + logline.indexOf("to") + 3);
}
}
function getSource(logline) {
val = logline.substring(logline.indexOf("received") + 9);
val = val.substring(0, val.indexOf(" "));
if (val == "network")
val = "Net";
return val;
}
function getDuration(logline) {
if(logline.lastIndexOf("seconds") > 0) {
val = logline.substring(0, logline.lastIndexOf("seconds"));
val = val.substring(val.lastIndexOf(",") + 2);
return val;
} else {
return "";
}
}
function getLoss(logline) {
if(logline.lastIndexOf("seconds") > 0) {
val = logline.substring(logline.lastIndexOf("seconds") + 9, logline.indexOf("%"));
if (val.indexOf("BER") == -1) {
return val;
} else {
return "";
}
} else {
return "";
}
}
function getBER(logline) {
if(logline.lastIndexOf("BER") > 0) {
return logline.substring(logline.lastIndexOf("BER") + 4);
} else {
return "";
}
}
function getAddToQSO(logline) {
retval = '<div class="bd-clipboard"><button type="button" class="btn-cpQSO" title="Copy to QSO" id="' + getCallsign(logline) + '" onclick="copyToQSO(\'' + getCallsign(logline) + '\')">Copy</button></div>';
return retval;
}
function clocktime() {
var now = new Date(),
h = now.getHours(),
m = now.getMinutes(),
s = now.getSeconds();
m = leadingZero(m);
s = leadingZero(s);
return h + ':' + m + ':' + s;
}
function leadingZero(zahl) {
zahl = (zahl < 10 ? '0' : '' )+ zahl;
return zahl;
}
function copyToQSO(callsign) {
$(document).ready(function() {
t_qso.row.add( [
callsign,
new Date().toUTCString()
] ).draw();
});
alert("" + callsign + " added to in QSO-Tab");
}
function getLastHeard(document, event) {
// 00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333
// 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
// M: 2020-11-01 21:33:27.454 YSF, received network data from DG2MAS to DG-ID 0 at DG2MAS
// M: 2020-11-01 21:33:35.025 YSF, received network end of transmission from DG2MAS to DG-ID 0, 7.7 seconds, 0% packet loss, BER: 0.0%
$(document).ready(function() {
var rowIndexes = [];
t_lh.rows( function ( idx, data, node ) {
if(data[2] === getCallsign(event.data)){
rowIndexes.push(idx);
}
return false;
});
if (rowIndexes[0]) {
newData = [
getTimestamp(event.data),
getMode(event.data),
getCallsign(event.data),
getTarget(event.data),
getSource(event.data),
getDuration(event.data),
getLoss(event.data),
getBER(event.data),
getAddToQSO(event.data)
]
t_lh.row(rowIndexes[0]).data( newData ).draw();
} else {
t_lh.row.add( [
getTimestamp(event.data),
getMode(event.data),
getCallsign(event.data),
getTarget(event.data),
getSource(event.data),
getDuration(event.data),
getLoss(event.data),
getBER(event.data),
getAddToQSO(event.data)
] ).draw();
}
});
}
function getLocalHeard(document, event) {
// 00000000001111111111222222222233333333334444444444555555555566666666667777777777888888888899999999990000000000111111111122222222223333333333
// 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
// M: 2020-11-01 21:33:27.454 YSF, received network data from DG2MAS to DG-ID 0 at DG2MAS
// M: 2020-11-01 21:33:35.025 YSF, received network end of transmission from DG2MAS to DG-ID 0, 7.7 seconds, 0% packet loss, BER: 0.0%
$(document).ready(function() {
if (getSource(event.data) == "RF") {
if (getDuration(event.data) !== "") {
t_localh.row.add( [
getTimestamp(event.data),
getMode(event.data),
getCallsign(event.data),
getTarget(event.data),
getSource(event.data),
getDuration(event.data),
getBER(event.data)
] ).draw();
}
}
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

@ -0,0 +1,9 @@
[DEFAULT]
Host=0.0.0.0
Port=5678
Webport=8080
[MMDVMHost]
Logdir=/mnt/ramdisk/
Prefix=MMDVM
Num_Lines=10000

@ -0,0 +1,102 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#import time
import datetime
import os.path
import asyncio
import logging
import argparse
import websockets
import http.server
import socketserver
import configparser
import os
from collections import deque
from urllib.parse import urlparse, parse_qs
from ansi2html import Ansi2HTMLConverter
from threading import Thread
current_dir = os.getcwd()
config = configparser.ConfigParser()
config.read(current_dir + '/logtailer.ini')
NUM_LINES = int(config['MMDVMHost']['Num_Lines'])
# init
logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO)
conv = Ansi2HTMLConverter(inline=True)
@asyncio.coroutine
def view_log(websocket, path):
global config
logging.info('Connected, remote={}, path={}'.format(websocket.remote_address, path))
try:
now = datetime.datetime.now()
year = str(now.year)
month = str(now.month)
if len(month) == 1:
month = "0" + month
day = str(now.day)
if len(day) == 1:
day = "0" + day
file_path = config['MMDVMHost']['Logdir']+config['MMDVMHost']['Prefix']+"-"+year+"-"+month+"-"+day+".log"
if not os.path.isfile(file_path):
raise ValueError('Not found')
path = file_path
with open(file_path) as f:
content = ''.join(deque(f, NUM_LINES))
content = conv.convert(content, full=False)
lines = content.split("\n")
for line in lines:
if line.find('received') >0 and not line.find('network watchdog') > 0:
yield from websocket.send(line)
while True:
content = f.read()
if content:
content = conv.convert(content, full=False)
yield from websocket.send(content)
else:
yield from asyncio.sleep(1)
except ValueError as e:
try:
yield from websocket.send('<font color="red"><strong>{}</strong></font>'.format(e))
yield from websocket.close()
except Exception:
pass
log_close(websocket, path, e)
except Exception as e:
log_close(websocket, path, e)
else:
log_close(websocket, path)
def log_close(websocket, path, exception=None):
message = 'Closed, remote={}, path={}'.format(websocket.remote_address, path)
if exception is not None:
message += ', exception={}'.format(exception)
logging.info(message)
def websocketserver():
start_server = websockets.serve(view_log, config['DEFAULT']['Host'], config['DEFAULT']['Port'])
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
def main():
websocketserver()
if __name__ == '__main__':
main()

@ -0,0 +1,13 @@
[Unit]
Description=Python3 http.server
After=network.target
[Service]
ExecStartPre=/bin/sleep 30
Type=simple
Restart=always
# Modify for different location of Python3 or other port
ExecStart=/usr/bin/python3 -m http.server 8000 --directory /opt/MMDVMDash/html
[Install]
WantedBy=multi-user.target

@ -0,0 +1,14 @@
[Unit]
Description=Python3 logtailer for MMDVMDash
After=network.target
[Service]
ExecStartPre=/bin/sleep 30
Type=simple
Restart=always
# Modify for different location of Python3 or other port
WorkingDirectory=/opt/MMDVMDash/
ExecStart=/usr/bin/python3 /opt/MMDVMDash/logtailer.py
[Install]
WantedBy=multi-user.target
Loading…
Cancel
Save