Basic connection setup and audio level monitoring

master
Mark Qvist 2019-02-14 22:21:11 +01:00
parent bddb318780
commit 98f5ce330e
3 changed files with 515 additions and 35 deletions

View File

@ -1,18 +1,337 @@
import serial
from serial.tools import list_ports
import requests
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn
import time
import struct
from time import sleep
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from SocketServer import ThreadingMixIn
from urlparse import urlparse, parse_qs
import threading
import webview
import random
import os
import sys
portlist = []
kiss_interface = None
class RNS():
@staticmethod
def log(msg):
logtimefmt = "%Y-%m-%d %H:%M:%S"
timestamp = time.time()
logstring = "["+time.strftime(logtimefmt)+"] "+msg
print(logstring)
@staticmethod
def hexrep(data, delimit=True):
delimiter = ":"
if not delimit:
delimiter = ""
hexrep = delimiter.join("{:02x}".format(ord(c)) for c in data)
return hexrep
@staticmethod
def prettyhexrep(data):
delimiter = ""
hexrep = "<"+delimiter.join("{:02x}".format(ord(c)) for c in data)+">"
return hexrep
class Interface:
IN = False
OUT = False
FWD = False
RPT = False
name = None
def __init__(self):
pass
class KISS():
FEND = chr(0xC0)
FESC = chr(0xDB)
TFEND = chr(0xDC)
TFESC = chr(0xDD)
CMD_UNKNOWN = chr(0xFE)
CMD_DATA = chr(0x00)
CMD_TXDELAY = chr(0x01)
CMD_P = chr(0x02)
CMD_SLOTTIME = chr(0x03)
CMD_TXTAIL = chr(0x04)
CMD_FULLDUPLEX = chr(0x05)
CMD_SETHARDWARE = chr(0x06)
CMD_READY = chr(0x0F)
CMD_AUDIO_PEAK = chr(0x12)
CMD_OUTPUT_GAIN = chr(0x09)
CMD_INPUT_GAIN = chr(0x0A)
CMD_EN_DIAGS = chr(0x13)
CMD_RETURN = chr(0xFF)
@staticmethod
def escape(data):
data = data.replace(chr(0xdb), chr(0xdb)+chr(0xdd))
data = data.replace(chr(0xc0), chr(0xdb)+chr(0xdc))
return data
class KISSInterface(Interface):
MAX_CHUNK = 32768
owner = None
port = None
speed = None
databits = None
parity = None
stopbits = None
serial = None
def __init__(self, owner, name, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control):
self.serial = None
self.owner = owner
self.name = name
self.port = port
self.speed = speed
self.databits = databits
self.parity = serial.PARITY_NONE
self.stopbits = stopbits
self.timeout = 100
self.online = False
self.audiopeak = 0
self.has_decode = False
self.packet_queue = []
self.flow_control = flow_control
self.interface_ready = False
self.preamble = preamble if preamble != None else 350;
self.txtail = txtail if txtail != None else 20;
self.persistence = persistence if persistence != None else 64;
self.slottime = slottime if slottime != None else 20;
if parity.lower() == "e" or parity.lower() == "even":
self.parity = serial.PARITY_EVEN
if parity.lower() == "o" or parity.lower() == "odd":
self.parity = serial.PARITY_ODD
try:
RNS.log("Opening serial port "+self.port+"...")
self.serial = serial.Serial(
port = self.port,
baudrate = self.speed,
bytesize = self.databits,
parity = self.parity,
stopbits = self.stopbits,
xonxoff = False,
rtscts = False,
timeout = 0,
inter_byte_timeout = None,
write_timeout = None,
dsrdtr = False,
)
except Exception as e:
RNS.log("Could not open serial port "+self.port)
raise e
if self.serial.is_open:
# Allow time for interface to initialise before config
sleep(1.5)
thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True)
thread.start()
self.online = True
RNS.log("Serial port "+self.port+" is now open")
self.interface_ready = True
RNS.log("KISS interface configured")
else:
raise IOError("Could not open serial port")
def askForPeak(self):
kiss_command = KISS.FEND+KISS.CMD_AUDIO_PEAK+b'\x01'+KISS.FEND
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("Could not ask for peak data")
def displayPeak(self, peak):
peak_value = struct.unpack("b", peak)
self.audiopeak = peak_value[0]
def setPreamble(self, preamble):
preamble_ms = preamble
preamble = int(preamble_ms / 10)
if preamble < 0:
preamble = 0
if preamble > 255:
preamble = 255
RNS.log("Setting preamble to "+str(preamble)+" "+chr(preamble))
kiss_command = KISS.FEND+KISS.CMD_TXDELAY+chr(preamble)+KISS.FEND
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("Could not configure KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")")
def setTxTail(self, txtail):
txtail_ms = txtail
txtail = int(txtail_ms / 10)
if txtail < 0:
txtail = 0
if txtail > 255:
txtail = 255
kiss_command = KISS.FEND+KISS.CMD_TXTAIL+chr(txtail)+KISS.FEND
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("Could not configure KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")")
def setPersistence(self, persistence):
if persistence < 0:
persistence = 0
if persistence > 255:
persistence = 255
kiss_command = KISS.FEND+KISS.CMD_P+chr(persistence)+KISS.FEND
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("Could not configure KISS interface persistence to "+str(persistence))
def setSlotTime(self, slottime):
slottime_ms = slottime
slottime = int(slottime_ms / 10)
if slottime < 0:
slottime = 0
if slottime > 255:
slottime = 255
kiss_command = KISS.FEND+KISS.CMD_SLOTTIME+chr(slottime)+KISS.FEND
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("Could not configure KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")")
def setFlowControl(self, flow_control):
kiss_command = KISS.FEND+KISS.CMD_READY+chr(0x01)+KISS.FEND
written = self.serial.write(kiss_command)
if written != len(kiss_command):
if (flow_control):
raise IOError("Could not enable KISS interface flow control")
else:
raise IOError("Could not enable KISS interface flow control")
def setInputGain(self, gain):
if gain < 0:
gain = 0
if gain > 255:
gain = 255
kiss_command = KISS.FEND+KISS.CMD_INPUT_GAIN+chr(gain)+KISS.FEND
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("Could not configure KISS interface input gain to "+str(gain))
def setOutputGain(self, gain):
if gain < 0:
gain = 0
if gain > 255:
gain = 255
kiss_command = KISS.FEND+KISS.CMD_OUTPUT_GAIN+chr(gain)+KISS.FEND
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("Could not configure KISS interface input gain to "+str(gain))
def enableDiagnostics(self):
kiss_command = KISS.FEND+KISS.CMD_EN_DIAGS+chr(0x01)+KISS.FEND
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("Could not enable KISS interface diagnostics")
def disableDiagnostics(self):
kiss_command = KISS.FEND+KISS.CMD_EN_DIAGS+chr(0x00)+KISS.FEND
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("Could not disable KISS interface diagnostics")
os._exit(0)
def processIncoming(self, data):
self.has_decode = True
RNS.log("Decoded packet");
def processOutgoing(self,data):
pass
def queue(self, data):
pass
def process_queue(self):
pass
def readLoop(self):
try:
in_frame = False
escape = False
command = KISS.CMD_UNKNOWN
data_buffer = ""
last_read_ms = int(time.time()*1000)
while self.serial.is_open:
if self.serial.in_waiting:
byte = self.serial.read(1)
last_read_ms = int(time.time()*1000)
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
in_frame = False
self.processIncoming(data_buffer)
elif (byte == KISS.FEND):
in_frame = True
command = KISS.CMD_UNKNOWN
data_buffer = ""
elif (in_frame and len(data_buffer) < 611):
if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
command = byte
elif (command == KISS.CMD_DATA):
if (byte == KISS.FESC):
escape = True
else:
if (escape):
if (byte == KISS.TFEND):
byte = KISS.FEND
if (byte == KISS.TFESC):
byte = KISS.FESC
escape = False
data_buffer = data_buffer+byte
elif (command == KISS.CMD_AUDIO_PEAK):
self.displayPeak(byte)
else:
time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout:
data_buffer = ""
in_frame = False
command = KISS.CMD_UNKNOWN
escape = False
sleep(0.2)
self.askForPeak()
except Exception as e:
self.online = False
RNS.log("A serial port error occurred, the contained exception was: "+str(e))
RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.")
raise e
def __str__(self):
return "KISSInterface["+self.name+"]"
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
"""Handle requests threaded via mixin"""
class appRequestHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
return
def json_headers(request):
request.send_response(200)
request.send_header("Content-Type", "text/json")
@ -25,12 +344,40 @@ class appRequestHandler(BaseHTTPRequestHandler):
request.end_headers()
if (request.path == "/favicon.ico"):
request.path = "/app/favicon.ico"
#request.path = "/app/favicon.ico"
request.send_response(404)
if (request.path == "/getports"):
request.json_headers()
request.wfile.write(json.dumps(list_serial_ports()).encode("utf-8"))
if (request.path == "/getconfig"):
request.json_headers()
request.wfile.write(json.dumps({"response":"ok"}).encode("utf-8"))
if (request.path == "/getpeak"):
global kiss_interface
request.json_headers()
request.wfile.write(json.dumps({"response":"ok", "peak":kiss_interface.audiopeak, "decode": kiss_interface.has_decode}).encode("utf-8"))
kiss_interface.has_decode = False
if (request.path.startswith("/disconnect")):
close_device()
if (request.path.startswith("/connect")):
request.json_headers()
query = parse_qs(urlparse(request.path).query)
q_port = query["port"][0]
q_baud = int(query["baud"][0])
if (open_device(q_port, q_baud)):
request.wfile.write(json.dumps({"response":"ok"}).encode("utf-8"))
else:
request.wfile.write(json.dumps({"response":"failed"}).encode("utf-8"))
if (request.path.startswith("/app/")):
path = request.path.replace("/app/", "")
file = None
print(path)
if (path == ""):
file = "index.html"
@ -50,13 +397,35 @@ class appRequestHandler(BaseHTTPRequestHandler):
request.end_headers()
requestpath = "./public/"+file
print(requestpath)
fh = open(requestpath, "rb")
request.wfile.write(fh.read())
fh.close()
def list_serial_ports():
ports = list_ports.comports()
portlist = []
for port in ports:
portlist.insert(0, port.device)
return portlist
def open_device(port, baud):
global kiss_interface
try:
kiss_interface = KISSInterface(None, "OpenModem", port, baud, 8, "N", 1, None, None, None, None, False)
kiss_interface.enableDiagnostics()
kiss_interface.setInputGain(200)
return True
except Exception as e:
#raise e
return False
def close_device():
os._exit(0)
def get_port():
return 44444
return random.randrange(40000,49999,1)
def start_server():
@ -65,6 +434,7 @@ def start_server():
while not server_started and retries < 100:
try:
list_serial_ports()
port = get_port()
server_address = ("127.0.0.1", port)
httpd = ThreadedHTTPServer(server_address, appRequestHandler)
@ -84,7 +454,10 @@ def start_server():
def main():
include_path = os.path.dirname(os.path.realpath(sys.argv[0]))
os.chdir(include_path)
start_server()
ports = list_serial_ports()
print(ports)
if (len(sys.argv) == 1):
start_server()
if __name__ == "__main__":
main()

