diff --git a/openmodemconfig.py b/openmodemconfig.py index 26fe0c6..c24cc28 100644 --- a/openmodemconfig.py +++ b/openmodemconfig.py @@ -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() \ No newline at end of file diff --git a/public/index.html b/public/index.html index d6041d4..75915d0 100644 --- a/public/index.html +++ b/public/index.html @@ -17,12 +17,16 @@ -
+
+
+
+
  Connection - + +
@@ -36,9 +40,7 @@
-
@@ -50,19 +52,28 @@
- + + + + + +
-
+
Connect
+
+ Disconnect +

@@ -247,7 +258,7 @@
- Apply Modem Configuration + Save Modem Configuration
diff --git a/public/main.js b/public/main.js index 0b85c8b..352ecbe 100644 --- a/public/main.js +++ b/public/main.js @@ -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("") + 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}); }