Skip to content

The table tennis landscape -- today and the target

One page for the whole thing: the router, the box, the services on it, the boards and screens around it, and who talks to whom on which port. Solid lines and boxes run today, dashed ones are planned. The details stay where they are: the site, the cluster and the two name worlds in architecture.md, the box step by step in clusters/sthings-edge/README.md, the rules of each service in its own ADRs.

Update this page in the same pull request that changes what it shows.

Context

flowchart TB
  internet((Internet<br/>visitor Wi-Fi / LTE))
  hetzner["Hetzner DNS<br/>Let's Encrypt"]

  subgraph opal["GL.iNet Opal (GL-SFT1200) -- 192.168.8.1"]
    dns["dnsmasq<br/>DHCP + DNS"]
    portal["captive portal<br/>(planned, #431)"]
  end

  subgraph box["LattePanda Mu -- 192.168.8.10, k3s + Cilium + Flux"]
    gw["edge-gateway<br/>*.edge.sthings.lab · edge CA"]
    play["edge-play-gateway<br/>*.sthings-edge.com · Let's Encrypt"]
    zw["zaehlwerk"]
    sp["schmetterpause<br/>+ CNPG Postgres"]
    hr["homerun2<br/>redis · pitchers · catchers"]
    bao["OpenBao<br/>PKI + ACME<br/>(lab ESP mock today)"]
    minio["MinIO<br/>backups"]
    sink["log sink"]
  end

  subgraph table["At the table"]
    piezo["piezo ESP32"]
    buttons["buttons + hub<br/>ESP-NOW"]
    matrix["LED matrix<br/>Pi 3B+, 64x64"]
    wled["WLED<br/>ESP32, 36 LEDs"]
    phones["players' phones"]
  end

  internet --- opal
  opal --- box
  box -. DNS-01, outbound only .-> hetzner
  phones --> portal
  portal -.-> play
  phones --> play
  piezo --> gw
  buttons -.-> gw
  gw --> zw
  play --> sp
  play --> zw
  zw <-- "via play gateway" --> sp
  zw --> hr
  hr --> matrix
  hr --> wled
  sp --> minio
  piezo -. diagnostics .-> sink

  classDef planned stroke-dasharray: 5 5
  class buttons,portal planned
  • The box is the only server. Everything that keeps state runs on it; the router is the network and the name service, nothing more.
  • Two gateways, two name worlds. Devices and admin UIs use *.edge.sthings.lab with the edge CA; players use *.sthings-edge.com with a publicly trusted certificate, so a phone needs nothing installed.
  • zaehlwerk owns the running match, schmetterpause owns players, results and ratings. They talk over HTTPS in both directions -- players and pairings to zaehlwerk, won matches and phone check-ins back -- and deliberately through the players' names: the same links then work on a phone, and the Let's Encrypt certificate is in trust-manager's bundle anyway.
  • homerun2 is the bus to the lights. zaehlwerk pitches onto the tabletennis stream; the led-catcher and the light-catcher pick it up. No table tennis logic in either catcher.

Ports and protocols

flowchart TB
  piezo["piezo ESP32"] -- "HTTPS 443<br/>POST /ingest/piezo" --> gw
  hub["hub ESP32<br/>(planned)"] -. "HTTPS 443<br/>POST /ingest/button" .-> gw
  btn["button ESP32-C3<br/>(planned)"] -. "ESP-NOW" .-> hub
  piezo -- "UDP 9000<br/>JSON lines" --> sink["log sink<br/>192.168.8.23"]
  mock["ESP mock (lab)"] -- "ACME, HTTP-01" --> bao["OpenBao"]

  phone["phone / browser"] -- "HTTPS 443" --> play["edge-play-gateway<br/>192.168.8.21"]
  admin["laptop / kiosk"] -- "HTTPS 443" --> gw["edge-gateway<br/>192.168.8.20"]

  gw -- "HTTP 8080" --> zw["zaehlwerk"]
  play -- "HTTP 8080" --> zw
  play -- "HTTP" --> sp["schmetterpause"]
  zw -- "HTTPS, players' name<br/>/api/players, /api/results" --> play
  sp -- "HTTPS, players' name<br/>POST /checkin/claim" --> play
  zw -- "SSE<br/>/live/stream" --> phone
  zw -- "HTTP<br/>omni-pitcher" --> redis["redis-stack<br/>6379"]
  redis -- "stream tabletennis" --> led["led-catcher"]
  redis -- "stream tabletennis" --> light["light-catcher-tabletennis"]
  redis -- "TCP 6379<br/>192.168.8.22" --> matrix["LED matrix Pi"]
  light -- "HTTP JSON API" --> wled["WLED<br/>192.168.8.31"]
  sp -- "5432" --> pg["CNPG Postgres"]
  pg -- "S3, WAL + base backups" --> minio["MinIO"]