View File

@ -17,12 +17,16 @@
</head>
<body>
<div class="ui styled fullwidth accordion">
<div class="ui dimmer" id="ind_connecting">
<div class="ui loader"></div>
</div>
<div class="ui styled fullwidth accordion" id="configutil">
<div class="title active">
<i class="dropdown icon"></i>
<i class="fas fa-plug"></i>&nbsp;&nbsp;Connection
<span class="rightfloated">
<i class="fas fa-circle disconnected"></i>
<i class="fas fa-circle disconnected" id="ind_disconnected"></i>
<i class="fas fa-circle connected" id="ind_connected"></i>
</span>
</div>
<div class="content active">
@ -36,9 +40,7 @@
</div>
<div class="eight wide column">
<select class="ui fullwidth dropdown">
<option value="0">tty.USB0</option>
<option value="1">tty.USB1</option>
<select class="ui fullwidth dropdown" id="serialports">
</select>
</div>
@ -50,19 +52,28 @@
</div>
<div class="eight wide column">
<select class="ui fullwidth dropdown">
<select class="ui fullwidth dropdown" id="connectbaudrate">
<option value="1200">1200</option>
<option value="2400">2400</option>
<option value="9600">9600</option>
<option value="14400">14400</option>
<option value="19200">19200</option>
<option value="28800">28800</option>
<option value="38400">38400</option>
<option value="57600">57600</option>
<option value="76800">76800</option>
<option value="115200" selected>115200</option>
<option value="230400">230400</option>
</select>
</div>
<div class="sixteen wide column">
<div class="ui teal fullwidth button" tabindex="0">
<div class="ui teal fullwidth button" tabindex="0" id="connectbutton">
Connect
</div>
<div class="ui red fullwidth button" tabindex="0" id="disconnectbutton">
Disconnect
</div>
</div>
</div>
</p>
@ -247,7 +258,7 @@
<div class="sixteen wide column">
<div class="ui teal fullwidth button" tabindex="0">
Apply Modem Configuration
Save Modem Configuration
</div>
</div>
</div>

