10.2. Returning a snapshot

A status endpoint is fine, but the reason the cam exists is the lens. Add an endpoint that returns the JPEG of whatever the sensor is looking at right now.

import csi
from microdot import Response

csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QVGA)

@app.get('/snapshot.jpg')
async def snapshot(request):
    img = csi0.snapshot().compress(quality=85)
    return Response(
        body=img.bytearray(),
        headers={'Content-Type': 'image/jpeg'},
    )

Hit http://<cam-ip>/snapshot.jpg from a browser and a JPEG of the current view fills the tab. Refresh and you get a fresh one.

10.2.1. The Response object

A dict-returning handler lets microdot do the rest. JPEG bytes need the long form: a microdot.Response constructed explicitly. The body argument takes any bytes-like value – the camera’s image.Image buffer is exposed via bytearray(), so the same buffer the sensor wrote into goes straight to the socket.

Content-Type: image/jpeg is what tells the browser to render the body as an image. Without it the browser would try to display the JPEG bytes as text and you’d see a screenful of garbage.

image.Image.compress() runs JPEG encoding on the existing image buffer in place and returns the same image (now JPEG-formatted) so its bytes can be sent as-is. quality=85 is the usual default – high enough that the picture is sharp, low enough that the file fits through a slow link.

10.2.2. Capture blocks the loop

csi.CSI.snapshot() waits for the sensor to finish exposing and DMA’ing a frame before it returns. Inside an async handler that means the event loop stalls for the duration of the exposure – ten, twenty, fifty milliseconds depending on lighting. With one client asking one route at a time this is invisible; with multiple clients, or a capture coroutine running alongside, it would block everything else.

A non-blocking variant of snapshot() exists for the multi-coroutine case (blocking=False returns the next ready frame or None). For one snapshot per request, the default blocking call is fine.

The owner can now poke a URL and get a fresh frame.