diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1dc122b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM ubuntu:xenial + +RUN apt-get update && \ + apt-get install -y xz-utils build-essential libc6-i386 wget nano && \ + apt-get clean + +# download Pocketbook SDK +RUN wget https://storage.googleapis.com/dennwc-public/pbsdk-linux-1.1.0.deb -qO /tmp/pbsdk-linux.deb && \ + dpkg -i /tmp/pbsdk-linux.deb && \ + rm /tmp/pbsdk-linux.deb + +ADD ./patches/* /tmp/ + +# download specified Go binary release that will act as a bootstrap compiler for Go toolchain +# download sources for that release and apply the patch +# build a new toolchain and remove an old one +RUN wget https://dl.google.com/go/go1.9.4.linux-amd64.tar.gz -qO /tmp/go.tar.gz && \ + tar -xf /tmp/go.tar.gz && \ + rm /tmp/go.tar.gz && \ + wget https://dl.google.com/go/go1.9.4.src.tar.gz -qO /tmp/go.tar.gz && \ + mkdir -p /gosrc && tar -xf /tmp/go.tar.gz -C /gosrc && \ + rm /tmp/go.tar.gz && \ + patch /gosrc/go/src/cmd/go/internal/work/build.go < /tmp/go-pb.patch && \ + patch /gosrc/go/src/net/dnsconfig_unix.go < /tmp/dns-pb.patch && \ + cd /gosrc/go/src && GOROOT_BOOTSTRAP=/go ./make.bash && \ + rm -r /go && mv /gosrc/go /go && rm -r /gosrc + +WORKDIR /app +VOLUME /app + +ADD build.sh / +ENTRYPOINT ["/build.sh"] + +ADD ./* /gopath/src/github.com/dennwc/inkview/ \ No newline at end of file diff --git a/README.md b/README.md index f013751..1e15758 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,63 @@ -Go SDK for Pocketbook based on libinkview. +# Go SDK for Pocketbook -To build your app or examples, run: +Unofficial Go SDK for Pocketbook based on libinkview. + +Supports graphical user interfaces and CLI apps. + +## Build a CLI app + +Standard Go compiler should be able to cross-compile the binary +for the device (no need for SDK): + +``` +GOOS=linux GOARCH=arm GOARM=5 go build main.go +``` + +Note that some additional workarounds are necessary if you want to access +a network from your app. In this case you may still need SDK. + +Although this binary will run on the device, you will need a third-party +application to actually see an output of you program (like +[pbterm](http://users.physik.fu-berlin.de/~jtt/PB/)). + +The second option is to wrap the program into `RunCLI` - it will +emulate terminal output and write it to device display. + +## Build an app with UI + +To build your app or any example, run (requires Docker): ```bash -cd ./go_app_path/ +cd ./examples/devinfo/ docker run --rm -v $PWD:/app dennwc/pocketbook-go-sdk main.go -``` \ No newline at end of file +``` + +You may also need to mount GOPATH to container to build your app: + +``` +docker run --rm -v $PWD:/app -v $GOPATH:/gopath dennwc/pocketbook-go-sdk main.go +``` + +To run an binary, copy it into `applications/app-name.app` folder +on the device and it should appear in the applications list. + +## Notes on networking + +By default, device will try to shutdown network interface to save battery, +thus you will need to call SDK functions to keep device online (see `KeepNetwork`). + +Also note that establishing TLS will require Go to read system +certificate pool that might take up to 30 sec on some devices and will +lead to TLS handshake timeouts. You will need to call `InitCerts` first +to fix the problem. + +IPv6 is not enabled on some devices, thus a patch to Go DNS lib is required +to skip lookup on IPv6 address (SDK already includes the patch). +Similar problems may arise when trying to dial IPv6 directly. + +## Notes on workdir + +Application will have a working directory set to FS root, and not to +a parent directory. +To use relative paths properly change local dir to a binary's parent +directory: `os.Chdir(filepath.Dir(os.Args[0]))`. \ No newline at end of file diff --git a/app.go b/app.go new file mode 100644 index 0000000..6130e81 --- /dev/null +++ b/app.go @@ -0,0 +1,27 @@ +package ink + +type App interface { + // Init is called when application is started. + Init() error + // Close is called before exiting an application. + Close() error + + // Draw is called each time an application view should be updated. + // Can be queued by Repaint. + Draw() + + //// Show is called when application becomes active. + //// Delivered on application start and when switching from another app. + //Show() bool + //// Hide is called when application becomes inactive (switching to another app). + //Hide() bool + + // Key is called on each key-related event. + Key(e KeyEvent) bool + // Pointer is called on each pointer-related event. + Pointer(e PointerEvent) bool + // Touch is called on each touch-related event. + Touch(e TouchEvent) bool + // Orientation is called each time an orientation of device changes. + Orientation(o Orientation) bool +} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..33818d8 --- /dev/null +++ b/build.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +export GOROOT=/go +export GOPATH=/gopath +export PATH="$GOROOT/bin:$PATH" +cd /app +CC=arm-none-linux-gnueabi-gcc GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=1 go build "$@" diff --git a/certs.go b/certs.go new file mode 100644 index 0000000..d8211c1 --- /dev/null +++ b/certs.go @@ -0,0 +1,26 @@ +package ink + +import ( + "crypto/x509" + "encoding/asn1" +) + +// InitCerts will read system certificates pool. +// +// This pool is usually populated by the first call to tls.Dial or similar, +// but this operation might take up to 30 sec on some devices, leading to handshake timeout. +// +// Calling this function before dialing will fix the problem. +func InitCerts() error { + // hand-crafted fake cert that will force system pool to be populated + // but will fail with an error directly after this + cert := x509.Certificate{ + Raw: []byte{0}, + UnhandledCriticalExtensions: []asn1.ObjectIdentifier{nil}, + } + _, err := cert.Verify(x509.VerifyOptions{}) + if _, ok := err.(x509.SystemRootsError); ok { + return err + } + return nil +} diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..fcec36b --- /dev/null +++ b/cli.go @@ -0,0 +1,208 @@ +package ink + +import ( + "context" + "fmt" + "io" + "sync" + "sync/atomic" + "time" +) + +var DefaultFontHeight = 14 + +type RunFunc func(ctx context.Context, w io.Writer) error + +func newLogWriter(log *Log, update func()) *logWriter { + return &logWriter{log: log, update: update} +} + +type logWriter struct { + log *Log + update func() +} + +func (w *logWriter) Write(p []byte) (int, error) { + defer w.update() + return w.log.Write(p) +} +func (w *logWriter) draw() { + w.log.Draw() +} +func (w *logWriter) close() { + w.log.Close() +} + +func newCliApp(fnc RunFunc, c RunConfig) *cliApp { + return &cliApp{cli: fnc, conf: c} +} + +type cliApp struct { + redraws int32 // atomic + + conf RunConfig + cli RunFunc + + wg sync.WaitGroup + err error + stop func() + + log *logWriter + + stopNet func() + + rmu sync.Mutex + running bool +} + +func (app *cliApp) setRunning(v bool) { + app.rmu.Lock() + app.running = v + app.rmu.Unlock() +} + +func (app *cliApp) isRunning() bool { + app.rmu.Lock() + v := app.running + app.rmu.Unlock() + return v +} + +func (app *cliApp) redraw() { + // allow only one repaint in queue + if atomic.CompareAndSwapInt32(&app.redraws, 0, 1) { + Repaint() + } +} +func (app *cliApp) draw() { + ClearScreen() + app.log.draw() + FullUpdate() + atomic.StoreInt32(&app.redraws, 0) +} + +func (app *cliApp) println(args ...interface{}) { + fmt.Fprintln(app.log, args...) +} + +func (app *cliApp) Init() error { + ClearScreen() + l := NewLog(Pad(Screen(), 10), DefaultFontHeight) + app.log = newLogWriter(l, app.redraw) + + if app.conf.Certs { + now := time.Now() + app.println("reading certs...") + app.draw() + if err := InitCerts(); err != nil { + app.println("error reading certs:", err) + } else { + app.println("loaded certs in", time.Since(now)) + } + app.draw() + } + + if app.conf.Network { + var err error + app.stopNet, err = KeepNetwork() + if err != nil { + app.println("cannot connect to the network:", err) + } else { + app.println("network connected") + } + app.draw() + } + + app.setRunning(true) + + ctx, cancel := context.WithCancel(context.Background()) + app.stop = cancel + app.wg.Add(1) + go func() { + defer app.wg.Done() + err := app.cli(ctx, app.log) + if app.stopNet != nil { + app.stopNet() + } + app.err = err + if err != nil { + app.println("error:", err) + } + app.println("") + app.redraw() + }() + return nil +} + +func (app *cliApp) stopCli() error { + if !app.isRunning() { + return app.err + } + app.stop() + app.wg.Wait() + app.setRunning(false) + return app.err +} + +func (app *cliApp) Close() error { + err := app.stopCli() + app.log.close() + if app.stopNet != nil { + app.stopNet() + } + return err +} + +func (app *cliApp) Draw() { + app.draw() +} + +func (*cliApp) Show() bool { + return false +} + +func (*cliApp) Hide() bool { + return false +} + +func (app *cliApp) Key(e KeyEvent) bool { + if app.isRunning() || (e.Key == KeyPrev && e.State == KeyStateDown) { + Exit() + return true + } + return false +} + +func (app *cliApp) Pointer(e PointerEvent) bool { + if app.isRunning() { + Exit() + return true + } + if e.State == PointerDown { + app.redraw() + } + return true +} + +func (*cliApp) Touch(e TouchEvent) bool { + return false +} + +func (*cliApp) Orientation(o Orientation) bool { + return false +} + +type RunConfig struct { + Certs bool // initialize certificate pool + Network bool // keep networking enabled while app is running +} + +// RunCLI starts a command-line application that can write to device display. +// Context will be cancelled when application is closed. +// Provided callback can use any SDK functions. +func RunCLI(fnc RunFunc, c *RunConfig) error { + if c == nil { + c = &RunConfig{} + } + return Run(newCliApp(fnc, *c)) +} diff --git a/events.go b/events.go index 04f59f5..822799b 100644 --- a/events.go +++ b/events.go @@ -9,46 +9,21 @@ package ink import "C" import "image" -type Event interface { - isEvent() -} - -type UnknownEvent struct { - Type int - P1, P2 int -} - -func (UnknownEvent) isEvent() {} - -type InitEvent struct{} - -func (InitEvent) isEvent() {} - -type ExitEvent struct{} - -func (ExitEvent) isEvent() {} - type KeyEvent struct { Key Key State KeyState } -func (KeyEvent) isEvent() {} - type PointerEvent struct { image.Point State PointerState } -func (PointerEvent) isEvent() {} - type TouchEvent struct { image.Point State TouchState } -func (TouchEvent) isEvent() {} - type KeyState int const ( @@ -98,3 +73,12 @@ const ( KeyPrev2 = Key(C.KEY_PREV2) KeyNext2 = Key(C.KEY_NEXT2) ) + +type Orientation int + +const ( + Orientation0 = Orientation(C.ROTATE0) + Orientation90 = Orientation(C.ROTATE90) + Orientation180 = Orientation(C.ROTATE180) + Orientation270 = Orientation(C.ROTATE270) +) diff --git a/examples/devinfo/main.go b/examples/devinfo/main.go index 2967182..6339d09 100644 --- a/examples/devinfo/main.go +++ b/examples/devinfo/main.go @@ -1,43 +1,24 @@ package main import ( + "context" + "fmt" + "io" + "github.com/dennwc/inkview" ) func main() { - var ( - log *ink.Log - ) - ink.Run(func(e ink.Event) { - switch e := e.(type) { - case ink.InitEvent: - ink.ClearScreen() - log = ink.NewLog(ink.Pad(ink.Screen(), 10), 14) - - log.Println("Device:") - log.Println(ink.DeviceModel()) - log.Println(ink.SoftwareVersion()) - log.Println(ink.HwAddress()) - log.Println() - - for _, name := range ink.Connections() { - log.Printf("conn: %q", name) - } - log.Draw() + ink.RunCLI(func(ctx context.Context, w io.Writer) error { + fmt.Fprintln(w, "Device:") + fmt.Fprintln(w, ink.DeviceModel()) + fmt.Fprintln(w, ink.SoftwareVersion()) + fmt.Fprintln(w, ink.HwAddress()) + fmt.Fprintln(w) - ink.FullUpdate() - case ink.ExitEvent: - log.Close() - case ink.PointerEvent: - if e.State == ink.PointerDown { - log.Printf("click: %v", e.Point) - log.Draw() - ink.SoftUpdate() - } - case ink.KeyEvent: - if e.Key == ink.KeyPrev && e.State == ink.KeyStateDown { - ink.Close() - } + for _, name := range ink.Connections() { + fmt.Fprintf(w, "conn: %q", name) } - }) + return nil + }, nil) } diff --git a/graphics.go b/graphics.go index b183cae..baa1230 100644 --- a/graphics.go +++ b/graphics.go @@ -33,6 +33,7 @@ func colorToInt(cl color.Color) int { return (int(b>>8) & 0xff) + int((g>>8)&0xff)<<8 + int((r>>8)&0xff)<<16 } +// ClearScreen fills current canvas with white color. func ClearScreen() { C.ClearScreen() } @@ -79,7 +80,3 @@ func DrawSelection(r image.Rectangle, cl color.Color) { sz := r.Size() C.DrawSelection(C.int(r.Min.X), C.int(r.Min.Y), C.int(sz.X), C.int(sz.Y), C.int(colorToInt(cl))) } - -func Repaint() { - C.Repaint() -} diff --git a/inkview.go b/inkview.go index f33a8be..7940346 100644 --- a/inkview.go +++ b/inkview.go @@ -9,53 +9,105 @@ extern int main_handler(int t, int p1, int p2); #cgo LDFLAGS: -pthread -lpthread -linkview */ import "C" -import "image" +import ( + "image" + "sync" +) -type Handler func(e Event) +var ( + mainMu sync.Mutex + mainApp App -var mainHandler Handler + errMu sync.Mutex + mainErr error +) + +func SetErr(err error) { + errMu.Lock() + mainErr = err + errMu.Unlock() +} // Run starts main event loop. It should be called before calling any other function. -func Run(h Handler) { - if h == nil { - panic("no handler") +func Run(app App) error { + if app == nil { + panic("no app") } - mainHandler = h + mainMu.Lock() + defer mainMu.Unlock() + SetErr(nil) + + mainApp = app C.InkViewMain(C.iv_handler(C.main_handler)) + + errMu.Lock() + err := mainErr + errMu.Unlock() + return err } -//export goMainHandler -func goMainHandler(typ int, p1, p2 int) int { - var e Event = UnknownEvent{Type: typ, P1: p1, P2: p2} +func handleEvent(typ int, p1, p2 int) bool { switch typ { - case 38, 39: - return 0 case C.EVT_INIT: - e = InitEvent{} + if err := mainApp.Init(); err != nil { + mainErr = err + Exit() + } + return true case C.EVT_EXIT: - e = ExitEvent{} + if err := mainApp.Close(); err != nil { + mainErr = err + } + return true + case C.EVT_SHOW: + mainApp.Draw() + return true + //case C.EVT_HIDE: + //case C.EVT_FOREGROUND: + // return mainApp.Show() + //case C.EVT_BACKGROUND: + // return mainApp.Hide() + case C.EVT_ORIENTATION: + return mainApp.Orientation(Orientation(p1)) default: switch { case typ >= C.EVT_KEYDOWN && typ <= C.EVT_KEYREPEAT: - e = KeyEvent{Key: Key(p1), State: KeyState(typ)} + return mainApp.Key(KeyEvent{ + Key: Key(p1), + State: KeyState(typ), + }) case typ >= C.EVT_POINTERUP && typ <= C.EVT_POINTERHOLD: - e = PointerEvent{Point: image.Pt(p1, p2), State: PointerState(typ)} + return mainApp.Pointer(PointerEvent{ + Point: image.Pt(p1, p2), + State: PointerState(typ), + }) case typ >= C.EVT_TOUCHUP && typ <= C.EVT_TOUCHMOVE: - e = TouchEvent{Point: image.Pt(p1, p2), State: TouchState(typ)} + return mainApp.Touch(TouchEvent{ + Point: image.Pt(p1, p2), + State: TouchState(typ), + }) } } - mainHandler(e) - return 0 + return false } -func OpenScreen() { - C.OpenScreen() +//export goMainHandler +func goMainHandler(typ int, p1, p2 int) int { + if handleEvent(typ, p1, p2) { + return 1 + } + return 0 } -func OpenScreenExt() { - C.OpenScreenExt() -} +//func OpenScreen() { +// C.OpenScreen() +//} + +//func OpenScreenExt() { +// C.OpenScreenExt() +//} -func Close() { +// Exit can be called to exit an application event loop. +func Exit() { C.CloseApp() } diff --git a/log.go b/log.go index d28ebb9..84d5f6e 100644 --- a/log.go +++ b/log.go @@ -1,6 +1,7 @@ package ink import ( + "bytes" "fmt" "image" ) @@ -15,18 +16,51 @@ func NewLog(r image.Rectangle, sz int) *Log { type Log struct { clip image.Rectangle font *Font - lines []string - h int - Spacing int + lines []string // lines buffer + w int // font width (since we use monospaced font) + h int // font height + Spacing int // line spacing } func (l *Log) Close() { l.font.Close() l.lines = nil } - +func (l *Log) appendLine(line string) { + h := l.clip.Size().Y + lh := l.h + l.Spacing + n := h / lh + if h%lh != 0 { + n++ + } + if dn := len(l.lines) + 1 - n; dn > 0 { + // remove exceeding lines + copy(l.lines, l.lines[dn:]) + l.lines = l.lines[:len(l.lines)-dn] + } + l.lines = append(l.lines, line) +} +func (l *Log) Write(p []byte) (int, error) { + lines := bytes.Split(p, []byte{'\n'}) + if len(lines) != 0 && len(l.lines) != 0 { + // append string to the last line + li := len(l.lines) - 1 + last := l.lines[li] + l.lines = l.lines[:li] + l.appendLine(last + string(lines[0])) + lines = lines[1:] + } + // add new lines + for _, line := range lines { + l.appendLine(string(line)) + } + return len(p), nil +} func (l *Log) Draw() { l.font.SetActive(Black) + if l.w == 0 { + l.w = CharWidth('a') + } FillArea(l.clip, White) h := l.clip.Size().Y if h < l.h { @@ -37,9 +71,6 @@ func (l *Log) Draw() { s := l.lines[i] h -= l.h + l.Spacing if h < 0 { - n := len(l.lines) - copy(l.lines, l.lines[i+1:]) - l.lines = l.lines[:n-(i+1)] break } if s == "" { @@ -51,8 +82,8 @@ func (l *Log) Draw() { } func (l *Log) WriteString(s string) error { - l.lines = append(l.lines, s) - return nil + _, err := l.Write([]byte(s)) + return err } func (l *Log) Println(args ...interface{}) error { diff --git a/network.go b/network.go index 66c0426..93ef308 100644 --- a/network.go +++ b/network.go @@ -9,13 +9,17 @@ package ink import "C" import ( + "errors" "fmt" ) +// HwAddress returns device MAC address. func HwAddress() string { return C.GoString(C.GetHwAddress()) } +// Connections returns all available network connections. +// Name can be used as an argument to Connect. func Connections() []string { list := C.EnumConnections() return strArr(list) @@ -59,3 +63,26 @@ func Disconnect() error { func OpenNetworkInfo() { C.OpenNetworkInfo() } + +var ( + ErrNoConnections = errors.New("no connections available") +) + +// KeepNetwork will connect a default network interface on the device and will keep it enabled. +// Returned function can be called to disconnect an interface. +func KeepNetwork() (func(), error) { + conns := Connections() + if len(conns) == 0 { + return nil, ErrNoConnections + } + var last error + for _, c := range conns { + last = Connect(c) + if last == nil { + return func() { + _ = Disconnect() + }, nil + } + } + return nil, last +} diff --git a/patches/dns-pb.patch b/patches/dns-pb.patch new file mode 100644 index 0000000..fa618dd --- /dev/null +++ b/patches/dns-pb.patch @@ -0,0 +1,11 @@ +--- dnsconfig_unix.go 2018-02-11 00:03:52.513314071 +0200 ++++ dnsconfig_unix.go 2018-02-11 00:04:09.653412723 +0200 +@@ -15,7 +15,7 @@ + ) + + var ( +- defaultNS = []string{"127.0.0.1:53", "[::1]:53"} ++ defaultNS = []string{"127.0.0.1:53"} + getHostname = os.Hostname // variable for testing + ) + diff --git a/patches/go-pb.patch b/patches/go-pb.patch new file mode 100644 index 0000000..83968a5 --- /dev/null +++ b/patches/go-pb.patch @@ -0,0 +1,11 @@ +--- build.go 2018-02-10 21:34:30.152052311 +0200 ++++ build.go 2018-02-10 21:37:02.217443227 +0200 +@@ -3829,7 +3829,7 @@ + func (b *Builder) disableBuildID(ldflags []string) []string { + switch cfg.Goos { + case "android", "dragonfly", "linux", "netbsd": +- ldflags = append(ldflags, "-Wl,--build-id=none") ++ // ldflags = append(ldflags, "-Wl,--build-id=none") + } + return ldflags + } diff --git a/screen.go b/screen.go index ff76372..7f6f271 100644 --- a/screen.go +++ b/screen.go @@ -25,37 +25,33 @@ func Screen() image.Rectangle { } } +// Repaint puts Draw event into app's events queue. Eventually Draw method will be called on app object. +// +// Usage: Call Repaint to make app (eventually) redraw itself on the screen. +func Repaint() { + C.Repaint() +} + +// FullUpdate sends content of the whole screen buffer to display driver. Display depth is set to 2 bpp (usually) or 4 +// bpp if necessary. Function isn't synchronous i.e. it returns faster, than display is redrawn. +// Update is performed for active app (task) only, if display isn't locked and NO_DISPLAY flag in +// ivstate.uiflags isn't set. +// +// Usage: Tradeoff between quality and speed. Recommended for text and common UI elements. Not +// recommended if quality of picture (image) is required, in such case use FullUpdateHQ(). func FullUpdate() { C.FullUpdate() } +// SoftUpdate is an alternative to FullUpdate. It's effect is (almost) PartialUpdate for the whole screen. func SoftUpdate() { C.SoftUpdate() } +// PartialUpdate sends content of the given rectangle in screen buffer to display driver. Function is smart and tries to +// perform the most suitable update possible: black and white update is performed if all pixels in given rectangle +// are black and white. Otherwise grayscale update is performed. If whole screen is specified, then grayscale update is performed. func PartialUpdate(r image.Rectangle) { sz := r.Size() C.PartialUpdate(C.int(r.Min.X), C.int(r.Min.Y), C.int(sz.X), C.int(sz.Y)) } - -func PartialUpdateBW(r image.Rectangle) { - sz := r.Size() - C.PartialUpdateBW(C.int(r.Min.X), C.int(r.Min.Y), C.int(sz.X), C.int(sz.Y)) -} - -func DynamicUpdateBW(r image.Rectangle) { - sz := r.Size() - C.DynamicUpdateBW(C.int(r.Min.X), C.int(r.Min.Y), C.int(sz.X), C.int(sz.Y)) -} - -func FineUpdate() { - C.FineUpdate() -} - -func DynamicUpdate() { - C.DynamicUpdate() -} - -func FineUpdateSupported() bool { - return C.FineUpdateSupported() != 0 -}