Flere anlegg – hovedscene, resepsjon, crew – og ett sted å sende fra, med avkryssing for hvilke soner meldingen skal til. Sentralen er en vanlig announcer med [zones] i config. En sone er en vanlig announcer med ANNOUNCER_MESH_SECRET. Samme image, ingen mode, og en sone kan brukes alene hvis sentralen faller ut. Hver avhengighet med C-kode har aarch64-wheel i uv.lock, så det samme bildet går på en Raspberry Pi uten å kompilere noe. Sentralen syntetiserer én gang og sender ferdig WAV (POST /api/play, multipart). Sonen trenger da verken nett, edge-tts eller Pipers 670 MB for det den får – Item.wav er hele endringen i avspillingen. Køene forblir per sone: dispatcheren har egen tråd og egen kø, leverer til hver sone i hver sin tråd med timeout, og går videre. En opptatt hovedscene holder ikke igjen resepsjonen, og en sone som er nede feiler alene – synlig per sone i UI og revisjonslogg. Auth gjenbrukes: ny leverandør mesh på header X-Mesh-Key (ikke Bearer – den leses av JWT-leverandøren, som avviser alt som ikke er et JWT før neste får prøve), med én handling, play. To regler som fulgte med: - play følger aldri med «alt åpent». Uten det kunne hvem som helst POSTe en WAV til en åpen sone og få den spilt. Testen for åpen modus fanget det. - En legitimasjon som vises fram og er feil gir 401 også der gjester ellers har lov. Før slapp en feil PIN i åpen modus stille gjennom som gjest. mesh teller ikke som innlogging for folk: en sone med bare nøkkelen har fortsatt åpen side lokalt. Planlagte sendinger husker sonene sine og fordeles gjennom dispatcheren; planen ender som sent hvis minst én sone tok imot, med «hovedscene: kø #3, crew: nede» som note. Hodeløs Pi: install.sh kjører loginctl enable-linger, ellers finnes ikke lydsocketen når ingen logger inn. docs/mesh.md tar også raspi-config for jack framfor HDMI. 38 nye tester, 148 totalt. Fordelingen testes uten nett med stubbet transport; selve HTTP-en testes mot en levende werkzeug-server, så den håndrullede multiparten møter Flask sin parser. Verifisert i compose med sentral og to soner: til alle, bare til én, og én sone stoppet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
119 lines
5 KiB
Markdown
119 lines
5 KiB
Markdown
# Mesh: én sentral, flere soner
|
||
|
||
Flere anlegg – hovedscene, resepsjon, crewområde – og ett sted å sende fra,
|
||
med avkryssing for hvilke soner meldingen skal til.
|
||
|
||
## Hvordan det virker
|
||
|
||
**Sentralen** er en vanlig announcer med `[zones]` i `data/config.toml`.
|
||
**En sone** er en vanlig announcer med `ANNOUNCER_MESH_SECRET` i `.env`.
|
||
Samme image, ingen «mode». En sone kan derfor brukes alene også: faller
|
||
sentralen ut, går crew bort til sonens egen side og roper derfra.
|
||
|
||
Sentralen syntetiserer meldingen **én gang** og sender ferdig WAV til hver
|
||
valgt sone (`POST /api/play`). Sonen trenger verken nett, edge-tts eller
|
||
Pipers stemmer for det den får derfra – en Raspberry Pi med en høyttaler
|
||
holder.
|
||
|
||
Køene er **per sone**. Er hovedscenen opptatt med en lang melding, går en
|
||
resepsjonsmelding ut med en gang. Sentralen fordeler og går videre; hver
|
||
sone køer selv. Faller én sone ut, feiler bare den – de andre får meldingen,
|
||
og UI-et viser hvilken som ikke svarte.
|
||
|
||
Soner som får samme melding starter innen ~1 s av hverandre. Fint for
|
||
adskilte rom. Lekker rommene inn i hverandre hører man det; synkron start
|
||
mot NTP er en påbygging som ikke finnes ennå.
|
||
|
||
## Sette opp
|
||
|
||
### Sentralen
|
||
|
||
```toml
|
||
# data/config.toml
|
||
[zones]
|
||
her = "local" # eget anlegg. Utelat hvis sentralen ikke har høyttalere
|
||
hovedscene = "http://10.20.70.11:8090"
|
||
resepsjon = "http://10.20.70.12:8090"
|
||
crew = "http://10.20.70.13:8090"
|
||
```
|
||
|
||
```bash
|
||
# .env
|
||
ANNOUNCER_MESH_SECRET=<openssl rand -hex 24>
|
||
```
|
||
|
||
`docker compose restart`. Loggen sier `mesh: sentral for her, hovedscene, …`.
|
||
|
||
### Hver sone
|
||
|
||
```bash
|
||
git clone https://git.1x1.no/kent/announcer.git && cd announcer
|
||
ANNOUNCER_MESH_SECRET=<samme nøkkel> ./install.sh
|
||
```
|
||
|
||
Loggen sier `mesh: sone, tar imot fra sentralen`. Ingen `[zones]` på sonen.
|
||
|
||
Sonen må nås fra sentralen på adressen i `[zones]`. Med NetBird setter
|
||
`install.sh` `BIND_IP` til sonens NetBird-adresse – bruk den i `[zones]`.
|
||
Uten NetBird: sett `BIND_IP=0.0.0.0` (eller LAN-adressen) i sonens `.env`.
|
||
|
||
### Raspberry Pi uten skjerm
|
||
|
||
To ting som ellers gir en sone som aldri sier noe:
|
||
|
||
1. **Lyd uten innlogging.** Lydsocketen ligger i `/run/user/<uid>` og finnes
|
||
bare når brukerens systemd-instans kjører – normalt fra grafisk
|
||
innlogging. En Pi i et skap har ingen som logger inn. `install.sh` kjører
|
||
`loginctl enable-linger`, som starter brukerens tjenester (PipeWire
|
||
inkludert) ved boot uansett. Sjekk: `ls /run/user/$(id -u)/pipewire-0`
|
||
etter en omstart uten å logge inn.
|
||
2. **Hvilken utgang.** En Pi med både HDMI og jack sender lyden til HDMI som
|
||
standard – altså til skjermen som ikke er der. `sudo raspi-config` →
|
||
System Options → Audio → jack (eller USB-lydkortet). Eller sett
|
||
`ANNOUNCER_SINK` i `.env` til navnet fra `pactl list short sinks`.
|
||
|
||
## I bruk
|
||
|
||
Over «Send ut» står sonene som avkryssing, alle valgt som standard; valget
|
||
huskes i nettleseren. Forhåndslytt gjelder lyden, ikke sonene – du kan bytte
|
||
sone uten å lytte på nytt.
|
||
|
||
«Anlegget»-kortet viser én linje per sone: grønn prikk og hva som spilles,
|
||
eller rød prikk og «nede». «Siste meldinger» viser hver fordeling med et
|
||
merke per sone: `hovedscene ✓`, `crew ✗ nede`.
|
||
|
||
Planlagte sendinger husker sonene sine og overlever omstart av sentralen.
|
||
|
||
## Hva som kan gå galt
|
||
|
||
| | Hva du ser | Hva det betyr |
|
||
|---|---|---|
|
||
| Sone nede ved sending | `✗ nede` på den sonen, `✓` på resten | nådde ikke fram innen 5 s. Resten gikk |
|
||
| `✗ avvist` | `401 Feil mesh-nøkkel` i merket | `ANNOUNCER_MESH_SECRET` er ulik på sentral og sone |
|
||
| Rød prikk på en sone | «nede: …» i sonelista | statuspollingen når den ikke. Sjekk adresse i `[zones]` og `BIND_IP` på sonen |
|
||
| Alle soner `✗ feilet` | feilmelding på fordelingen | syntesen feilet på sentralen (nettet nede og nevral stemme valgt). Velg en lokal stemme |
|
||
| Sonen spiller ikke, men viser `feilet` | `fant verken pw-play eller paplay`-aktig feil i sonens kø | ingen lydsocket: se «Raspberry Pi uten skjerm» |
|
||
| Sone restartet med melding i kø | meldingen borte | køen ligger i minnet, som for lokale meldinger. Akseptert |
|
||
|
||
Alt havner i sentralens `data/logs/announcements.jsonl` som `announce`
|
||
(hvem, til hvilke soner) og `dispatched` (hva hver sone svarte), og i hver
|
||
sones logg som `play`.
|
||
|
||
## Protokoll, for den som vil skrive sin egen sone
|
||
|
||
`POST /api/play`, header `X-Mesh-Key: <hemmelighet>`, multipart:
|
||
|
||
| felt | |
|
||
|---|---|
|
||
| `wav` | fila. 16-bit mono WAV, jingle og tale ferdig satt sammen |
|
||
| `meta` | JSON: `origin_id`, `text`, `voice`, `user`, `subject`, `method` |
|
||
|
||
Svar `200 {"id": <kø-id>}`. Statusen ses i `GET /api/status` med samme
|
||
header. Alt annet er 400 med `{"error": "…"}`; feil nøkkel er 401.
|
||
|
||
Egen header, ikke `Authorization: Bearer`: den leses av JWT-leverandøren,
|
||
som avviser alt som ikke er et JWT før neste leverandør får prøve.
|
||
|
||
`play` følger aldri med «alt åpent». En sone uten `config.toml` lar hvem
|
||
som helst sende lokalt, men ikke spille en fil de har med seg – det krever
|
||
nøkkelen.
|