Simple Kiosk Framework in Python

ftrias

Fernando Trias

Posted on April 10, 2020

Simple Kiosk Framework in Python

Summary: jyserver creates HTML-based app front ends and kiosks where Python and Javascript seamlessly exchange data and execute functions. 

For example, using jyserver, a server running Python can do this:

self.js.document.getElementById("act").innerHTML = "Jump"
Enter fullscreen mode Exit fullscreen mode

And it will automatically convert the expression to Javascript, send it to the browser and execute it causing the page to update. It can also work in reverse as in this HML snip:

<button id="b1" onclick="server.increment()">Increase</button>
Enter fullscreen mode Exit fullscreen mode

When the button is clicked, increment() is executed on the server. No additional code is needed. The framework provides the library that makes this possible.

HTML->JS->Python

Keep it simple

Traditionally, Python web frameworks like Flask or Django are a complicated interplay of HTML, CCS, Javascript and Python. Each page update involves some HTML, some Javascript, marshaling parameters, async communication to the server, some crunching in Python and a trip back to Javascript. Or some variation on that.

Seems like a lot of work if you just want to create a simple a front end for an application. 

But what if all of that complexity was hidden and the language syntax dynamically provided the plumbing? Code Javascript in Python? 

HTML <-> JS <-> Python

What does that mean for your code? An example will illuminate. Let's say your page is very simple. You want to create a counter and a button to increase the count. You want everything to be controlled by the server. Your index.html file would look like this:

<html><body>
<p id="count">0</p>
<button id="b1" onclick="server.increment()">Increase</button>
</body><html>
Enter fullscreen mode Exit fullscreen mode

Increment app

Your server needs to change the text for count and respond to increment(). Using jyserver, your server code would look like this:

from jyserver import Client, Server
class App(Client):
    def __init__(self):
        self.count = 0
    def increment(self):
        self.count += 1
        self.js.document.getElementById("count").innerHTML = self.count
httpd = Server(App)
print("serving at port", httpd.port)
httpd.start()
Enter fullscreen mode Exit fullscreen mode

That's all the code you need. When the browser calls server.increment(), it causes App.increment() to execute on the server, which then changes the DOM in real-time by updating innerHTML for count.

One of the principal benefits of this approach is that it makes it easy to put program logic in a single place. If you want the server to control things, you use the self.js object to change the DOM and execute statements on the browser. On the other hand, if you want to control everything from the client, you can write the logic in Javascript and use the server object to execute specific commands or query variables on the server.

How is this accomplished? The secret sauce is in the server object in the browser and the self.js object in Python.

This is what happens behind the scenes.

  1. After calling httpd.start(), the server will listen for new http requests.

  2. When "/" is requested, jyserver will read index.html and insert special Javascript code into the HTML that enables communication before sending it to the browser. This code creates the server Proxy object.

  3. This injected code will cause the browser to send an asynchronous http request to the server asking for new commands for the browser to execute. Then it waits for a response in the background.

  4. When the user clicks on the button b1, the server Proxy object is called. It will extract the method name--in this case increment--and then make an http request to the server to execute that statement. 

  5. The server will receive this http request, look at the App class, find a method with that name and execute it.

  6. The executed method increment() first increases the variable count. Then it begins building a Javascript command by using the special self.js command. self.js uses Python's dynamic language features __getattr__, __setattr__, etc. to build Javascript syntax on the fly. 

  7. When this "dynamic" statement get assigned a value (in our case self.count), it will get converted to Javascript and sent to the browser, which has been waiting for new commands in step 3. The statement will look like: document.getElementById("count").innerHTML = 1

  8. The browser will get the statement, evaluate it and return the results to the server. Then the browser will query for new commands in the background.

It seems complicated but this process usually takes less than a 0.01 seconds. If there are multiple statements to execute, they get queued and processed together, which cuts back on the back-and-forth chatter.

As is typical in web applications, all communication is initiated by the browser asynchronously. The server keeps a queue of pending commands and matches results as they are returned.

Flow

The exception to asynchronous requests is when the browser initiates a server call. In that case, the browser waits for the server to respond when execution is complete. Often, this wait is not necessary, but it is used to make flow more predictable and avoid out-of-order execution.

The components

There are three main parts to the system.

  1. HTTP server

  2. Javascript "plumbing" and server object

  3. Python "plumbing" and self.js object

The HTTP server

The jyserver module leverages Python's http.server.ThreadingTCPServer to service requests. On the plus side, this means it's pretty robust and there are no extra dependencies. On the negative side, this is a very simple server that is insecure out-of-the-box. jyserver adds some security by isolating instances of the Client application by means of a unique session id so that different sessions cannot access each other's data. But given the dynamic nature of the execution, it's still possible for malicious clients to cause havoc. However, since the main purpose of jyserver is to create application front ends in controlled environments, this is not a big problem.

For added security, when the server starts, it will listen on a specific port and interface. This means you can restrict connections to only accept local connections and reject network connections. This makes it ideal for kiosks.