View File

@ -1,68 +1,160 @@
jQuery(document).ready(function() {
init_elements();
init_gfx();
update_ports();
})
function update_ports() {
jQuery.getJSON("/getports", function(data) {
jQuery("#serialports").empty();
jQuery.each(data, function(key, val) {
jQuery("#serialports").append("<option value=\""+val+"\">"+val+"</option>")
console.log(val);
});
});
}
function request_disconnect() {
jQuery.getJSON("/disconnect", function(data) {
console.log("Exiting...");
});
}
function request_connection() {
jQuery("#connectbutton").addClass("disabled");
jQuery("#ind_connecting").addClass("active");
port = jQuery("#serialports").val();
baud = jQuery("#connectbaudrate").val()
jQuery.getJSON("/connect?port="+port+"&baud="+baud, function(data) {
console.log(data);
if (data["response"] == "ok") {
console.log("Serial port open");
document.connection_state = true;
setTimeout(request_config, 250);
} else {
console.log("Could not connect");
document.connection_state = false;
alert("Could not connect to the specified serial port. Please make sure the correct serial port is selected.");
request_disconnect()
}
});
}
function request_config() {
console.log("Requesting config...");
$.ajax({
dataType: "json",
url: "/getconfig",
timeout: 500,
success: function(data) {
if (data["response"] == "ok") {
console.log("Connected!");
jQuery("#disconnectbutton").show();
jQuery("#connectbutton").hide();
jQuery("#serialports").addClass("disabled");
jQuery("#connectbaudrate").addClass("disabled");
jQuery("#ind_connecting").removeClass("active");
jQuery("#ind_disconnected").hide();
jQuery("#ind_connected").show();
jQuery("#configutil").accordion("open", 1);
setInterval(function() {
askForPeakData();
}, 250);
} else {
console.log("Invalid response on config request");
alert("Could not get configuration from selected device. Please make sure the correct serial port is selected.");
request_disconnect();
}
},
error: function(jqXHR, status, error) {
console.log("Request timed out");
alert("Could not get configuration from selected device. Please make sure the correct serial port is selected.");
request_disconnect();
}
});
}
function init_elements() {
document.connection_state = false;
jQuery("#ind_connected").hide();
jQuery("#disconnectbutton").hide();
jQuery('.ui.accordion').accordion();
jQuery('.ui.dropdown').dropdown();
jQuery('.ui.checkbox').checkbox()
$('#p-selection').range({
jQuery('#p-selection').range({
min: 0,
max: 255,
start: 128
});
$('#led-selection').range({
jQuery('#led-selection').range({
min: 0,
max: 255,
start: 192
start: 0
});
$('#ingain-selection').range({
jQuery('#ingain-selection').range({
min: 0,
max: 255,
start: 192
start: 0
});
$('#outgain-selection').range({
jQuery('#outgain-selection').range({
min: 0,
max: 255,
start: 192
start: 0
});
jQuery("#connectbutton").click(function() {
request_connection();
});
jQuery("#disconnectbutton").click(function() {
request_disconnect();
});
}
var graph_height = 0;
function init_gfx() {
graph_height = 100;
starty = graph_height-2;
starty = graph_height-3;
document.ingraph = Raphael(document.getElementById("inputgraph"), 519, 100);
setInterval(function() {
udpateInputGraph();
}, 100);
}
function getInputPeak() {
var rval = Math.random()*255+35;
if (rval > 255) rval = 255;
return rval;
function askForPeakData() {
jQuery.getJSON("/getpeak", function(data) {
if (data["response"] == "ok") {
console.log(data);
if (data["decode"] == true) {
udpateInputGraph(1024);
console.log("DECODE");
} else {
udpateInputGraph(parseInt(data["peak"]));
}
}
})
}
function normalize(sample) {
var factor = 255/graph_height;
return (sample/factor)*0.98;
var res = ((sample*2)/factor)*0.98;
//console.log(res);
return res;
}
var in_samples_max = 103;
var in_samples = [];
var starty;
var t = 5;
var peak_threshold = 255;
var peak_threshold = 96;
var sw = t-2;
var ofs = 4;
function udpateInputGraph() {
in_samples.push(getInputPeak());
function udpateInputGraph(peakval) {
in_samples.push(peakval);
if (in_samples.length > in_samples_max) {
in_samples.shift();
}
@ -70,9 +162,13 @@ function udpateInputGraph() {
for (i = 0; i < in_samples.length; i++) {
var sample = normalize(in_samples[i]);
var line = document.ingraph.path("M"+(i*t+ofs)+" "+starty+"V"+(graph_height-sample));
var line = document.ingraph.path("M"+(i*t+ofs)+" "+starty+"V"+(starty-sample));
if (in_samples[i] >= peak_threshold) {
line.attr({stroke: "#c00", "stroke-width": sw});
if (in_samples[i] >= 1024) {
line.attr({stroke: "#00c", "stroke-width": sw});
} else {
line.attr({stroke: "#c00", "stroke-width": sw});
}
} else {
line.attr({stroke: "#0c0", "stroke-width": sw});
}