10.6. Kojelaudan rakentaminen

Kameran JSON-API on hyvä curl-työkalulle, mutta omistaja haluaa osoittaa puhelimella URL-osoitetta ja nähdä jotain. Se tarkoittaa HTML:n, JS:n ja CSS:n tarjoamista kameralta API:n rinnalla.

10.6.1. Staattiset tiedostot tiedostojärjestelmästä

microdot.Response.send_file() lukee tiedoston kameran tiedostojärjestelmästä ja kirjoittaa sen takaisin oikealla Content-Type-arvolla, joka päätellään tiedostopäätteestä. Yksi kaiken nappaava reitti reititystaulun pohjalla lähettää kaiken, mikä ei täsmännyt API-reittiin:

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

Edellisen sivun <path:filename>-muunnin täsmää segmentteihin, jotka sisältävät kauttaviivoja – sekä app.js että css/site.css menevät läpi. Kaksirivinen tarkistus hylkää minkä tahansa polun, joka yrittää paeta hakemistosta /sdcard/static/ käyttäen .. tai absoluuttista polkua – ilman sitä utelias asiakas voisi lukea tiedoston ../../boot.py tai /flash/secrets.txt. send_file() itse ei tee mitään puhdistusta: se avaa minkä tahansa polun, jonka sille annat.

10.6.2. Kojelaudan tiedostot

Kolme tiedostoa kameralla hakemiston /sdcard/static/ alla:

<!-- /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%; }

HTML lataa MJPEG-virran suoraan <img>-elementtiin – selain käsittelee monikenttäisen vastauksen läpinäkyvästi. Liukusäädin lukee kynnysarvon reitiltä /config sivun latautuessa ja lähettää uuden arvon takaisin jokaisella muutoksella.

10.6.3. MIME-tyypit ja pakkaus

send_file() lukee tiedoston päätteen ja valitsee Content-Type-arvon kohteesta microdot.Response.types_map. .html muuttuu arvoksi text/html, .js arvoksi text/javascript, .css arvoksi text/css, .jpg arvoksi image/jpeg. Päätteet, jotka olet lisännyt kojelautaasi (.svg, .ico), on jo rekisteröity. Mikä tahansa tuntematon saa oletuksena arvon application/octet-stream – uuden vastaavuuden lisäämiseksi laajenna Response.types_map kerran käynnistyksen yhteydessä.

Resursseille, jotka olet pakannut etukäteen (style.css.gz, app.js.gz), välitä compressed=True, jolloin microdot tarjoaa .gz-tiedoston otsakkeella Content-Encoding: gzip. Selain purkaa sen läpinäkyvästi ja säästät muutaman kilotavun jokaisella sivun latauksella.

Omistaja avaa nyt osoitteen http://yard-cam.local/index.html ja näkee reaaliaikaisen syötteen, nykyisen kynnysarvon ja tyhjän tapahtumalokin.