oled

package
v1.0.16 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package oled draws STR's own splash animation on the SoundTouch Portable's OLED panel: after a fresh install, after an update, on a box boot, and while an agent update is being received. The same panel carries STR's short display notices (notice.go) and the hidden games (game.go).

The panel is a plain Linux framebuffer (/dev/fb0, ssdspi, 128x100, 8 bpp) that BoseApp fills about six times a second from one thread named DirectFBFlipTas. For the few seconds of the animation that one thread is held with PTRACE_SEIZE + PTRACE_INTERRUPT (no signal, every other BoseApp thread keeps running, playback included) and released afterwards; BoseApp then repaints its own screen within a frame. If the agent dies mid-splash the kernel releases the thread on its own. Nothing is written to NAND except the small splash-seen marker.

Only the SoundTouch panel geometry (ST20, ST30, Portable, Wave) is recognised; on every other model, and when the user switched it off, all entry points return without touching anything.

Index

Constants

View Source
const (
	KeyPlay       = 0
	KeyPause      = 1
	KeyStop       = 2
	KeyPrev       = 3
	KeyNext       = 4
	KeyThumbsUp   = 5
	KeyThumbsDown = 6
	KeyPower      = 8
	KeyPreset1    = 12 // .. KeyPreset1+5 for preset 6
	KeyAux        = 18
)

Key codes in BoseApp's KEY_VAL order, as the speaker's own key trace names them.

View Source
const (
	KeyPressed  = 0
	KeyReleased = 1
	KeyRepeat   = 2
)

Key states.

View Source
const (
	Width     = 128
	Height    = 100
	FrameSize = Width * Height
)

Panel geometry of the SoundTouch Portable's OLED (ssdspi, 8 bpp, 16 grey levels). Frames handed around in this package hold grey levels 0..15, one byte per pixel; the panel code scales them to the 0x00..0xFF the driver expects.

View Source
const (

	// TrackVolumePause is how long a volume change hands the panel back to
	// BoseApp, long enough for its volume bar.
	TrackVolumePause = 4 * time.Second
)

Variables

View Source
var (
	CueWaveClear = []int{72, 76, 79, 84, 0, 84, 88, 91}
	CueShieldHit = []int{84, 79, 84, 79}
	CueShipLost  = []int{67, 66, 64, 0, 60, 59, 55, 0, 48}
)

Cues, one MIDI note per sixteenth, 0 = rest.

View Source
var (
	// FlagPath is the per-speaker switch. Absent means on.
	FlagPath = "/mnt/nv/streborn/display-splash"
	// SeenPath remembers the agent version the last splash was decided for,
	// so an install and an update are told apart from a plain boot.
	SeenPath = "/mnt/nv/streborn/splash-seen"
)

Paths on the speaker; vars so tests can redirect them.

View Source
var (
	// IntroTrack plays under the intro logo.
	IntroTrack = Track{Lead: gameLead, Bass: gameBass, Loop: true}
	// OverTrack plays under the game-over screen.
	OverTrack = Track{Lead: overLead, Bass: overBass}
)
View Source
var ErrBusy = errors.New("display busy")

ErrBusy means the panel or a round is already in use.

View Source
var Games = []GameDef{
	{
		ID: "blockfall", Title: "BLOCKFALL",
		Code:  []string{"THUMBS_UP", "THUMBS_UP", "THUMBS_DOWN", "THUMBS_DOWN", "PREV_TRACK", "NEXT_TRACK", "PREV_TRACK", "NEXT_TRACK"},
		Intro: IntroTrack,
		Over:  OverTrack,
		// contains filtered or unexported fields
	},
	{
		ID: "starguard", Title: "STARGUARD",
		Code:  []string{"THUMBS_DOWN", "THUMBS_UP", "THUMBS_DOWN", "THUMBS_UP", "NEXT_TRACK", "NEXT_TRACK", "NEXT_TRACK"},
		Intro: sgIntroTrack,
		Over:  sgOverTrack,
		Live:  true,
		// contains filtered or unexported fields
	},
}

Games are all games, in the order the app lists them.

View Source
var NoticesFlagPath = "/mnt/nv/streborn/display-notices"

NoticesFlagPath is the per-speaker switch for the notices. Absent means on. A var so tests can redirect it.

View Source
var ScoreDir = "/mnt/nv/streborn"

Score storage on NAND, one file per game, written once per round so the app can collect it and offer to share it. A var so tests can redirect it.

Functions