From To Port / protocol Address TLS
piezo ESP32 zaehlwerk /ingest/piezo HTTPS 443 zaehlwerk.edge.sthings.lab → .20 edge root CA, checked on the board
hub ESP32 (planned) zaehlwerk /ingest/button HTTPS 443 zaehlwerk.edge.sthings.lab → .20 edge root CA
button ESP32-C3 (planned) hub ESP-NOW (no IP) hub MAC, same Wi-Fi channel --
piezo ESP32 log sink UDP 9000, JSON lines 192.168.8.23 none (diagnostics only)
ESP mock (lab only) OpenBao ACME HTTPS, HTTP-01 openbao.<edge domain> edge root CA
phones schmetterpause HTTPS 443 schmetterpause.sthings-edge.com → .21 (router answers locally) Let's Encrypt
phones zaehlwerk page, SSE /live/stream HTTPS 443 zaehlwerk.sthings-edge.com → .21 Let's Encrypt
admin, kiosk zaehlwerk, homerun2 UIs, OpenBao, MinIO HTTPS 443 *.edge.sthings.lab → .20 edge root CA
zaehlwerk schmetterpause /api HTTPS 443, bearer token schmetterpause.sthings-edge.com → .21 (pods resolve through the Opal) Let's Encrypt, in trust-manager's bundle
schmetterpause zaehlwerk /checkin/claim HTTPS 443, bearer token zaehlwerk.sthings-edge.com → .21 Let's Encrypt
zaehlwerk homerun2 HTTP to the omni-pitcher, or Redis 6379 in-cluster --
LED matrix Pi redis-stack TCP 6379, password 192.168.8.22 --
light-catcher-tabletennis WLED HTTP JSON API 192.168.8.31 --
schmetterpause Postgres (CNPG) TCP 5432 in-cluster CNPG's own
CNPG MinIO S3 in-cluster --
cert-manager Hetzner DNS, Let's Encrypt HTTPS 443, outbound only public public

The fixed addresses all sit outside the Opal's DHCP range .100-.249: .1 router, .10 box, .20 gateway, .21 players, .22 Redis for the matrix, .23 log sink, .30 LED matrix Pi, .31 WLED.

Two networks, two roles of the box

flowchart TB
  subgraph tt["Table tennis network -- Opal, 192.168.8.0/24"]
    box1["box, role tt<br/>k3s + Cilium + Flux<br/>no browser"]
    matrix1["LED matrix Pi<br/>192.168.8.30"]
    wled1["WLED<br/>192.168.8.31"]
  end
  subgraph sn["SthingsNet -- dd-wrt, 192.168.10.0/24"]
    box2["box, role kiosk<br/>Firefox kiosk<br/>no k3s"]
    matrix2["LED matrix Pi<br/>192.168.10.120"]
    wled2["WLED<br/>192.168.10.123"]
    hrdev["homerun2-dev"]
  end
  box1 <-. "same box, role from the default gateway<br/>(/usr/local/bin/edge-role)" .-> box2
  matrix1 <-. same Pi .-> matrix2
  wled1 <-. "same WLED, two Wi-Fi networks" .-> wled2
  box2 -- "kiosk tabs" --> hrdev
  matrix2 -- "stream messages" --> hrdev

The box, the matrix and the WLED move between the two networks without being reconfigured: the box picks its role from the default gateway, the WLED keeps both Wi-Fi networks, and the matrix reads from whichever Redis its network offers.

Today and the target

Today Target Where it is tracked
Box on the Opal runs: k3s, Cilium, Flux, gateways, OpenBao, MinIO (2026-10-12) -- box README, harvester#364
Piezo board reports over HTTPS, edge root checked on the board -- zaehlwerk-firmware
Buttons not built ESP-NOW buttons to a hub, the hub reports to zaehlwerk zaehlwerk-firmware#2, #3
Device certificates OpenBao ACME runs (HTTP-01, no EAB); only the lab's ESP mock enrols, the piezo board trusts the edge root but has no certificate of its own boards enrol themselves; EAB per device + static leases architecture.md, Internal
Players reach schmetterpause join sthings-edge, open the URL free Wi-Fi, captive sheet, one tap to schmetterpause harvester#431
Kiosk role kiosk in SthingsNet only -- box README, step 7

Planned: a captive portal

Tracked in harvester#431. The goal is settled, the way there is not:

  • The goal. A free Wi-Fi anybody can join; the captive sheet opens on its own, and one tap later the player is on schmetterpause. Players care about schmetterpause only -- zaehlwerk, homerun2 and the rest are for admins and do not belong on the page. After the tap the uplink is open, so nobody at the table is offline.
  • Where it runs. On the Opal: openNDS intercepts and lets clients through, the page itself is served by the box (FAS) on the players' gateway. Whether openNDS runs on the GL firmware (OpenWrt 18.06) is the first thing to find out; the fallbacks are in the issue.
  • What it must not break. The boards, the matrix and the WLED join the same Wi-Fi without a browser -- they must pass untouched (by MAC). And the players' certificate is publicly trusted only for *.sthings-edge.com: the page has to live on that name, or phones show a certificate warning.