Show the code in these languages

Focus, a case study

Focus is a focus timer for the robot, in the style of the Pomodoro Technique®: focus for 25 minutes, a 5 minute break, and a longer break after every four. The robot is the timer; a phone is optional. Its code: i41-eu/q41eu-focus at v0.5.0 (Apache-2.0), online at focus.q41.eu. The Pomodoro Technique® is a registered trademark of Francesco Cirillo; Focus is not affiliated with it.

What the robot and the phone show

A Stackchan robot running Focus with minutes and seconds shown: 17:42 left in a red ring, its LEDs counting down
Focus on the robot, here with minutes and seconds shown (a setting). By default no number counts down: the ring empties in six calm steps.

How it is built

About 2,200 lines of Go and a 390-line page, in four parts; of the kit it uses robothub for the robots' connections and sound for its three chimes.

Focus's parts
PartWhat it doesLines
internal/timerthe timer's rules: phases, start, pause, skip, reset, the history and the statistics. Pure: the caller passes the time.384
internal/screenthe robot's picture (the ring, and a 5×7 pixel font of its own) and the LEDs' colours381
internal/appthe robots through the kit's hub, the page's API and its event stream, pairing, sign-in through the manager, the browsers reported to it1,214
cmd/q41eu-focusthe flags, the state (a file or Postgres), the server210

The tests (730 lines): the timer's rules with times passed in, the pictures, and the app end to end, a fake robot over a real connection and a test clock.

What it uses of the robot

Every message Focus sends the robot or handles, and the same through Dev's API: a program can do what Focus shows on the robot with these actions.

What Focus sends and handles, and Dev's actions for it
The robotForThrough Dev
a picture (binary 0x10, a 320×240 JPEG)the timershow image
leds with 12 pixelsthe time leftleds pixels (or bar)
speaker sound (binary 0x03): the chimes start, done, break_overa start, the end of a phasesound chime
nod; look at yaw 0, pitch 30the end of a phasemove gesture nod; yaw, pitch
the events screen_tap and head_pressstart or pauseevents: tap; for the head pat or pet, when the touch ends
the event screen_long_pressskiplong_press

The key code

The robots, through the kit's hub

The hub checks the robots' tokens (the shared one, or one a manager gave the robot), keeps one connection per robot, makes the pairing codes for its QR screen and reports the paired browsers to the manager. Focus only says what to do when a robot connects, leaves or reports something; the timer is its own.

internal/app/robot.go, lines 18–38 at v0.5.0
// newHub: robots connect through the dev kit's hub (github.com/i41-eu/q41eu-dev/robothub), which
// checks their tokens, keeps their connections and pairing codes; the timer is the app's.
func (a *App) newHub() *robothub.Hub {
	return robothub.New(robothub.Config{
		Token: a.cfg.RobotToken, Manager: a.cfg.Manager, PublicURL: a.cfg.PublicURL, PairTTL: a.cfg.PairTTL, Log: a.log,
		Connected:    a.attach,
		Disconnected: a.detach,
		Event:        func(c *robothub.Robot, ev wire.RobotEventBody) { a.robotEvent(c.ID(), ev) },
		// The timer needs no browser: the robot always hears of one, so it shows the timer, not its QR screen.
		Paired: func(id string) wire.PairedBody {
			a.mu.Lock()
			defer a.mu.Unlock()
			return a.paired(id)
		},
		Owner: func(id, owner string) {
			a.mu.Lock()
			defer a.mu.Unlock()
			a.setOwnerLocked(id, owner)
		},
	})
}

No browser needed

A robot whose app has no paired browser shows its QR screen. The timer needs no browser, so the robot always hears of at least one, and shows the timer at once (the QR code stays a swipe away).

internal/app/app.go, lines 148–164 at v0.5.0
// paired is what the robot hears about browsers. The timer needs none, so the robot always
// hears at least one: it shows the timer, not its QR screen (the QR stays a swipe away).
// a.mu held.
func (a *App) paired(robotID string) wire.PairedBody {
	viewers, seen := 0, map[string]bool{}
	for _, s := range a.sessions {
		if slices.Contains(s.Robots, robotID) {
			viewers++
		}
	}
	for sub := range a.subs {
		if s := a.sessions[sub.sid]; s != nil && slices.Contains(s.Robots, robotID) {
			seen[sub.sid] = true
		}
	}
	return wire.PairedBody{Viewers: max(1, viewers), Watching: len(seen), Reconnect: true}
}