func ClearTrack added in v1.0.13

func ClearTrack(reason string)

ClearTrack ends the track screen and hands the panel back to BoseApp. Blocks (bounded) until the panel is let go, so a caller that exits next leaves nothing held. reason goes to the log.

func Enabled

func Enabled() bool

Enabled reads the per-speaker switch. On unless the flag file says otherwise.

func FeedKey

func FeedKey(name string)

FeedKey is called for every physical key press the speaker decodes. When the last presses spell a game's code, the registered starter runs.

func GameActive

func GameActive() bool

GameActive reports whether a round is running; key bindings (webhooks, group keys) stand down meanwhile.

func KeyHoldCancel added in v1.0.13

func KeyHoldCancel(key int, reason string)

KeyHoldCancel ends the held-key session of key unless an outcome already reached it (a tap after all, a save that gave up without a word).

func KeyHoldReleased added in v1.0.13

func KeyHoldReleased(key int, at time.Time)

KeyHoldReleased tells the held-key session of key that the key went up.

func KeyHoldStart added in v1.0.13

func KeyHoldStart(key int, pressAt time.Time)

KeyHoldStart starts the held-key screen for key, pressed at pressAt. It returns at once; a running ordinary notice (anything but identify) gives way, and so does an older held-key session.

func LastRoundAt added in v1.0.6

func LastRoundAt() time.Time

LastRoundAt is when the most recent round of any game was saved, zero when none was. It only stats the score files, so the version endpoint can carry it on every probe.

func NoticesEnabled added in v1.0.13

func NoticesEnabled() bool

NoticesEnabled reads the notices switch, with the same parsing as Enabled.

func PauseTrack added in v1.0.13

func PauseTrack(d time.Duration, reason string)

PauseTrack hands the panel back to BoseApp for d (a volume change: its own volume bar shows), then the track screen returns by itself.

func ProcessCPUms added in v1.0.13

func ProcessCPUms(name string) int64

ProcessCPUms is the CPU time (user plus system) the named process has used since it started, in milliseconds; -1 when it is not running. For the display_cost debug section: the panel hold stops BoseApp's flip thread, and whether BoseApp spends more or less while it is held is the question.

func SanitizeText added in v1.0.13

func SanitizeText(s string, limit int) string

SanitizeText makes s drawable: upper case, umlauts and ß spelled out, other accents dropped to the base letter, whitespace runs folded to one space, everything the font cannot draw removed, and the result cut to limit characters.

func ScoresPath

func ScoresPath(id string) string

ScoresPath is where game id keeps its scores.

func ScreenPNG

func ScreenPNG(frame []byte) []byte

ScreenPNG renders a frame 3x up as a greyscale PNG, the size people share.

func ScreenshotPath

func ScreenshotPath(id string) string

ScreenshotPath is where game id keeps its last round's final screen.

func SelfCPUms added in v1.0.13

func SelfCPUms() int64

SelfCPUms is the agent's own CPU time in milliseconds.

func SetGameStarter

func SetGameStarter(f func(id string))

