The event loop and touch
One goroutine wakes every 20 ms, reads what the firmware posted, and calls the app; it alone calls the app, and it builds long presses, drags and key repeats out of the firmware's bare downs, moves and ups.
The previous chapter showed the mailboxes. Something has to check them,
turn their bytes into events, and call the app. That is the
event loopEvent loop: The one goroutine that polls for input and firmware messages every 20 ms and calls the app. It is the only one that draws., in internal/device/pocketbook. It is small, but every
touch you make goes through it.
A night watchman doing rounds: every 20 ms he walks past the same doors in the same order, looks in each mailbox, and deals with what he finds. Between rounds, he sits by a doorbell that colleagues can ring if they need him at once.
Why poll every 20 ms?
The obvious design is to sleep until a message arrives. The firmware's queues do not allow it:
the task queue is a System V queue, which is not a file and cannot be watched like one, and
/hwevent is opened non-blocking and read without waiting. As the code says, neither queue can
wake the app. So the loop does what libinkview
does: it wakes on a ticker every 20 ms (pollInterval) and looks.
20 ms is short enough that you cannot feel it (the panel itself takes hundreds of milliseconds to change), and long enough that looking costs nothing.
One tick, in order
At each tick, poll does the same steps, in this order:
readReplies, only while a request waits: collect the firmware's replies.readTaskMessages: answer sync events, pass news toApp.System, stop on exit.followForeground: readvisible(). In the background, stop here: forget held buttons and touches. Back in front, callDraw, since the panel shows another app's picture.readInput: read every waiting/hweventmessage, and callApp.KeyorApp.Pointer.repeatHeldKey: if a button is held long enough, send a repeat.sendLongPress: if a finger is held still long enough, send a long press.
The last two run at every tick, even with no input: a finger held still sends nothing, so only the clock can say "it has been 500 ms".
The doorbell: channels
A Go channel is a pipe that goroutines pass values through, and a select waits on several
channels at once and takes whichever is ready first. The loop is a select on the ticker and a
few channels:
// trimmed: logging and the conversion to seconds left out
select {
case <-repaints: app.Draw()
case <-exits: return app.Close()
case duration := <-sleepRequests: loop.request(taskBanSuspend, seconds(duration), time.Now())
case <-signals: return app.Close()
case now := <-ticker.C: if loop.poll(now) { return app.Close() }
}
The rule that keeps the app simple: only this goroutine calls the app. A download or a cover
loader running on another goroutineGoroutine: Go’s lightweight thread. Inkshelf starts them only from the main thread, with state.Go. never draws. It calls
Repaint(), Exit() or PreventSleepFor(), which only drop a request into a channel that holds
one. A second Repaint sent before the first is read is dropped: one Draw does both. Channels
can wake a select, so a repaint does not wait for the next tick.
From raw touches to gestures
The firmware sends a touch as down, move, move…, up, with raw coordinates. Everything else, the app's long presses, holds and drags, the loop makes itself, as libinkview did.
Turning the coordinates. The touch panel is mounted turned, like the display. touchPoint
converts: screen x = raw y, screen y = 1871 − raw x, clamped to the screen. The InkPad 3
has no calibration file, so this fixed rule is all there is.
Only the first finger. A message with a touch id other than 0 is a second finger: ignored.
The dead zone. A finger always trembles. A touch counts as still while it stays within a
squared distance of 200 of where it went down (a radius of about 14 pixels, about 1.2 mm on a
300 dpi screen). Past it, every move is also sent as a PointerDrag, and that touch gets no long
press.
Long press and hold. A still touch held for 500 ms becomes PointerLong, once. Then,
every 500 ms while it is held, PointerHold.
case eventPointerMove:
touch.last = point
loop.app.Pointer(PointerEvent{Point: point, State: PointerMove})
if !touch.dragging && squaredDistance(point, touch.start) > dragDeadZone {
touch.dragging = true
}
if touch.dragging {
loop.app.Pointer(PointerEvent{Point: point, State: PointerDrag})
}
Key repeats
Buttons work the same way. The firmware sends a press and a release; it also sends repeats of its own (event 28), but libinkview ignored them and timed repeats itself, and so does Inkshelf. A held button repeats after 600 ms, then every 300 ms, checked at each tick. The reader turns page after page while you hold Next.
Going deeper
- Timestamps are tick times. Input waits in
/hweventuntil the next tick, and the loop stamps it with the tick's time. That is why the times above land on multiples of 20 ms, and why a long press arrives at the first tick at or after 500 ms. - Background resets.
followForegroundclears the held button and the active touch when the app leaves the front: their releases will never be seen. - Waiting for nothing.
requestsends and returns; the reply is picked up by a later tick. The loop never blocks on the firmware, so a slow firmware cannot freeze the app. - Signals. SIGINT, SIGTERM, SIGHUP and SIGQUIT go through the same
select, soCloseruns on the loop's goroutine like everything else.
Where it lives in the code
internal/device/pocketbook/app.go · runThe select: channels, signals and the 20 ms ticker.internal/device/pocketbook/app.go · RepaintRepaint, Exit, PreventSleepFor: one-slot channels, safe from any goroutine.internal/device/pocketbook/event_loop.go · pollOne tick: replies, task messages, foreground, input, repeats, long presses.internal/device/pocketbook/touch.go · touchPointRaw touch coordinates to screen ones.internal/device/pocketbook/touch.go · handleTouchDown, move, up; the dead zone and drags.internal/device/pocketbook/touch.go · sendLongPressLong press after 500 ms, hold every 500 ms.internal/device/pocketbook/keys.go · repeatHeldKeyKey repeats: 600 ms, then every 300 ms.
The full story: E-ink and the firmware, and for what the app does with these events, App threading.
Next: all of this can run on your computer, with no reader at all. That is the emulator.