Touches: one action each

The robot reports raw input: a touch on the screen, a press on the head. Focus starts or pauses on either, at most once in 1.5 s (touchGap), so one touch is one action however it is reported; a long press skips.

internal/app/robot.go, lines 61–83 at v0.5.0
// robotEvent: a tap on the screen or a touch on the head starts or pauses; a long press on the
// screen skips to the next phase.
func (a *App) robotEvent(id string, ev wire.RobotEventBody) {
	a.mu.Lock()
	defer a.mu.Unlock()
	r := a.robots[id]
	if r == nil {
		return
	}
	now := a.cfg.Now()
	r.lastSeen = now
	switch ev.Name {
	case "screen_tap", "head_press":
		if now.Sub(r.lastTouch) < touchGap {
			return
		}
		r.lastTouch = now
		a.actLocked(r, "toggle", "", now)
	case "screen_long_press":
		r.lastTouch = now
		a.actLocked(r, "skip", "", now)
	}
}

Send only what changed

The robot's picture is Focus's own drawing, sent as a JPEG. A key names each picture, and a new one goes to the robot only when the key changes: six times a phase when calm, every second only when the seconds are shown. The LEDs likewise.

internal/app/robot.go, lines 159–183 at v0.5.0
// showLocked sends the robot its picture and LEDs when they changed: a picture every second
// only with the seconds shown, once a minute with minutes, six times a phase when calm. a.mu held.
func (a *App) showLocked(r *robot, now time.Time) {
	c := r.conn
	if c == nil {
		return
	}
	v := a.screenView(r, now)
	if key := screen.Key(v); key != r.shownFrame {
		r.shownFrame = key
		c.Binary(wire.BinShowJPEG, screen.Frame(v))
	}
	if !r.timer.Settings.Lights {
		v.Status = "idle" // all off
	}
	leds := strings.Join(screen.LEDs(v), ",")
	if leds != r.shownLEDs && slices.Contains(r.commands, "leds") {
		r.shownLEDs = leds
		px := []any{}
		for _, c := range screen.LEDs(v) {
			px = append(px, c)
		}
		c.Command("leds", map[string]any{"pixels": px})
	}
}

The end of a phase

The chimes are the kit's (sound.Chime), each short enough for one speaker message (the robot buffers about 3 s); the head nods or looks up, unless the person turned the moves off.

internal/app/robot.go, lines 124–149 at v0.5.0
// reactLocked is the robot's answer to what happened: a sound, a nod, a look up. a.mu held.
func (a *App) reactLocked(r *robot, ev timer.Event) {
	c, s := r.conn, r.timer.Settings
	if c == nil {
		return
	}
	play := func(name string) {
		if s.Sound && slices.Contains(r.commands, "speaker") {
			c.Binary(wire.BinSpeakerPCM, sound.Message(sound.Chime(name)))
		}
	}
	switch ev {
	case timer.Started, timer.Resumed:
		play("start")
	case timer.Completed: // a break now: a bell and a nod
		play("done")
		if s.Moves {
			c.Command("nod", nil)
		}
	case timer.BreakOver: // back to work: two notes, a look up at the person
		play("break_over")
		if s.Moves {
			c.Command("look", map[string]any{"yaw": 0, "pitch": 30})
		}
	}
}

The rules, pure

The timer never reads a clock: the app's loop calls Tick with the time every 200 ms, and the tests call it with any time they like, so a whole set of focus periods and breaks runs in milliseconds.

internal/timer/timer.go, lines 227–243 at v0.5.0
// Tick moves the timer on when a running phase is over. It returns what happened (one event at
// most; call it again for the next).
func (t *Timer) Tick(now time.Time) Event {
	if t.Status != Running || now.Before(t.EndsAt) {
		return ""
	}
	end := t.EndsAt
	if t.Phase == Focus {
		t.History = append(t.History, Done{At: end.UTC(), Minutes: t.Settings.FocusMin, Task: t.Task})
		t.prune(now)
		t.Round++
		t.next(end, true)
		return Completed
	}
	t.next(end, true)
	return BreakOver
}

What it teaches

Also in the kit for an app like it: leds.Bar and leds.Dim make a bar like Focus's own LEDs, robot.Play paces a sound of any length for the speaker, and robot.Gesture and robot.Look check that the robot offers the command first.