When the server receives a web page request, it will first look for a matching method name in the Client application and execute it. If there is no match, it will look for a file with that name and send it to the browser. In this way, it works similarly to most web servers.

In addition, the server will run a method named main if it is available. Otherwise, it will loop forever waiting for requests.

The server object

The server object lives in the browser and is used by Javascript to execute commands on the server. Basically, the server object is a proxy for the Client app. It can call methods, query values and set values. For example, the following code will call reset() on the server for every click.

<button id="b1" onclick="server.reset(0)">Zero</button>
Enter fullscreen mode Exit fullscreen mode

In addition, setting a value on the server's Client object is possible:

<button id="b1" onclick="server.count=0">Zero</button>
Enter fullscreen mode Exit fullscreen mode

You also can run methods and get return values.

alert(server.getresult(125, 99))
Enter fullscreen mode Exit fullscreen mode

The self.js object

Python code uses the self.js object to communicate with the browser. Let's say you have a function in Javascript on the browser.

var factor = 1.3
function adjust(value) { return value * factor; }
Enter fullscreen mode Exit fullscreen mode

This can be run from the Python server side using:

result = self.js.adjust(i)
print("Factor is", self.js.factor, "2 x result is", 2 * result)
Enter fullscreen mode Exit fullscreen mode

To change values, just set them in code.

self.js.factor = 2.3
self.js.data = {"a":15.4, "b":12.7, "c":[5,4,6]}
Enter fullscreen mode Exit fullscreen mode

The last statement will convert the structure to a Javascript dictionary. This data conversion is accomplished via the json module in Python and the JSON module in Javascript.

Just to make life even easier, the self.js object has a special shorthand for querying elements by id using the keyword dom. These two statements are the same:

self.js.document.getElementById("count").innerHTML = 10
self.js.dom.count.innerHTML = 10
Enter fullscreen mode Exit fullscreen mode

A more complex example

To illustrate a few more features, we'll create a stopwatch app. The design is to run a function on the server that updates the time on the HTML page every so often. We also provide two buttons: one to reset to zero and other to pause the updates.

The first thing to note is that instead of serving out an index.html file we embed the HTML in the file itself. That way there are no external dependencies.

from jyserver import Server, Client
import time
class App(Client):
    def __init__(self):
        self.html = """
<p id="time">WHEN</p>
<button id="b1" onclick="server.reset()">Reset</button>
<button id="b2" onclick="server.stop()">Pause</button>
"""
        self.running = True
Enter fullscreen mode Exit fullscreen mode

The class will need to define the reset() and the stop() methods. Just for kicks, we'll be dynamically changing the Pause callback.

    def reset(self):
        self.start0 = time.time()
        self.js.dom.time.innerHTML = "{:.1f}".format(0)
    def stop(self):
        self.running = False
        self.js.dom.b2.innerHTML = "Restart"
        self.js.dom.b2.onclick = self.restart
    def restart(self):
        self.running = True
        self.js.dom.b2.innerHTML = "Pause"
        self.js.dom.b2.onclick = self.stop
Enter fullscreen mode Exit fullscreen mode

Notice that when you click Stop, the stop() method gets called, which changes the text and then modifies the onclick callback of the button. The next click will then run restart(), which will then change the text and callback.

Next, we need a main function that gets executed for every new session. In our case, the program runs for 1000 iterations and then terminates. When it ends, the server will also shut down. Naturally, you can convert this to an infinite loop and the program will never terminate. Or, if the function is omitted, then the server just listens for connections indefinitely.

    def main(self):
        self.start0 = time.time()
        for i in range(1000):
            if self.running:
                t = "{:.1f}".format(time.time() - self.start0)
                self.js.dom.time.innerHTML = t
                time.sleep(0.1)
Enter fullscreen mode Exit fullscreen mode

Lastly, we start the server.

httpd = Server(App)
print("serving at port", httpd.port)
httpd.start()
Enter fullscreen mode Exit fullscreen mode

App

Installation and source code

jyserver is available in pip or conda.

pip install jyserver
Enter fullscreen mode Exit fullscreen mode

The source code is found in the Github repository jyserver

GitHub logo ftrias / jyserver

Jyserver Web Framework with Pythonic Javascript Syntax

Future directions

Since jyserver's aim is to simplify the creation of web-based front ends for apps and kiosks, it lacks a lot of the bells and whistles found in more complex frameworks such as Flask or Django, which are aimed at creating web sites. Specifically, jyserver lacks user logins, templates, substitutions and many other features. This can be remedied in two ways. First, existing frameworks can use the dynamic programming tricks and techniques used by jyserver to further simplify their APIs. Or, jyserver can grow to encompass more functionality, while still retaining the philosophy of simplicity.

About the author

Fernando "Fen" Trias is a serial entrepreneur, CEO of Vindor Music and an avid Python and C++ coder specializing in data science, embedded development and cybersecurity in the Boston areas. He is the author is jyserver, PyEmbedC, TeensyThreads and other open source projects.

💖 💪 🙅 🚩
ftrias
Fernando Trias

Posted on April 10, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related