Source code for web

'''
Web-Interface for the display.

Start with

    $ flask --app web.py run --host=0.0.0.0

Use FLASK_ENV=development for a development server.

The webserver provides an address at location '/px/<x>/<y>/<on or off>' with
coordinate values for <x> and <y> and a status value for <on or off> which
must "on" or "off". You can send a GET or POST request to this addres.

For instance to turn pixel at location (2,3) on you can use

    $ curl HOSTNAME/px/2/3/on

Route /page can be read with method GET or changed with method POST.

/page (GET) returns a list of 1s and 0s that represent the current display.

/page (POST) expects a parameter data of 1s and 0s to change the current 
display. Entries with x are ignored and allow for partial updates of the
display.

/display (GET) returns the current display as JSON.

/display (POST) expects a JSON with a single image or a list of images to 
display. Each image is a dictionary with the keys "pixels" and "duration_ms".
The value of "pixels" is a string of 0s and 1s. The value of "duration_ms" is
the duration of the image in milliseconds. For instance::    

        {
            "images": [
                {   
                    "pixels": "000011x0...",  # values of x are ignored
                    "duration_ms": 1000
                },
                ...
            ]            
        }

To show text, send a json with the text::
    
        {
            "text": "Hello World",
            "scrolling": true,     (optional, default false)
            "fps": 10,             (optional, default 10)
            "duration_ms": 1000    (optional, default 1000)
        }

'''

from flask import Flask, request, render_template
import displayprovider
import configuration
import flipdotfont
import time

app = Flask(__name__)

