10.6. Az irányítópult felépítése

A kamera JSON API-ja jó a curl számára, de a tulajdonos egy URL-re szeretné irányítani a telefonját, és látni valamit. Ez azt jelenti, hogy a kamera az API mellett HTML-t, JS-t és CSS-t is kiszolgál.

10.6.1. Statikus fájlok a fájlrendszerből

A microdot.Response.send_file() beolvas egy fájlt a kamera fájlrendszeréből, és visszaírja a kiterjesztésből kikövetkeztetett helyes Content-Type értékkel. Egyetlen mindent elkapó útvonal a routing tábla alján mindent elküld, ami nem illeszkedett egy API-útvonalra:

@app.get('/<path:filename>')
async def static(request, filename):
    if '..' in filename or filename.startswith('/'):
        abort(403)
    return Response.send_file('/sdcard/static/' + filename)

A <path:filename> konverter az előző oldalról a perjeleket tartalmazó szegmenseket illeszti – az app.js és a css/site.css is átjut. A kétsoros épelméjűségi ellenőrzés elutasít minden olyan elérési utat, amely .. vagy abszolút út segítségével próbál kiszökni a /sdcard/static/ mappából – enélkül egy kíváncsi kliens kiolvashatná a ../../boot.py vagy a /flash/secrets.txt fájlt. A send_file() maga semmilyen megtisztítást nem végez: bármilyen elérési utat is adsz át neki, megnyitja.

10.6.2. Az irányítópult fájljai

Három fájl a kamerán a /sdcard/static/ alatt:

<!-- /sdcard/static/index.html -->
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Backyard cam</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>Backyard cam</h1>
    <img id="stream" src="/stream.jpg">
    <label>Threshold:
      <input id="threshold" type="range" min="0" max="100">
      <span id="threshold-value"></span>
    </label>
    <h2>Recent events</h2>
    <ul id="events"></ul>
    <script src="app.js"></script>
  </body>
</html>
// /sdcard/static/app.js
const slider = document.getElementById('threshold');
const sliderValue = document.getElementById('threshold-value');

async function loadConfig() {
    const r = await fetch('/config');
    const cfg = await r.json();
    slider.value = cfg.threshold;
    sliderValue.textContent = cfg.threshold;
}
loadConfig();

slider.addEventListener('change', async () => {
    sliderValue.textContent = slider.value;
    await fetch('/config', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({threshold: parseInt(slider.value)}),
    });
});
/* /sdcard/static/style.css */
body { font-family: sans-serif; max-width: 720px; margin: 1em auto; }
img#stream { width: 100%; }

A HTML egyenesen egy <img> elembe tölti az MJPEG-streamet – a böngésző átlátszóan kezeli a multipart választ. A csúszka az oldal betöltésekor beolvassa a küszöbértéket a /config útvonalról, és minden változáskor visszaposztolja az új értéket.

10.6.3. MIME-típusok és tömörítés

A send_file() beolvassa a fájl kiterjesztését, és a microdot.Response.types_map alapján kiválaszt egy Content-Type értéket. A .html text/html lesz, a .js text/javascript lesz, a .css text/css lesz, a .jpg image/jpeg lesz. Az irányítópulthoz hozzáadott kiterjesztések (.svg, .ico) már regisztrálva vannak. Bármi ismeretlen alapértelmezésben application/octet-stream lesz – új leképezés hozzáadásához bővítsd ki egyszer a Response.types_map változót indításkor.

Az általad előre tömörített eszközökhöz (style.css.gz, app.js.gz) add át a compressed=True értéket, és a microdot a .gz fájlt szolgálja ki Content-Encoding: gzip fejléccel. A böngésző átlátszóan kicsomagolja, és minden oldalbetöltéskor néhány KB-ot megspórolsz.

A tulajdonos most megnyitja a http://yard-cam.local/index.html címet, és látja az élő képet, az aktuális küszöbértéket és egy üres eseménynaplót.