Web Server Gateway Interface (WSGI) daemon for serial

This is early work so it is not really usable, but it may help show the idea. To be extra clear I have not used WSGI so this is new to me.

The Python code old code see GitHub

#!/usr/bin/env python3
# Web Server Gateway Interface (WSGI) daemon  
# use to serve request on a TCP/IP PORT for serial devices that use simple commands 
# at CLI run with
# python3 WSGIdaemon.py
# killing it is a PITA, I just close the terminal, yep that needs some work.
# to try it open a brower with url: http://localhost:8000/?addr=0&cmd=id&q=true

# For info on WSGI see https://docs.python.org/3.7/library/wsgiref.html
# For info on CGI see https://docs.python.org/3.7/library/cgi.html

# The goal is to hold a seril link open while a web server runs pages, e.g. use the WSGI daemon to access the serial link.
# it takes commands from the CGI query string and turns them into the format I am using e.g. "/0/id?" or "/0/adc 3,4,5" 

# I am using python3 so make sure the serial package is installed. 
# sudo apt-get install python3-serial 

import json
import serial
from time import sleep
from wsgiref.simple_server import make_server
from cgi import parse_qs, escape

# claim the serial link (e.g. it is a resourse that I will provide use of)
# hardwar serial on R-Pi is/dev/ttyAMA0
# FTDI and some other usb serial is /dev/ttyUSB0
# Arduino's ATmega16u2 is a modem... what? /dev/ttyACM0
device = "/dev/ttyUSB0"
sio = serial.Serial(device,38400, timeout=3)
print("claim serial link " + device + "\n")
# TBD only run one WSGI server with one thread

# A relatively simple WSGI application. 
def simple_app(environ, start_response):
    
    # Returns a dictionary from CGI in which the values are lists, it is the easy way
    query_string = parse_qs(environ['QUERY_STRING'])
    #given URL: http://localhost:8000/?addr=0&cmd=id&q=true
    #the QUERY_STRING is: addr=0&cmd=id&q=true
    addr = query_string.get('addr', [''])[0] # Returns the first addr value
    cmd_base = query_string.get('cmd', [''])[0] # Returns the first cmd value
    q = query_string.get('q', [''])[0] # Returns the first q value
    # TBD thre are arguments to add, but this is a start

    status = '200 OK'
    headers = [('Content-type', 'text/plain; charset=utf-8')]

    start_response(status, headers)

    #check for correctness of command
    if (addr == ''):
        addr = '0' # for default of /0/id?
    if ( (addr < '0') or (addr > '9') ):
        ret = [ ("ERR: addr must be in the range 0..9\n").encode('utf-8') + 
                (b"ERR: bad query_string \"addr=" + addr.encode('utf-8') + b"\"\n").decode().encode('utf-8') ]
        return ret
    if (cmd_base == ''):
        cmd_base = "id" # default
    if ( not (cmd_base.isalpha()) ):
        ret = [ ("ERR: cmd must be a string that isalpha\n").encode('utf-8') + 
                (b"ERR: bad query_string \"cmd=" + cmd_base.encode('utf-8') + b"\"\n").decode().encode('utf-8') ]
        return ret
    if (q == ''):
        q = 'true' # default needs a ? after the cmd_base 
    if ( not ( (q == 'true') or (q == 'false')) ):
        ret = [ ("ERR: q must be true or false\n").encode('utf-8') + 
                (b"ERR: bad query_string \"q=" + q.encode('utf-8') + b"\"\n").decode().encode('utf-8') ]
        return ret

    # note: if the command will not encode with ascii then use bytes e.g.
    # command = b"/"+bytes([192])+b"/id?"
    if (q == "true"):
        command =  "/"+addr+"/"+cmd_base+"?\n"
    else:
        command =  "/"+addr+"/"+cmd_base+"\n"
    sio.write(command.encode('utf-8')) # "/0/id?" is like the command I want to send on the serial link 
    sio_echo_cmd = sio.readline().strip() # my serial device echo's the command 
    sio_echo = sio.readline().strip() # and then outpus the JSON
    
    # format as bytestring suitable for transmission as HTTP response headers
    ret = [ (sio_echo_cmd + b"\n").decode().encode('utf-8') +
            (sio_echo + b"\n").decode().encode('utf-8') ]
    return ret


with make_server('', 8000, simple_app) as httpd:
    print("Serving on port 8000...")
    httpd.serve_forever()