[docs] def get_display(): if 'display' not in app.config: app.config['display'] = displayprovider.get_display(width=configuration.WIDTH, height=configuration.HEIGHT) return app.config['display']
[docs] def get_buffer(): if 'buffer' not in app.config: buffersize = get_display().width * get_display().height app.config['buffer'] = [False] * buffersize return app.config['buffer']
[docs] def set_px(x, y, val): get_display().px(x, y, val) # update buffer buffer = get_buffer() buffer[y * get_display().width + x] = val
[docs] @app.route('/') def route_index(): display = get_display() dimension = str(display.width) + ' x ' + str(display.height) return render_template( 'index.html', dimension=dimension)
[docs] @app.route('/px/<int:x>/<int:y>/<string:onoff>', methods=['GET', 'POST']) def route_px(x, y, onoff): display = get_display() if not(0 <= x < display.width): return 'x too big', 400 if not(0 <= y < display.height): return 'y too big', 400 if onoff == 'on': set_px(x, y, True) display.show() elif onoff == 'off': set_px(x, y, False) display.show() else: return 'value must be "on" or "off"', 400 return 'ok', 200
def _display_text(txt, scrolling=False, fps=10, duration_ms=1000): "Display text on display." display = get_display() scroller = flipdotfont.TextScroller(display, txt, flipdotfont.small_font()) if duration_ms > configuration.web_max_show_time_ms: return "duration too long", 400 if fps < 0: return "fps must be positive", 400 if scrolling: start = time.time() while True: scroller.scrolltext() display.show() time.sleep(1/fps) if time.time() - start > duration_ms / 1000: break else: display.show() return "ok", 200
[docs] @app.route('/page', methods=['POST']) def route_page_post(): data = request.get_data(as_text=True) return _show(data)
def _show(data): "Show data (string of 0s and 1s) on the display)." display = get_display() x, y = 0, 0 for d in data: if d == '1': onoff = True elif d == '0': onoff = False elif d in ['x', 'X']: onoff = None else: return 'data must be 0 or 1 or x', 400 if onoff is not None: if 0 <= x < display.width and 0 <= y < display.height: set_px(x, y, onoff) else: return 'image too big', 400 if x+1 < display.width: x += 1 else: y += 1 x = 0 display.show() return 'ok', 200 def _show_sequence(list_of_images): "Show a sequence of images." showtime_ms = 0 for image in list_of_images: showtime_ms += image["duration_ms"] if showtime_ms > configuration.web_max_show_time_ms: return "overall duration_ms too long", 400 desc, code = _show(image["pixels"]) if code != 200: return "Error in image: " + desc, code time.sleep(image["duration_ms"] / 1000) return "ok", 200
[docs] @app.route('/page', methods=['GET']) def route_page_get(): "Return the current display as string of 0s and 1s." response = '' for b in get_buffer(): response += '1' if b else '0' return response, 200
[docs] @app.route("/display", methods=['GET']) def route_display_get(): "Return the current display (in JSON): width, height, data" pixels = "" for b in get_buffer(): pixels += "1" if b else "0" js = { "width": get_display().width, "height": get_display().height, "pixels": pixels } return js, 200
[docs] @app.route("/display", methods=['POST']) def route_display_post(): """ Send a new display. Expecting json of the form:: { "pixels": "00001100..." } To send a sequence of images, send a list of images including duration in ms:: { "images": [ { "pixels": "00001100...", "duration_ms": 1000 }, ... ] } To show text, send a json with the text:: { "text": "Hello World", "scrolling": true, (optional, default false) "fps": 10, (optional, default 10) "duration_ms": 1000 (optional, default 1000) } To set the led brightness, send a json with the led value:: { "led": "on" # or "off" } """ data = request.get_json() if "pixels" in data: return _show(data["pixels"]) if "images" in data: return _show_sequence(data["images"]) if "text" in data: return _display_text(data["text"], data.get("scrolling", False), data.get("fps", 10), data.get("duration_ms", 1000)) if "led" in data: return _set_led(data["led"]) return "no text, pixels, led brightness or images in json", 400
def _set_led(onoff): "Set led on or off." if onoff not in ["on", "off"]: return "led must be on or off", 400 get_display().led(onoff == "on") return "ok", 200
[docs] def test_display_get(): client = app.test_client() response = client.get("/display") assert response.status_code == 200 js = response.get_json() assert "width" in js assert int(js["width"]) == configuration.WIDTH assert "height" in js assert int(js["height"]) == configuration.HEIGHT for d in js["pixels"]: assert d in ["0", "1"]
[docs] def test_display_post(): client = app.test_client() pixels = "00001100" response = client.post("/display", json={"pixels": pixels}) assert response.status_code == 200 js = client.get("/display").json assert js["pixels"][0:len(pixels)] == pixels # sequence of images images = [] for pxs in ["00001100", "11110000", "00000000"]: images.append({"pixels": pxs, "duration_ms": 100}) response = client.post("/display", json={"images": images}) assert response.status_code == 200 # too many images old_config_value = configuration.web_max_show_time_ms configuration.web_max_show_time_ms = 100 images = [] for pxs in ["00001100", "11110000", "00000000"]: images.append({"pixels": pxs, "duration_ms": 1000}) response = client.post("/display", json={"images": images}) assert response.status_code == 400, response.get_data(as_text=True) configuration.web_max_show_time_ms = old_config_value # show text response = client.post("/display", json={"text": "Hello World"}) assert response.status_code == 200, response.get_data(as_text=True) response = client.post("/display", json={ "text": "Hello World", "scrolling": True, "fps": 10, "duration_ms": 1000}) assert response.status_code == 200, response.get_data(as_text=True) # set led brightness for onoff in ("on", "off"): response = client.post("/display", json={"led": onoff}) assert response.status_code == 200, response.get_data(as_text=True)
[docs] def test_px(): client = app.test_client() for onoff in ['on', 'off']: for x,y in [(3,5), (11,9)]: resp = client.get('/px/{x}/{y}/{onoff}'.format(x=x, y=y, onoff=onoff)) assert resp.status_code == 200 assert resp.data == b'ok' resp = client.get('px/1000/1000/on') assert resp.status_code == 400
[docs] def test_page(): client = app.test_client() resp = client.post('/page', data='000abc') assert resp.status_code == 400 resp = client.post('/page', data='000000') assert resp.status_code == 200 resp = client.get('/page') assert resp.data[:9] == b'000000000', resp.data resp = client.post('/page', data='110110110') assert resp.status_code == 200 resp = client.get('/page') assert resp.data[:9] == b'110110110' assert resp.data[-9:] == b'000000000' resp = client.post('/page', data='000xxxXXx') assert resp.status_code == 200 resp = client.get('/page') assert resp.data[:9] == b'000110110' assert resp.data[-9:] == b'000000000'
[docs] def test_index(): client = app.test_client() resp = client.get('/') assert resp.status_code == 200
[docs] def test_plasmademo(): from math import sin, cos client = app.test_client() display = get_display() ticks = 0 frames = 100 for _ in range(frames): payload = '' for y in range(display.height): for x in range(display.width): ticks += 0.001 s = sin(ticks / 50.0) * 2.0 + 6.0 v = 0.3 + (0.3 * sin((x * s) + ticks / 4.0) * cos((y * s) + ticks / 4.0)) show_px = v > 0.3 payload += '1' if show_px else '0' client.post('/page', data={'data': payload})
[docs] def test_plasmademo_remote(): from math import sin, cos from urllib.request import urlopen import os if 'FFF_IP' in os.environ: host = 'http://' + os.environ['FFF_IP'] else: print('No Public ip given in FFF_IP. Ignoring test.') return width, height = 28, 13 ticks = 0 frames = 100 for _ in range(frames): payload = '' for y in range(height): for x in range(width): ticks += 0.005 s = sin(ticks / 50.0) * 2.0 + 6.0 v = 0.3 + (0.3 * sin((x * s) + ticks / 4.0) * cos((y * s) + ticks / 4.0)) show_px = v > 0.3 payload += '1' if show_px else '0' urlopen(host + '/page', data=('data=' + payload).encode())