Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,19 @@ and serves the box's own screens:
room is open, boxd notices (heartbeats keep coming while the page is
alive) and moves the TV to the offline screen after a couple of minutes.

- **No network at all: the setup hotspot.** A box with no Ethernet and
no Wi-Fi becomes its own open Wi-Fi network, `Syncphony-Box-XXXX`
(the last four of its MAC). The TV shows a QR code that joins it, and
the phone's captive-portal page opens with the Wi-Fi networks in range,
a password field, and the server URL if the box doesn't have one yet.
Saving joins that Wi-Fi. If the join fails, the hotspot comes back and
the TV says why. A new box starts the hotspot 60 s after boot. A box
that has been online before shows the offline screen first and only
falls back to the hotspot after 5 minutes without a network, so a
router reboot never puts it in setup mode. Imager's Wi-Fi settings
still work and are the fast path
([ADR 0010](docs/adr/0010-wifi-setup-hotspot.md)).

Logs go to journald: `journalctl -u boxd -u syncphony-kiosk`.

## The TV remote (HDMI-CEC)
Expand Down Expand Up @@ -338,6 +351,12 @@ has its own page: [docs/testing.md](docs/testing.md).
- `audio=usb` with a USB DAC: the DAC is the default sink and the test tone
plays through it after a reboot.
- Unplug HDMI while music plays, plug it back: audio returns by itself.
- Fresh flash, no Imager Wi-Fi, no cable: about a minute after boot the
TV shows the `Syncphony-Box-XXXX` QR; a phone joins, the setup page
opens by itself, and choosing the house Wi-Fi (plus the server URL)
ends in the pairing screen. A wrong password brings the hotspot back
with the reason on the TV ([docs/testing.md](docs/testing.md), "The
Wi-Fi setup run").
- Fresh flash, no `server_url`: the setup screen's QR appears; setting the
URL from a phone (the box validates it via `/healthz`) brings up `/tv`'s
pairing code with no SD-card editing.
Expand Down Expand Up @@ -368,7 +387,8 @@ has its own page: [docs/testing.md](docs/testing.md).

1. Write the `.img.xz` to an SD card with Raspberry Pi Imager.
2. In the OS customization settings, set at minimum a username and password
(Wi-Fi, SSH and hostname as needed). The first user in the image ships with
(Wi-Fi, SSH and hostname as needed — without Wi-Fi settings or a cable,
the box offers its setup hotspot instead). The first user in the image ships with
a **locked password** — there is no default login; the Imager settings are
applied on first boot by cloud-init.
3. Boot the Pi. Without Imager customization the image behaves like stock
Expand All @@ -379,9 +399,9 @@ has its own page: [docs/testing.md](docs/testing.md).
```
pi-gen/ pi-gen submodule (pinned, never edited)
stage-syncphony/ custom pi-gen stage (packages, kiosk session,
read-only root, CEC, ...)
read-only root, CEC, Wi-Fi setup, ...)
boxd/ the box's Go helper daemon (bridge, config, screens,
watchdogs, CEC)
watchdogs, CEC, Wi-Fi setup hotspot)
config pi-gen build configuration
build.sh wraps pi-gen's build-docker.sh (builds boxd first,
then appends the /data partition after export)
Expand Down
1 change: 1 addition & 0 deletions boxd/internal/daemon/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func (d *Daemon) bridgeMux() *http.ServeMux {
mux.HandleFunc("GET /setup", d.handleSetupScreen)
mux.HandleFunc("GET /start", d.handleStartScreen)
mux.HandleFunc("GET /storage", d.handleStorageScreen)
mux.HandleFunc("GET /hotspot", d.handleHotspotScreen)
return mux
}

Expand Down
18 changes: 16 additions & 2 deletions boxd/internal/daemon/daemon.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Package daemon is boxd's running half: the box bridge on loopback
// (ADR 0016), the local screens the kiosk shows when there's no server
// to display — setup with a QR code, and the offline "can't reach"
// screen — and the watchdogs and thermals that make the box heal itself
// to display — setup with a QR code, the offline "can't reach" screen,
// and the Wi-Fi setup hotspot for a box with no network at all
// (MAD-808, ADR 0010) — and the watchdogs and thermals that make the box heal itself
// (MAD-806, ADR 0008).
package daemon

Expand All @@ -19,6 +20,7 @@ import (

"github.com/madeofpendletonwool/syncphony-box/boxd/internal/cec"
"github.com/madeofpendletonwool/syncphony-box/boxd/internal/config"
"github.com/madeofpendletonwool/syncphony-box/boxd/internal/wifi"
)

const (
Expand Down Expand Up @@ -103,6 +105,13 @@ type Daemon struct {
lastNightlyDay string // local day the nightly reload last ran ("2006-01-02")
therm thermals // last thermal sample (zero = none yet)
lastFlush time.Time // last pairing-durability sync burst

// The Wi-Fi setup hotspot (MAD-808): NetworkManager behind an
// interface for tests, its state, and netOp serializing the
// NetworkManager work (the watch loop, a join, a recheck).
wifi netDriver
net netState
netOp sync.Mutex
}

// New builds a daemon around the current syncphony.txt (the SB_* env hooks
Expand All @@ -121,6 +130,10 @@ func New(version string, log *log.Logger, run config.RunCommand) *Daemon {
},
}
d.reloadConfig()
d.wifi = wifi.New()
if _, err := os.Stat(seenMarker()); err == nil {
d.net.seenOnline = true
}
d.cec = cec.New(d.cecConfig(d.cfg), log, d.onRemoteLongPress)
d.wakeTV = d.cec.WakeAndSwitch
return d
Expand Down Expand Up @@ -218,6 +231,7 @@ func (d *Daemon) Run() error {

d.log.Printf("boxd: %s bridge on http://%s (server: %s)", d.version, bridgeAddr, d.serverOrNone())
go d.watch()
go d.netWatch()
go d.sampleThermalsLoop()
go func() {
// CEC (MAD-807): register as a playback device on the adapters
Expand Down
3 changes: 3 additions & 0 deletions boxd/internal/daemon/daemon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ func newTestDaemon(t *testing.T, txtContent string) (*Daemon, *recorder) {
// No dev machine has /data mounted: the storage hook pins "healthy" so
// the screens under test don't trip over the host's mount table.
t.Setenv("SB_STORAGE", "ok")
// boxd's /data state (the hotspot's "been online" marker) in the
// test's own directory.
t.Setenv("SB_BOXD_DIR", dir)
d := New("test-version", log.New(log.Writer(), "", 0), rec.run)
return d, rec
}
Expand Down
Loading
Loading