SetGameStarter registers what a code starts (the webui's round runner, which owns playback state). It gets the game's ID.

func SetNoticeLogger added in v1.0.13

func SetNoticeLogger(l *slog.Logger)

SetNoticeLogger sets where the notice lines go (slog.Default otherwise).

func SetTrackPlaying added in v1.0.13

func SetTrackPlaying(playing bool)

SetTrackPlaying tells the track screen whether the speaker plays, from its own play status: the playing symbol moves only while it does.

func SetTrackSource added in v1.0.13

func SetTrackSource(name string, ours bool)

SetTrackSource tells the track screen which source the speaker switched to and whether it is one of STR's own. On any other source (standby, AUX, Bluetooth, a firmware service such as TuneIn that shows its own titles) the track leaves the panel and titles are ignored until STR's source is back. Does not block (it is called from the gabbo read loop): the flag and the wanted text change at once, in order, and only letting go of the panel runs in the background.

func Show added in v1.0.13

func Show(n Notice)

Show queues n for the panel and returns at once. Safe from any goroutine.

func ShowAtStart

func ShowAtStart(kind Kind, logger *slog.Logger)

ShowAtStart waits for BoseApp to be up and plays the splash for kind once. Blocking; run it in its own goroutine.

func ShowTrack added in v1.0.13

func ShowTrack(t TrackText)

ShowTrack puts t on the panel, or updates what is there. Returns at once.

func StartUpdating

func StartUpdating(logger *slog.Logger) (stop func())

StartUpdating shows the logo with a running bar while an agent update is received and written. The returned stop zooms it out and hands the panel back; it is only needed on the paths that do not end in a reboot (the reboot takes the animation down with the process). Safe to call stop more than once. A hard cap releases the panel even if stop is never called.

func StreamMusic

func StreamMusic(w io.Writer, flush func(), done <-chan struct{}, track Track) error

StreamMusic writes track as an endless WAV to w, paced to real time: a 2 s head start fills the renderer's buffer, then it stays about 1 s ahead. It returns when done closes or a write fails (the speaker hung up).

func Supported

func Supported() bool

Supported reports whether this speaker has the one panel the splash knows how to draw on (the Portable's).

func TrackShowing added in v1.0.13

func TrackShowing() bool

TrackShowing reports whether the track screen holds the panel.

func TrackStats added in v1.0.13

func TrackStats() map[string]any

TrackStats is what the track screen has cost since the agent started: how long it held the panel, how often it drew and how long the drawing took (the copy to the panel not included). For the display_cost debug section.

func TrackSupported added in v1.0.13

func TrackSupported() bool

TrackSupported reports whether STR can draw the track on this speaker's panel. Where it cannot, the caller keeps the old metadata path.

Types

type Blockfall

type Blockfall struct {
	Score int
	Lines int
	Over  bool
	// contains filtered or unexported fields
}

Blockfall is one round.

func NewBlockfall

func NewBlockfall(seed uint64) *Blockfall

NewBlockfall starts a round.

func (*Blockfall) End added in v1.0.6

func (g *Blockfall) End()

End finishes the round from outside.

func (*Blockfall) Frame

func (g *Blockfall) Frame(buf []byte)

Frame renders the round into buf (grey levels 0..15).

func (*Blockfall) Key

func (g *Blockfall) Key(key, state int)

func (*Blockfall) State added in v1.0.6

func (g *Blockfall) State() (bool, int, int)

State reports whether the round is over, its score and the cleared rows.

func (*Blockfall) Step

func (g *Blockfall) Step()

Step advances one frame (bfFrameHz per second).

type GameDef added in v1.0.6

type GameDef struct {
	ID, Title string
	Code      []string // remote sequence, by the key names of the speaker's own trace
	Intro     Track
	Over      Track
	// Live: the round's music runs through the game too and follows it
	// (liveaudio.go). Only for games played without Previous/Skip, which are
	// also the firmware's own skip keys and stop the stream.
	Live bool
	// contains filtered or unexported fields
}

GameDef describes one game: how it is started and what it sounds like.

func Game added in v1.0.6

func Game(id string) (GameDef, bool)

Game looks a game up by its ID.

type KeyPress

type KeyPress struct {
	Key   int
	State int
}

KeyPress is one remote key state change: Key in BoseApp's KEY_VAL order, State as BoseApp logs it (0 press, 1 release, 2 repeat while held, 3 and 4 press-and-hold stages, 5 timeout).

type Kind

type Kind int

Kind selects the splash text.

const (
	KindNone      Kind = iota
	KindBoot           // a real box boot
	KindInstalled      // first start of STR on this box
	KindUpdated        // first start of a new agent version
)

func DecideKind

func DecideKind(stamp string, otaMarker, boxBoot bool) Kind

DecideKind picks the splash for this agent start. otaMarker is whether the update handler's reboot marker was present (read before anything consumes it), stamp the running version, boxBoot whether the box itself just booted (as opposed to the agent being respawned on a running box, which gets no splash at all). It records stamp in SeenPath when that changes, one small NAND write per version.

func (Kind) String

func (k Kind) String() string

type LiveAudio added in v1.0.6

type LiveAudio struct {
	// contains filtered or unexported fields
}

LiveAudio is the music of a round that changes with the game: one stream from the intro to the game-over jingle, switched inside the stream so the speaker never has to start a new one. The game sets the tempo and queues short cues; the synth reads them while it renders.

The speaker buffers a second or two of the stream, so everything here is chosen to sound right when it arrives late: a march whose tempo follows the aliens left, a warble while the saucer crosses, and fanfares for a cleared wave or a lost ship. Shot and explosion sounds would always lag the picture and are left out on purpose.

func NewLiveAudio added in v1.0.6

func NewLiveAudio() *LiveAudio

NewLiveAudio starts in the intro.

func (*LiveAudio) Cue added in v1.0.6

func (la *LiveAudio) Cue(notes []int)

Cue queues a short phrase over the music. At most a few are kept; a game that queues faster than they play loses the oldest.

func (*LiveAudio) GameOver added in v1.0.6

func (la *LiveAudio) GameOver()

GameOver switches to the game-over jingle, played once.

func (*LiveAudio) SetSaucer added in v1.0.6

func (la *LiveAudio) SetSaucer(on bool)

SetSaucer turns the saucer warble on or off.

func (*LiveAudio) SetTempo added in v1.0.6

func (la *LiveAudio) SetTempo(pct int)

SetTempo sets the march tempo in percent of the base (clamped 100..230).

func (*LiveAudio) StartGame added in v1.0.6

func (la *LiveAudio) StartGame()

StartGame switches the stream from the intro melody to the in-game music.

type Logo struct {
	Subtitle string
	Total    float64 // seconds; the zoom-out fills the last 1.6 s
	Hold     bool    // stay assembled with a running bar until Exit is called
	// contains filtered or unexported fields
}

Logo animates the STR mark: the dots of the big letters fly in from outside the panel and settle, the subtitle types in under it, a highlight band sweeps across, and the finale zooms the whole picture towards the viewer until it is gone. With Hold set it never zooms out and instead shows a running bar (for "UPDATING", which ends with the reboot).

func NewLogo(subtitle string, total float64, seed uint64) *Logo

NewLogo lays out the mark; seed only varies where the dots fly in from.

func (*Logo) Done

func (l *Logo) Done(t float64) bool

Done reports whether the animation has nothing left to show at time t.

func (*Logo) Exit

func (l *Logo) Exit(t float64)

Exit starts the zoom-out of a Hold logo at time t; Done reports true once it has finished.

func (*Logo) Frame

func (l *Logo) Frame(t float64, buf []byte)

Frame renders time t (seconds since start) into buf (grey levels 0..15).

type Notice added in v1.0.13

type Notice struct {
	Kind NoticeKind
	// Key is the preset key the notice is about (the count of keys for
	// NoticeRestored).
	Key int
	// Line1 is the big line, Line2 and Line3 the small ones; all already
	// sanitised to what the font can draw.
	Line1, Line2, Line3 string
	// Hold is how long the notice stays, fade-in included.
	Hold time.Duration
	// Bar, when above 0, draws a bar filled to that fraction (0..1).
	Bar float64
	// Bypass shows the notice even when notices are switched off.
	Bypass bool
	// contains filtered or unexported fields
}

Notice is one message for the panel. Build it with the constructors below.

func AlreadyOnKey added in v1.0.13

func AlreadyOnKey(other int) Notice

AlreadyOnKey: a save was refused because the item already sits on other.

func EmptyKey added in v1.0.13

func EmptyKey(key int) Notice

EmptyKey: a hardware press on a key that holds nothing.

func Identify added in v1.0.13

func Identify(name string) Notice

Identify: "find this speaker", HELLO and its name with pulsing rings.

func KeyHeld added in v1.0.13

func KeyHeld(key int) Notice

KeyHeld is the screen while a preset key is held: the key with a busy badge, KEY N, SAVING...

func KeysRestored added in v1.0.13

func KeysRestored(n int) Notice

KeysRestored: the reconcile wrote n keys back after a reboot.

func NotSaved added in v1.0.13

func NotSaved(key int) Notice

NotSaved: what plays cannot be kept on key.

func SavedByKey added in v1.0.13

func SavedByKey(key int, name string) Notice

SavedByKey: a held preset key stored name.

func SavedFromApp added in v1.0.13

func SavedFromApp(key int) Notice

SavedFromApp: the app or the phone page stored something on key.

func Swapped added in v1.0.14

func Swapped(key, other int, otherName string) Notice

Swapped: a held key took over the item that sat on other, and other got what the held key had (otherName), or became empty.

type NoticeKind added in v1.0.13

type NoticeKind string

NoticeKind names a notice in the log and in tests.

const (
	NoticeSavedByKey   NoticeKind = "saved-key"
	NoticeSavedFromApp NoticeKind = "saved-app"
	NoticeAlreadyOnKey NoticeKind = "already-on-key"
	NoticeSwapped      NoticeKind = "swapped"
	NoticeNotSaved     NoticeKind = "not-saved"
	NoticeEmptyKey     NoticeKind = "empty-key"
	NoticeRestored     NoticeKind = "keys-restored"
	NoticeIdentify     NoticeKind = "identify"
)

The notices STR shows.

const NoticeKeyHeld NoticeKind = "key-held"

NoticeKeyHeld is the held-key screen.

type Result

type Result struct {
	Score, Lines int
	Reason       string // "game over", "idle", "time", "key", "stopped"
	Screen       []byte // last frame, grey levels 0..15
}

Result is how a round ended.

func PlayRound

func PlayRound(id string, stop <-chan struct{}, hooks RoundHooks, logger *slog.Logger) (Result, error)

PlayRound runs one round of game id: intro, game, game-over screen. It blocks until the round ends or stop closes.

type RoundHooks

type RoundHooks struct {
	// IntroReady reports whether the intro may end: the caller holds it
	// while the speaker wakes and the intro music starts (capped at introMax).
	IntroReady func() bool
	// GameStart runs when the intro ends and the game begins (music off).
	GameStart func()
	// GameOver runs when the game-over screen appears (jingle on).
	GameOver func()
	// OverHold keeps the game-over screen up past gameOverShown while it
	// reports true (the jingle is still playing), capped at gameOverMax.
	OverHold func() bool
	// Live is the round's live music, handed to a game that drives it.
	Live *LiveAudio
}

RoundHooks lets the caller run the music around a round. Every hook is called from the render loop and must not block. All are optional.

type Scores

type Scores struct {
	Best     int       `json:"best"`
	BestAt   time.Time `json:"bestAt,omitzero"`
	Last     int       `json:"last"`
	LastRows int       `json:"lastRows"`
	LastAt   time.Time `json:"lastAt,omitzero"`
	Rounds   int       `json:"rounds"`
}

Scores is what the app reads back.

func LoadScores

func LoadScores(id string) Scores

LoadScores reads game id's stored scores; missing means none yet.

func SaveRound

func SaveRound(id string, r Result, now time.Time) (Scores, error)

SaveRound records a finished round of game id and its last screen. Rounds that never started (score 0 and nothing cleared) are not worth a NAND write.

type Starguard added in v1.0.6

type Starguard struct {
	Score int
	// contains filtered or unexported fields
}

Starguard is one round.

func NewStarguard added in v1.0.6

func NewStarguard(seed uint64) *Starguard

NewStarguard starts a round.

func (*Starguard) End added in v1.0.6

func (g *Starguard) End()

End finishes the round from outside.

func (*Starguard) Frame added in v1.0.6

func (g *Starguard) Frame(buf []byte)

Frame renders the current state into buf (grey levels 0..15).

func (*Starguard) Key added in v1.0.6

func (g *Starguard) Key(key, state int)

Key: thumbs down steers left, thumbs up right. A tap moves the ship one small step. Held, it moves on smoothly until the release and stops there, and held longer it speeds up.

Measured on the remote (key log of a round, 2026-10-06): one press and one release per touch, nothing in between, no repeats and no second frame. Taps last 150 to 470 ms, deliberate holds 600 ms and more, so a press counts as a hold only after sgHoldDelay. Every press is a step of its own, however quickly it follows the one before.

func (*Starguard) State added in v1.0.6

func (g *Starguard) State() (bool, int, int)

State reports whether the round is over, its score and the waves cleared.

func (*Starguard) Step added in v1.0.6

func (g *Starguard) Step()

Step advances one step (30 per second).

type Synth

type Synth struct {
	Track Track
	// contains filtered or unexported fields
}

Synth renders a track sample by sample.

func (*Synth) Render

func (s *Synth) Render(buf []int16)

Render fills buf with the next mono samples.

type Track

type Track struct {
	Lead, Bass []int
	Loop       bool
	// Live, when set, makes this the music of one round that follows the
	// game (liveaudio.go); Lead and Bass are then unused.
	Live *LiveAudio
}

Track is one piece of music: a lead and a bass line in the formats above. A track that does not loop is followed by silence until the stream stops, so the speaker keeps the same stream and shows no state change.

func (Track) Length

func (tr Track) Length() time.Duration

Length is how long one pass of the track lasts.

type TrackText added in v1.0.13

type TrackText struct {
	Station, Artist, Title string
	Clock, Hour12          bool
}

TrackText is what the track screen shows. Station is the small top line; Title is the main text, Artist the line above it. Either of Artist and Title may be empty (the per-speaker mode shows only one of them). Clock adds the time: in the top line when there is a track, large on its own when Artist and Title are both empty. Hour12 draws it as "3:42 PM", as the speaker's own clock setting says.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL