Talking to the firmware
The firmware and the apps share three things: a whiteboard that says who is in front, a public mailbox of touches and key presses that only the app in front may open, and pigeonholes sorted by recipient for news, requests and replies.
On the reader, Inkshelf is not alone. A program called pocketbook, the firmware, runs as root
and owns the device: it starts apps, reads the touch panel and the buttons, decides who is in
front, turns Wi-Fi on, and puts the reader to sleep. Apps run as an ordinary user. They may write
the framebuffer, but they cannot read the touch panel or the buttons themselves: every touch
comes through the firmware.
Without the SDK, Inkshelf has to speak the firmware's language directly. That language is three inter-process communicationIPC: Inter-process communication: how the app and the reader's firmware talk, through shared memory and message queues. objects, and a few rules.
Think of a small post office. On the wall, a whiteboard says which app is at the counter. By the door, a public mailbox collects every touch and key press, and only the app at the counter may take letters from it. Behind the counter, pigeonholes sorted by name hold the letters for each app, the requests to the firmware, and the firmware's replies.
Several apps, one in front
The firmware launches every app as a task, and knows it by its process group id (pgid). Several tasks can be open at once: press Home and another app comes to the front, while Inkshelf waits in the background. Only the foregroundForeground: The app the reader shows. Only the foreground app may read input and refresh the screen. app may read input and draw on the panel. Breaking that rule means stealing another app's touches, or scribbling over its screen.
The whiteboard: global state
The first object is shared memoryShared memory: Memory two processes see at once. The firmware publishes its state there, such as which app is in the foreground.: 0x44d8 bytes, found by the key 0xa12302, that every process can map. The firmware writes in it; Inkshelf only reads, at a few offsets learned from libinkview:
| Offset | Size | Meaning |
|---|---|---|
0x02 | byte | non-zero while the firmware suppresses refreshes |
0x28 | int32 | the pgid of the foreground task |
0x30 | int32 | battery charge, in percent |
0x34 | int32 | bit 0 set while charging |
0x3c | int32 | 0x544d when task switching is on |
From it, one small function decides whether the app may touch the screen:
func (reader *readerFirmware) visible() bool {
if reader.global == nil { return true }
if reader.global[globalSuppressed] != 0 { return false }
return int32At(reader.global, globalMultitask) != multitaskMagic ||
int(int32At(reader.global, globalForeground)) == reader.pgid
}
flush asks it before every refresh, and 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. (the next
chapter) asks it at every tick, every 20 ms.
The public mailbox: /hwevent
Input arrives on a POSIX message queueMessage queue: A mailbox in the kernel: one process posts messages, another reads them. Inkshelf reads input and firmware events from two. named /hwevent. Each
message is 32 bytes: eight int32s, {type, touch id, key code or raw x, raw y, pressure, tool, seconds, microseconds}. Inkshelf reads the first four. The firmware sends only presses and
releases (types 25 and 26) and touches down, move and up (30, 31, 29); the event loop makes
everything else, as the next chapter shows.
There is one /hwevent for all apps. Whoever reads a message takes it. So Inkshelf reads it
only while it is in front; otherwise it would swallow the touches meant for the app you are
using. And when Inkshelf starts, it first empties the queue of input that was meant for the app
in front before it (dropStaleEvents).
The pigeonholes: the task queue
The third object is a System V message queue, key 0xa1230f, shared by the firmware and every app. A System V queue has a trick that makes it a set of pigeonholes: every message starts with a number, its mtype, and a reader can ask for "the next message of mtype N" only. The mtype says who a message is for:
| mtype | Pigeonhole of |
|---|---|
0x7a000000 | pgid | news for one task (the firmware's task events) |
0x7fffffff | requests to the firmware |
| a request's id | the firmware's reply to that request |
A task message is a header, {mtype, id, type, argument, length}, and maybe data. Its type says
what it is:
- an async event (type
0x50d): news, no answer expected. Its data is{event, 0, code, value}. - a sync event (type
0x513): the same, but the firmware waits for an answer before it goes on. The event loop answers first thing, withtaskReply(id), then handles the event. - a request from the app, such as
taskBanSuspend(0x123, "do not sleep for N seconds"), sent with mtype0x7fffffff. Its id holds the app's pid in bits 12 to 27 and a serial number in bits 0 to 11, so the reply comes back in a pigeonhole nobody else uses.
The events Inkshelf cares about:
| Event | Number | What Inkshelf does |
|---|---|---|
| exit | 22 | closes the app (Close) and returns |
| foreground, background | 151, 152 | nothing: it watches the whiteboard instead |
| save state | 155 | App.System(SaveState): saves the reading position |
| storage changed | 73 | App.System(StorageChanged) |
| USB storage in, out | 163, 164 | saves the position before the computer takes the storage |
| network up, down | 256, 257 | network up: sends the progress read offline |
Why ignore the foreground and background events, when they say exactly what we want to know?
Because the whiteboard says more: it also covers the moments when the firmware suppresses
refreshes. followForeground reads visible() at every tick and acts when it changes.
Try the post office
Requests, and why replies must be read
The only request Inkshelf sends today is "do not sleep". The reader suspends itself after a few
minutes without a touch, which would cut a download in the middle. So the download code calls
PreventSleepFor(1 minute), and again every 30 seconds while it works.
PreventSleepFor can be called from any GoroutineGoroutine: Go’s lightweight thread. Inkshelf starts them only from the main thread, with state.Go., but only the event loop touches
the queues:
the call just drops the duration in a channel. The loop sends the request and remembers its id.
Then, at each tick, readReplies looks for the reply with that id. It is not curious about the
answer: it reads it because nobody else would. An unread reply stays in the firmware's queue
forever. After 30 seconds without one, the loop forgets the request.
Going deeper
- How these numbers were found. Event numbers match the 6.5 SDK's
inkview.h; older headers number some differently. The keys, offsets and reply format come from disassembling libinkview (hw_ipcrequest,hw_ipcreply) and probing on the reader. A sync event is answered with kind 0 and argument 1, exactly as libinkview does: what the firmware makes of those two numbers is unknown, only that it waits for them. - The canvas is a fourth object. Each task also gets its own shared memory for its canvas,
located by two small files in
/var/run/task/<pgid>/(fbshmkeyandfbshmsize). See Running Go on a PocketBook. - Missing pieces are not fatal. If the global state, the task queue or
/hweventcannot be opened, the app logs it and runs: without the whiteboard it considers itself always visible. - Signals. The task switcher may stop a background app with SIGINT.
Runcatches SIGINT, SIGTERM, SIGHUP and SIGQUIT and callsClose, so the app saves its state first. - Wi-Fi. Turning Wi-Fi on does not go through the queues: like libinkview's
NetConnect,KeepWiFiOnruns the firmware's own tool,/ebrmain/bin/netagent net on, then waits up to 40 s for an interface with an IPv4 address. The tool also fills/etc/resolv.conf, so Go's own DNS resolver works.
Where it lives in the code
internal/device/pocketbook/firmware_shared_memory.goThe keys of the three objects and the offsets in the global state.internal/device/pocketbook/firmware_messages.goThe byte layouts of input and task messages, event numbers, taskReply, taskRequest, requestID.internal/device/pocketbook/firmware_linux_arm.go · visibleReads the whiteboard: in front, and refreshes not suppressed.internal/device/pocketbook/event_loop.go · handleTaskMessageAnswers sync events, passes system events to the app.internal/device/pocketbook/event_loop.go · readRepliesCollects the replies to requests, so they do not pile up.internal/device/pocketbook/system_events.go · systemEventsWhich firmware events reach App.System.internal/device/pocketbook/syscall_linux_arm.goThe raw system calls package syscall lacks: msgrcv, msgsnd, shmat, mq_open…internal/device/pocketbook/wifi_linux_arm.go · KeepWiFiOnWi-Fi through the firmware’s netagent.
The full story: E-ink and the firmware.
Next: something has to check these mailboxes all the time. That is the event loop.