The idea is to have the daemon make a command for the serial link, the commands I use resemble "/0/id?". The second character is an address on the serial bus (it is a multi-drop), the command is "id", and the "?" means it is a query (but everything has an echo and returns JSON so it's just adding overhead)

If I do end up using this I will likely keep the updates at

My serial commands can have up to five arguments so I wanted to add that. Also deal with a bad request a little better.

Python (old code see GitHub)

#!/usr/bin/env python3
# Web Server Gateway Interface (WSGI) daemon  
# use to serve request on a TCP/IP PORT for serial devices that use simple commands 
# at CLI run with
# python3 WSGIdaemon.py
# killing it is a PITA, I just close the terminal, yep that needs some work.
# to try it open a brower with url: http://localhost:8000/?addr=0&cmd=id&q=true

# For info on WSGI see https://docs.python.org/3.7/library/wsgiref.html
# For info on CGI see https://docs.python.org/3.7/library/cgi.html

# The goal is to hold a seril link open while a web server runs pages, e.g. use the WSGI daemon to access the serial link.
# it takes commands from the CGI query string and turns them into the format I am using e.g. "/0/id?" or "/0/adc 3,4,5" 

# I am using python3 so make sure the serial package is installed. 
# sudo apt-get install python3-serial 

import json
import serial
from time import sleep
from wsgiref.simple_server import make_server
from cgi import parse_qs, escape

# claim the serial link (e.g. it is a resourse that I will provide use of)
# hardwar serial on R-Pi is/dev/ttyAMA0
# FTDI and some other usb serial is /dev/ttyUSB0
# Arduino's ATmega16u2 is a modem... what? /dev/ttyACM0
device = "/dev/ttyUSB0"
sio = serial.Serial(device,38400, timeout=3)
print("claim serial link " + device)
# TBD only run one WSGI server with one thread

# A relatively simple WSGI application. 
def simple_app(environ, start_response):
    
    # Returns a dictionary from CGI in which the values are lists, it is the easy way
    query_string = parse_qs(environ['QUERY_STRING'])
    #given URL: http://localhost:8000/?addr=0&cmd=id&q=true&arg1=1&arg2=2&arg3=3&arg4=4&arg5=5
    #the QUERY_STRING is: addr=0&cmd=id&q=true&arg1=1&arg2=2&arg3=3&arg4=4&arg5=5
    addr = query_string.get('addr', [''])[0] # Returns the first addr value
    cmd_base = query_string.get('cmd', [''])[0] # Returns the first cmd value
    q = query_string.get('q', [''])[0] # Returns the first q value
    arg1 = query_string.get('arg1', [''])[0] # Returns the first argument value
    arg2 = query_string.get('arg2', [''])[0] # Returns the first argument value
    arg3 = query_string.get('arg3', [''])[0] # Returns the first argument value
    arg4 = query_string.get('arg4', [''])[0] # Returns the first argument value
    arg5 = query_string.get('arg5', [''])[0] # Returns the first argument value

    #check for correctness of command
    if (addr == ''):
        addr = '0' # for default of /0/id?
    if ( not (addr.isalnum() and (len(addr) == 1) ) ):
        status = '400 Bad Request'
        headers = [('Content-type', 'text/plain; charset=utf-8')]
        start_response(status, headers)
        ret = [ ("ERR: addr must be a string of len == 1 that isalnum\n").encode('utf-8') + 
                (b"ERR: bad query_string \"addr=" + addr.encode('utf-8') + b"\"\n").decode().encode('utf-8') ]
        return ret
    if (cmd_base == ''):
        cmd_base = "id" # default
    if ( not (cmd_base.isalpha()) ):
        status = '400 Bad Request'
        headers = [('Content-type', 'text/plain; charset=utf-8')]
        start_response(status, headers)
        ret = [ ("ERR: cmd must be a string that isalpha\n").encode('utf-8') + 
                (b"ERR: bad query_string \"cmd=" + cmd_base.encode('utf-8') + b"\"\n").decode().encode('utf-8') ]
        return ret

    # start putting the command togather
    command =  "/"+addr+"/"+cmd_base

    if (q == ''):
        q = 'true' # default needs a ? after the cmd_base 
    if ( not ( (q == 'true') or (q == 'false')) ):
        status = '400 Bad Request'
        headers = [('Content-type', 'text/plain; charset=utf-8')]
        start_response(status, headers)
        ret = [ ("ERR: q must be true or false\n").encode('utf-8') + 
                (b"ERR: bad query_string \"q=" + q.encode('utf-8') + b"\"\n").decode().encode('utf-8') ]
        return ret

    # baggage from looking at GPIB commands for too many years
    if (q == "true"):
        command =  command+"?"

    if (len(arg1) >= 1):
        if ( not (arg1.isalnum() ) ):
            status = '400 Bad Request'
            headers = [('Content-type', 'text/plain; charset=utf-8')]
            start_response(status, headers)
            ret = [ ("ERR: arg1 must be a string that isalnum\n").encode('utf-8') + 
                    (b"ERR: bad query_string \"arg1=" + arg1.encode('utf-8') + b"\"\n").decode().encode('utf-8') ]
            return ret
        command =  command+" "+arg1
        
        #ignore arg2 if arg1 not given
        if (len(arg2) >= 1):
            if ( not (arg2.isalnum() ) ):
                status = '400 Bad Request'
                headers = [('Content-type', 'text/plain; charset=utf-8')]
                start_response(status, headers)
                ret = [ ("ERR: arg2 must be a string that isalnum\n").encode('utf-8') + 
                        (b"ERR: bad query_string \"arg2=" + arg2.encode('utf-8') + b"\"\n").decode().encode('utf-8') ]
                return ret
            command =  command+","+arg2

            #ignore arg3 if arg2 not given
            if (len(arg3) >= 1):
                if ( not (arg3.isalnum() ) ):
                    status = '400 Bad Request'
                    headers = [('Content-type', 'text/plain; charset=utf-8')]
                    start_response(status, headers)
                    ret = [ ("ERR: arg3 must be a string that isalnum\n").encode('utf-8') + 
                            (b"ERR: bad query_string \"arg3=" + arg3.encode('utf-8') + b"\"\n").decode().encode('utf-8') ]
                    return ret
                command =  command+","+arg3

                #ignore arg4 if arg3 not given
                if (len(arg4) >= 1):
                    if ( not (arg4.isalnum() ) ):
                        status = '400 Bad Request'
                        headers = [('Content-type', 'text/plain; charset=utf-8')]
                        start_response(status, headers)
                        ret = [ ("ERR: arg4 must be a string that isalnum\n").encode('utf-8') + 
                                (b"ERR: bad query_string \"arg4=" + arg4.encode('utf-8') + b"\"\n").decode().encode('utf-8') ]
                        return ret
                    command =  command+","+arg4

                    #ignore arg5 if arg4 not given
                    if (len(arg5) >= 1):
                        if ( not (arg5.isalnum() ) ):
                            status = '400 Bad Request'
                            headers = [('Content-type', 'text/plain; charset=utf-8')]
                            start_response(status, headers)
                            ret = [ ("ERR: arg5 must be a string that isalnum\n").encode('utf-8') + 
                                    (b"ERR: bad query_string \"arg5=" + arg5.encode('utf-8') + b"\"\n").decode().encode('utf-8') ]
                            return ret
                        command =  command+","+arg5

    sio.write((command+"\n").encode('utf-8')) # "/0/id?" is like the command I want to send on the serial link 
    sio_echo_cmd = b""
    sio_echo_cmd = sio.readline().strip() # my serial device echo's the command 
    if ( not (len(sio_echo_cmd) >= 1) ):
        status = '503 Service Unavailable'
        headers = [('Content-type', 'text/plain; charset=utf-8')]
        start_response(status, headers)
        ret = [ ("ERR: device did not echo command\n").encode('utf-8') + 
                (b"ERR: \command=\"" + command.encode('utf-8') + b"\"\n").decode().encode('utf-8') ]
        return ret
    sio_echo = sio.readline().strip() # and then outpus the JSON
    sio.write("\n".encode('utf-8')) # some commands keep outputing at timed intervals (e.g. /0/adc 1) this should stop them
    if ( not (len(sio_echo) >= 1) ):
        status = '503 Service Unavailable'
        headers = [('Content-type', 'text/plain; charset=utf-8')]
        start_response(status, headers)
        ret = [ ("ERR: device found but ouput not returned\n").encode('utf-8') + 
                (b"ERR: \"command=" + command.encode('utf-8') + b"\"\n").decode().encode('utf-8') ]
        return ret

    status = '200 OK'
    headers = [('Content-type', 'text/plain; charset=utf-8')]

    start_response(status, headers)
    
    # format as bytestring suitable for transmission as HTTP response headers
    ret = [ (sio_echo_cmd + b"\n").decode().encode('utf-8') +
            (sio_echo + b"\n").decode().encode('utf-8') ]
    return ret


with make_server('', 8000, simple_app) as httpd:
    print("Serving on port 8000...")
    httpd.serve_forever()

Now for the mind-bending stuff... JS

Every time I have tried to make sense of JS, I have had to walk away.

When I spin up the WSGI daemon and look at it with a browser it works fine (it has crashed when the browser runs my client.html page since its JS is making the daemon into an angry little beast).

Anyway, the callback does not get called when this client.html page runs its "onTest" JS function but the daemon receives the request (port 8000), sends it to serial (/dev/ttyUSB0), gets back the serial reply, and forwards that to the web browser (I have no clue what the callback port is on) where it is lost (Ubuntu 1804, Firefox). I wonder if the callback function itself goes out of scope?

html with JS

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <script type="text/javascript">
    <!--
        var xhr=new XMLHttpRequest();
        // Use XMLHttpRequest (XHR) objects to interact with servers
        // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
        // The Document Object Model (DOM) connects web pages to scripts or programming languages
        // https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model
        
        xhr.responseType = 'text'
        
        function onTest( url )
        {
           // set the callback to use when the wsgi daemon finishes.
            xhr.onreadystatechange=function()
            {
                if (xhr.readyState==4 && xhr.status==200)
                {
                    document.getElementById( "wsgi_daemon" ).innerHTML = xhr.responseText;
                    document.getElementById( "wsgi_daemon" ).style.color = 'green'
                }
            }

            document.getElementById( "wsgi_daemon" ).style.color = 'red'
            xhr.open("GET", url, true);
            xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
            xhr.send( ); 
        }


    -->
    </script>
</head>

<body>

    <p id="wsgi_daemon"> abcdef </p>
    <a href="javascript:onTest('http://localhost:8000')">/0/id?</a> Click it to run the JS

</body>

</html>

ron_sutherland:
Now for the mind-bending stuff... JS

When I do JS I always put the code into a separate .js file and for me that reduces it to an ordinary programming language.

Using a JS library such as jQuery also makes things a great deal easier. Your code can link to a copy of JQuery that is already hosted somewhere so your server does not have to serve it.

...R

I guess what I am trying to do is have the javascript running on the browser (e.g. the DOM) interact with the gateway daemon, so there is no web server or AJAX stuff going on.

This is the latest static HTML page with JS

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <script type="text/javascript">
    <!--
var xhr = new XMLHttpRequest();
// Use XMLHttpRequest (XHR) objects to interact with servers
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
// The Document Object Model (DOM) connects web pages to scripts or programming languages
// https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model
// what is this callback maddness 
// http://recurial.com/programming/understanding-callback-functions-in-javascript/
xhr.responseType = 'text';

function onTest(url, callback) {
    "use strict";
    // set the callback to use when the wsgi daemon finishes.
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
            callback.call(xhr.responseText);
        }
    }

    xhr.open("GET", url, true);
    xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
    xhr.send(); 
}
    -->
    </script>
</head>

<body>

    <p id="wsgi_daemon"> abcdef </p>
     <button onclick="clickID()">/0/id?</button> 
    
<script>
function clickID() {
    "use strict";
    // if it turns red that is DOM based evidance that call was done
    document.getElementById("wsgi_daemon").style.color = 'yellow';
    onTest('http://localhost:8000', function () {
        // pressent unamed callback gets this when it is called
        document.getElementById("wsgi_daemon").innerHTML = this;
        document.getElementById("wsgi_daemon").style.color = 'green';
    });
    document.getElementById("wsgi_daemon").style.color = 'red';
}
</script>
</body>

</html>

Turns out I wasted most of the day with formatting issues, javascript is way pickier than what I thought.

Links to the gateway daemon work, but that is Jurassic.

I thought the daemon was dead when it produced this on the terminal

127.0.0.1 - - [02/Oct/2018 20:05:26] "GET / HTTP/1.1" 200 99
127.0.0.1 - - [02/Oct/2018 20:05:26] "GET / HTTP/1.1" 200 99
127.0.0.1 - - [02/Oct/2018 20:05:27] "GET / HTTP/1.1" 200 99
127.0.0.1 - - [02/Oct/2018 20:05:58] "GET / HTTP/1.1" 200 99
127.0.0.1 - - [02/Oct/2018 20:05:59] "GET /favicon.ico HTTP/1.1" 200 99
127.0.0.1 - - [02/Oct/2018 20:05:59] "GET /favicon.ico HTTP/1.1" 200 99
Traceback (most recent call last):
  File "/usr/lib/python3.6/wsgiref/handlers.py", line 138, in run
    self.finish_response()
  File "/usr/lib/python3.6/wsgiref/handlers.py", line 180, in finish_response
    self.write(data)
  File "/usr/lib/python3.6/wsgiref/handlers.py", line 274, in write
    self.send_headers()
  File "/usr/lib/python3.6/wsgiref/handlers.py", line 332, in send_headers
    self.send_preamble()
  File "/usr/lib/python3.6/wsgiref/handlers.py", line 255, in send_preamble
    ('Date: %s\r\n' % format_date_time(time.time())).encode('iso-8859-1')
  File "/usr/lib/python3.6/wsgiref/handlers.py", line 453, in _write
    result = self.stdout.write(data)
  File "/usr/lib/python3.6/socketserver.py", line 800, in write
    self._sock.sendall(b)
BrokenPipeError: [Errno 32] Broken pipe
127.0.0.1 - - [02/Oct/2018 20:05:59] "GET /favicon.ico HTTP/1.1" 500 59
----------------------------------------
Exception happened during processing of request from ('127.0.0.1', 59038)
Traceback (most recent call last):
  File "/usr/lib/python3.6/wsgiref/handlers.py", line 138, in run
    self.finish_response()
  File "/usr/lib/python3.6/wsgiref/handlers.py", line 180, in finish_response
    self.write(data)
  File "/usr/lib/python3.6/wsgiref/handlers.py", line 274, in write
    self.send_headers()
  File "/usr/lib/python3.6/wsgiref/handlers.py", line 332, in send_headers
    self.send_preamble()
  File "/usr/lib/python3.6/wsgiref/handlers.py", line 255, in send_preamble
    ('Date: %s\r\n' % format_date_time(time.time())).encode('iso-8859-1')
  File "/usr/lib/python3.6/wsgiref/handlers.py", line 453, in _write
    result = self.stdout.write(data)
  File "/usr/lib/python3.6/socketserver.py", line 800, in write
    self._sock.sendall(b)
BrokenPipeError: [Errno 32] Broken pipe

but it turns out that it is complaining about the callback not receiving on the socket that was negotiated (e.g. the Broken Pipe). The daemon keeps on working with new connections, which was not what I expected.

