# Mesmera ExpOS

In opdracht · 2025–2026 · Scape Agency · tags: server, hardware

URL: https://jim.vanhekke.nl/mesmera/  
Bijgewerkt: 2026-10-07

Een besturingssysteem voor musea en tentoonstellingen: de server onder de installaties en alle apparaten in de zaal.

Een installatie die maanden in een zaal staat heeft meer nodig dan losse scripts. Mesmera ExpOS beheert een tentoonstelling en alles wat eraan hangt: sensoren, schermen, luidsprekers, microcontrollers. Een apparaat meldt zich aan, de server weet wat het is en stuurt het wat het moet doen. Talking Books, Posse en Emanon draaien erop.

Ik heb de eerste versie opgezet en de apparaatkant gebouwd. ExpOS is een Django-applicatie met Channels; elk apparaat houdt een websocket open en de server stuurt opdrachten terug.  Alle verbonden apparaten spreken diezelfde taal; een Pico met een sensor, een Pi met een bluetooth-scanner, of een NUC met een camera. Apparaten melden zich met hun MAC-adres op een afgesloten netwerk aan en krijgen in ExpOS een rol toegenwezen.
Alle modules zoals de websocket-laag en het actieregister zijn met het team losgetrokken tot herbruikbare Python-pakketten. De server staat lokaal in de zaal op een Raspberry Pi 5 of NUC of iedere willekeurige andere computer. Docker Compose met Postgres en Redis, geautomatiseerde tests, releases via versie-tags.

## Colofon

- **Studio:** Scape Agency (Mesmera is het merk van Scape Agency voor tentoonstellingstechniek)
- **Mijn aandeel:** eerste versie, apparaatprotocol, websocket-laag, e-ink-rendering, positiebepaling
- **Draait onder:** Talking Books, Posse, Emanon

## Code

Uit de positiebepaling: een beacon meet zijn afstand tot vaste ankers in de zaal en de server zoekt het punt dat daar het best bij past. Metingen die ontbreken of nergens op slaan tellen niet mee.

```python
def trilaterate_3d_least_squares(
    anchors, distances, bounds=((0, 0, 0), (np.inf, np.inf, np.inf))
):
    anchors = np.array(anchors, dtype=float)
    distances = np.array(distances, dtype=float)

    valid = np.isfinite(distances) & (distances > 0)
    anchors, distances = anchors[valid], distances[valid]
    if len(anchors) < 4:
        raise ValueError(f"At least 4 valid distance readings required, got {len(anchors)}")

    def residuals(p):
        return np.linalg.norm(anchors - p, axis=1) - distances

    result = least_squares(
        residuals,
        np.mean(anchors, axis=0),  # start in the middle of the anchors
        bounds=bounds,
        method="trf",
        loss="soft_l1",  # robust to outliers
        f_scale=0.1,
    )
    return result.x, result.cost, result.success
```

## Beelden

- ![De beheerpagina van de ExpOS-server](https://jim.vanhekke.nl/mesmera/images/mesmera-01-server.webp)
- ![Alle apparaten, met hun plek in de zaal](https://jim.vanhekke.nl/mesmera/images/mesmera-02-apparaten.webp)
- ![Space Manager: ankers en apparaten in een 3D-model van de zaal](https://jim.vanhekke.nl/mesmera/images/mesmera-03-space-manager-featured-image.webp)
- ![Fleet Dashboard: de staat van alle apparaten, live; wie online is, wie hapert en waarom](https://jim.vanhekke.nl/mesmera/images/mesmera-04-fleet.webp)
