1

I'm have written a python script on a Raspberry Pi that controls a large and somewhat complex piece of hardware. Currently the use interface is a python console. I run the python program, and the can enter commands from the console with input("> ").

Now I want to add a web interface to this program, and I'm trying to figure out the right way to do that. I've written a basic web UI in flask, but I haven't found a good way to connect the flask interface with the main script. Because the server controls a single piece of hardware, the main object (handling the hardware control) is only instantiated once. In addition, there is significant hardware configuration that must happen each time the script is run (each time the main object is created.)

Flask seems to create a new thread for each client session, and only persist variable across that session. How can I link flask events (i.e, web user presses a button) to a method in the main object?

Am I using the wrong tool for the job? What is the right tool? What is the correct way to do this? Alternatively, what is a jank way to do this?

EDIT: The linked questions are similar, and led me down the path to the answer below, but they really didn't answer the question, so much as point to some code that solved that specific example. I think the below answer is far more helpful. (Of course I'm a bit biased; I wrote it)

I found a solution, so I'll share it here. (I guess I can't answer my own questions?)

The Flask Web App system (and all WSGI web apps?) rely on the principle that the web app can be executed in a totally fresh environment, without needing to be passed objects on creation. I guess this makes sense for Web Apps, but it adds a ton of annoying complexity to Web UIs, that is, web interfaces purpose built to interface a single instance of a larger program. As a hardware/solutions engineer, I tend to need a Web UI to control a single piece of hardware. If there is something better than flask for this purpose please leave a comment. But flask is nice, and I now know its possible to use for this purpose.

Flask Web Apps are not designed to do the "heavy lifting" themselves. They maintain very limited state (i.e. every request triggers a fresh context), and as mentioned can't be passed references to other objects. In a proper Web App a full stack developer would connect Flask to a database server and other such WebDev-y systems. For controlling hardware, our goal is to trigger the execution of some arbitrary methods in another python process (quite possibly a separately executed process).

In python a convenient way to achieve this execution is the multiprocessing.managers module. Managers are a tool that allows you to easily construct Proxies, which link objects across processes. If you have an object bar = Bar() you can produce a <AutoProxy[get_bar]> proxy, that allows you to manipulate the original bar object from far away. Far away can be in a child process, on another computer across the internet. Here's an example.

server.py:

import multiprocessing.managers as m
import logging
logger = logging.getLogger()

class Bar: #First we setup a dummy class for this example.
    def __init__(self):
        self.text = ""
    def read(self):
        return str(self.text)
    def write(self, string):
        self.text = str(string)
        logger.error("Wrote!")
        logger.error(string)

bar = Bar() #On the server side we create an instance: this is the object we want to share to other processes.

m.BaseManager.register('get_bar', callable=lambda:bar) #then we register a 'get' function in the manager,
# to retrieve our object from afar. the lambda:bar is just shorthand for a function that returns the bar object.
manager = m.BaseManager(address=('', 50000), authkey=b'abc') #Then we setup the server on port 50000, with a password.
server = manager.get_server() 
server.serve_forever() #Then we start the server!

client.py:

import multiprocessing.managers as m
import logging

logger = logging.getLogger()

m.BaseManager.register('get_bar') #We register this so that the Manager knows it's a valid method
manager = m.BaseManager(address=('', 50000), authkey=b'abc') #Then we setup the server connection
manager.connect() #and connect!
bar = manager.get_bar() # now we can use our 'get' method to retrieve a Proxy of the object.

So there are a few interesting things to note here. First, we can get_bar() from as many clients as we want, and they'll all point back to the same bar. Second, we can call methods in bar, read() and write(), from a client, without having the Bar class on hand. Pretty neat.

So how to use this? If you have a console-enabled program, first split it into two parts, the console, and the functionality it controls. Have that functionality boxed up in a handful of objects that together comprise the instance of the application. Modify server.py above to instantiate those objects, and host them out with get methods in a Manager. Then tweak your console interface to connect like client.py and use the proxies instead of the actual objects. Finally, set up a flask app that also connects to the server and provides a web interface.

Now you're a Bona-Fide Web Developer! RIP.

Community
  • 1
  • 1
Tim Vrakas
  • 121
  • 6
  • Flask doesn't create a new thread for each session. – Daniel Roseman Aug 06 '18 at 18:53
  • It doesn't? How does it handle web apps with more than one user? How can I pass a single object into all client contexts? – Tim Vrakas Aug 06 '18 at 19:03
  • Flask - like all WSGI apps - doesn't manage threads at all, or even processes. It just uses whatever is spun up by the web server itself. Each of those threads or processes is long-lived and can last for many many requests. – Daniel Roseman Aug 06 '18 at 19:05
  • But am I correct in saying that each connected client/session/user is _isolated_ somehow, if not in separate threads, then some sort of separate context? – Tim Vrakas Aug 06 '18 at 19:16
  • For the record, the Flask default web server very much does create a new thread for each session. I can see it with `ps`. – Tim Vrakas Aug 07 '18 at 06:41
  • You certainly can answer your own question, but if it is a duplicate you should put your answer in the other question. – Dour High Arch Aug 07 '18 at 16:11

0 Answers0