ron_sutherland:
Turns out I wasted most of the day with formatting issues, javascript is way pickier than what I thought.

It's no worse than C/C++. However unlike (say) Python JS fails silently.

Do you use the Firefox Inspector? It shows the JS errors.

...R

I think I see what the problem is now

Failed to load http://127.0.0.1:8000/: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access.

Basically, the browser does not want to accept the connection because it does not know if it is safe, yet it will open it directly.

I ended up using Chrome to find the error, I did not know where to look on FireFox but I think it is probably the same sort of error. I can see where this needs some sort of authorization but...

Update: ouch

WSGI Access-Control Middleware

ron_sutherland:
I ended up using Chrome to find the error, I did not know where to look on FireFox but I think it is probably the same sort of error. I can see where this needs some sort of authorization but...

I presume Chrome has the same sort of developer tools as Firefox. In the Firefox menu click Web Developer / Inspector

I have had a quick look at your HTML file but I can't figure what you are trying to do.

I won't claim to be an expert of JS (I can just get my own stuff to work). I think this style

onTest('http://localhost:8000', function () {

is creating an anonymous function inside another function - which seems very advanced / complicated.

...R

I don't really understand callback's with C but I manage to do them somehow. I was looking at this

http://recurial.com/programming/understanding-callback-functions-in-javascript/

to try to get some insight into what made a JS callback work. I am betting that under the hood it is all done in C, and that article I linked was the only thing I found that seemed to line up with my C based hunches.

So that anonymous function in the clickID function is what gets called back when the onreaddystatechange anonymous function runs... yep it is a callback hell. This is how all my JS adventures have gone, but I think I finally got the callback right this time.

When I find JS code with anonymous functions I like to re-write it as a regular named function to make it easier for my small brain.

If you can explain what you are trying to achieve I am sufficiently interested (to refresh my JS skills) to have a go at doing it myself.

...R

I was trying to: click on web element -> send to wsgi -> have wsgi convert to a serial command -> run Arduino/avr stuff -> reply to wsgi via serial -> forward that back to the browser via a previously agreed on port that the callback has been set up to receive and finally pass the reply to a DOM element. It's a contraption that seems to have run into a firewall.

I'm going to be out for a few days ... so will catch up when I can.

ron_sutherland:
I was trying to: click on web element -> send to wsgi -> have wsgi convert to a serial command -> run Arduino/avr stuff -> reply to wsgi via serial -> forward that back to the browser

I have a couple of projects that do that using the Python Bottle web framework. And I am rather half-heartedly trying to write a minimal demo that I will post if I get it finished.

...R

Proof of Concept

CORS can be turned off in Firefox with the cors-everywhere-firefox-addon extension (which is for developer use)

The code I was using is on Github

ron_sutherland:
CORS can be turned off in Firefox with the [cors-everywhere-firefox-addon] extension (which is for developer use)

I am reminded of a line from the BBC "Yes Minister" TV series

"That is a brave decision, Minister"

...R

haha, I lost the voters, but now I know my JS callback is working. I think I will try to add the WSGI Access-Control Middleware into the daemon next (that should make the little monster rabid), and then I won't have to turn off CORS.

The daemon has now been set to allow the origin to be from everywhere, which means Cross-Origin Resource Sharing (CORS) does not need turned off in the browser. The reference I used to do this is over seven years old so if someone has a different idea please suggest.

The files I used to do this are in RPUpi/WSGIdaemon at e29239e4391a8bb64c23b63f60e7901c8e0ae7e9 · epccs/RPUpi · GitHub

The "client.html" file is a flat stand-alone HTML sourced from the disk with Edge, Chrome, or Firefox. I have modified it to run through a short list of serial port commands and populate some browser elements. I set the daemon to run on an Ubuntu 18.04 machine near my test bench (it has the local network address 192.168.0.7) and ran the stand-alone HTML file in three browsers on a Windows 10 computer.

These are the files I used

the changes form last time are probably the thing to look at

Since I have very little experience with the browser DOM, I suspect my next experiments will be fairly silly. I will just dump the results in Github.

Why is the callback mess done with recursion? Welp, I saw a way to add some more hell to that callback hell, but I'm trying to keep the daemon happy so does that make sense? It wants to do one task at a time, not all at once. The recursion makes sure that each task is done before the next begins, though I doubt it is the best way to do stuff like this.

I have only had the briefest look at your Python program but it seems to me you are re-inventing the wheel. Aren't you just creating a Python webserver?

...R

it seems to me you are re-inventing the wheel

hmm...

That is not what I call re-inventing, and as further evidence, I am fairly sure I did not learn how to do a web server.

I am not sure where it is going, so it may be a dead end. I looked at bottle and flask frameworks a little but they seem to be unnecessary (they are web servers). I want it to take a CGI style request and turn that into a serial command. An AVR will then take the command and send back a JSON reply, no beautification done by the daemon, just add the headers so the caller is happy, and it will get the pure JSON (I think JS might be able to turn that directly into an object without much fuss but have never tried such things)

Python has batteries included, lots of them. I am connecting Pythons WSGI battery to a serial port and then using another Python battery (the CGI's parse_qs) to piece together query_string elements that match with the commands I have been using on my AVR boards. In short, the daemon spits out the JSON that it was given from the AVR serial, not web pages. It adds headers, but I would not call that a web page. I guess the AVR could spit out HTML pages, and then it would be a web server... wow (but not what I want).

ron_sutherland:
I am not sure where it is going, so it may be a dead end. I looked at bottle and flask frameworks a little but they seem to be unnecessary (they are web servers).

Bottle is a lot simpler than Flask and has the very nice feature that the entire thing is in a single .py file. It is my "go to" for web related stuff. My initial explorations some years ago was with Ruby on Rails and then Ruby Sinatra which is simpler than Rails. When I switched to Python Bottle seemed a very close analogue to Sinatra.

I have no doubt that Bottle can do a lot of stuff that you don't need, but the way I see it if it can do what I want I will use it - the fact that it can do more would not put me off.

One thing that I discovered that Bottle cannot do "out of the box" is multiple overlapping requests - long polling, when the browser is prepared to wait a long time (seconds, minutes, maybe longer) for a response from the server so as to give the impression to the user of real-time updates when things change at the server end. However I got around that by using a different server engine (if that's the right phrase) in Bottle.

...R