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
- Variables
- func ClearTrack(reason string)
- func Enabled() bool
- func FeedKey(name string)
- func GameActive() bool
- func KeyHoldCancel(key int, reason string)
- func KeyHoldReleased(key int, at time.Time)
- func KeyHoldStart(key int, pressAt time.Time)
- func LastRoundAt() time.Time
- func NoticesEnabled() bool
- func PauseTrack(d time.Duration, reason string)
- func ProcessCPUms(name string) int64
- func SanitizeText(s string, limit int) string
- func ScoresPath(id string) string
- func ScreenPNG(frame []byte) []byte
- func ScreenshotPath(id string) string
- func SelfCPUms() int64
- func SetGameStarter(f func(id string))
- func SetNoticeLogger(l *slog.Logger)
- func SetTrackPlaying(playing bool)
- func SetTrackSource(name string, ours bool)
- func Show(n Notice)
- func ShowAtStart(kind Kind, logger *slog.Logger)
- func ShowTrack(t TrackText)
- func StartUpdating(logger *slog.Logger) (stop func())
- func StreamMusic(w io.Writer, flush func(), done <-chan struct{}, track Track) error
- func Supported() bool
- func TrackShowing() bool
- func TrackStats() map[string]any
- func TrackSupported() bool
- type Blockfall
- type GameDef
- type KeyPress
- type Kind
- type LiveAudio
- type Logo
- type Notice
- func AlreadyOnKey(other int) Notice
- func EmptyKey(key int) Notice
- func Identify(name string) Notice
- func KeyHeld(key int) Notice
- func KeysRestored(n int) Notice
- func NotSaved(key int) Notice
- func SavedByKey(key int, name string) Notice
- func SavedFromApp(key int) Notice
- func Swapped(key, other int, otherName string) Notice
- type NoticeKind
- type Result
- type RoundHooks
- type Scores
- type Starguard
- type Synth
- type Track
- type TrackText
Constants ¶
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.
const ( KeyPressed = 0 KeyReleased = 1 KeyRepeat = 2 )
Key states.
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.
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 ¶
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.
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.
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} )
var ErrBusy = errors.New("display busy")
ErrBusy means the panel or a round is already in use.
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.
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.
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
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
KeyHoldReleased tells the held-key session of key that the key went up.
func KeyHoldStart ¶ added in v1.0.13
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
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
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
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
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 ScreenshotPath ¶
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
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
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 ¶
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 ¶
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 ¶
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
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 ¶
Blockfall is one round.
func (*Blockfall) End ¶ added in v1.0.6
func (g *Blockfall) End()
End finishes the round from outside.
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.
type KeyPress ¶
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.
func DecideKind ¶
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.
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
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.
type Logo ¶
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).
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
AlreadyOnKey: a save was refused because the item already sits on other.
func Identify ¶ added in v1.0.13
Identify: "find this speaker", HELLO and its name with pulsing rings.
func KeyHeld ¶ added in v1.0.13
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
KeysRestored: the reconcile wrote n keys back after a reboot.
func SavedByKey ¶ added in v1.0.13
SavedByKey: a held preset key stored name.
func SavedFromApp ¶ added in v1.0.13
SavedFromApp: the app or the phone page stored something on key.
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.
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 ¶
LoadScores reads game id's stored scores; missing means none yet.
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
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
Frame renders the current state into buf (grey levels 0..15).
func (*Starguard) Key ¶ added in v1.0.6
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.
type Synth ¶
type Synth struct {
Track Track
// contains filtered or unexported fields
}
Synth renders a track sample by sample.
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.
type TrackText ¶ added in v1.0.13
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.