From ecc1eed38618341f1722f28c006936e305d915e5 Mon Sep 17 00:00:00 2001 From: Michael Hanselmann Date: Sat, 18 Dec 2021 11:15:47 +0100 Subject: [PATCH] waveshare213v2: Add support for partial drawing When the destination rectangle didn't start at (0,0) or cover the whole display the Dev.Draw function would continue to update the display in full. Obviously that didn't result in a good output. This change updates the drawing logic to only update the affected area (aligned to whole bytes on the horizontal axis) and limits the amount of transferred data to the minimum needed to cover the destination rectangle. The calculation of the various offsets as well as the function sending the image data are written to support unittesting. Signed-off-by: Michael Hanselmann --- go.mod | 1 + go.sum | 4 + waveshare2in13v2/drawing.go | 130 +++++++++++++++++++++ waveshare2in13v2/drawing_test.go | 146 ++++++++++++++++++++++++ waveshare2in13v2/waveshare213v2.go | 139 +++++++--------------- waveshare2in13v2/waveshare213v2_test.go | 38 ------ 6 files changed, 322 insertions(+), 136 deletions(-) create mode 100644 waveshare2in13v2/drawing.go create mode 100644 waveshare2in13v2/drawing_test.go delete mode 100644 waveshare2in13v2/waveshare213v2_test.go diff --git a/go.mod b/go.mod index 86c7a55..e2752cd 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ module periph.io/x/devices/v3 go 1.14 require ( + github.com/google/go-cmp v0.5.6 github.com/maruel/ansi256 v1.0.2 github.com/mattn/go-colorable v0.1.12 golang.org/x/image v0.0.0-20211028202545-6944b10bf410 diff --git a/go.sum b/go.sum index eea72f7..bd3956d 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/maruel/ansi256 v1.0.2 h1:AE5gYrrZ5vQaFTTwy5vxva8Bak7p7wID3Uqu3t1j3No= @@ -14,6 +16,8 @@ golang.org/x/sys v0.0.0-20211210111614-af8b64212486 h1:5hpz5aRr+W1erYCL5JRhSUBJR golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= periph.io/x/conn/v3 v3.6.10 h1:gwU4ssmZkq1D/uz8hU91i/COo2c9DrRaS4PJZBbCd+c= periph.io/x/conn/v3 v3.6.10/go.mod h1:UqWNaPMosWmNCwtufoTSTTYhB2wXWsMRAJyo1PlxO4Q= periph.io/x/d2xx v0.0.4/go.mod h1:38Euaaj+s6l0faIRHh32a+PrjXvxFTFkPBEQI0TKg34= diff --git a/waveshare2in13v2/drawing.go b/waveshare2in13v2/drawing.go new file mode 100644 index 0000000..ce7d2f0 --- /dev/null +++ b/waveshare2in13v2/drawing.go @@ -0,0 +1,130 @@ +// Copyright 2021 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package waveshare2in13v2 + +import ( + "image" + "image/draw" + + "periph.io/x/devices/v3/ssd1306/image1bit" +) + +type controller interface { + sendCommand(byte) + sendData([]byte) +} + +// setMemoryArea configures the target drawing area (horizontal is in bytes, +// vertical in pixels). +func setMemoryArea(ctrl controller, area image.Rectangle) { + ctrl.sendCommand(dataEntryModeSetting) + ctrl.sendData([]byte{ + // Y increment, X increment; update address counter in X direction + 0b011, + }) + + ctrl.sendCommand(setRAMXAddressStartEndPosition) + ctrl.sendData([]byte{ + // Start + byte(area.Min.X), + + // End + byte(area.Max.X - 1), + }) + + ctrl.sendCommand(setRAMYAddressStartEndPosition) + ctrl.sendData([]byte{ + // Start + byte(area.Min.Y % 0xFF), + byte(area.Min.Y / 0xFF), + + // End + byte((area.Max.Y - 1) % 0xFF), + byte((area.Max.Y - 1) / 0xFF), + }) + + ctrl.sendCommand(setRAMXAddressCounter) + ctrl.sendData([]byte{byte(area.Min.X)}) + + ctrl.sendCommand(setRAMYAddressCounter) + ctrl.sendData([]byte{ + byte(area.Min.Y % 0xFF), + byte(area.Min.Y / 0xFF), + }) +} + +type drawOpts struct { + cmd byte + devSize image.Point + dstRect image.Rectangle + src image.Image + srcPts image.Point +} + +type drawSpec struct { + // Destination on display in pixels, normalized to fit into actual size. + DstRect image.Rectangle + + // Size of memory area to write; horizontally in bytes, vertically in + // pixels. + MemRect image.Rectangle + + // Size of image buffer, horizontally aligned to multiples of 8 pixels. + BufferSize image.Point + + // Destination rectangle within image buffer. + BufferRect image.Rectangle +} + +func (o *drawOpts) spec() drawSpec { + s := drawSpec{ + DstRect: image.Rectangle{Max: o.devSize}.Intersect(o.dstRect), + } + + s.MemRect = image.Rect( + s.DstRect.Min.X/8, s.DstRect.Min.Y, + (s.DstRect.Max.X+7)/8, s.DstRect.Max.Y, + ) + s.BufferSize = image.Pt(s.MemRect.Dx()*8, s.MemRect.Dy()) + s.BufferRect = image.Rectangle{ + Min: image.Point{X: s.DstRect.Min.X - (s.MemRect.Min.X * 8)}, + Max: image.Point{Y: s.DstRect.Dy()}, + } + s.BufferRect.Max.X = s.BufferRect.Min.X + s.DstRect.Dx() + + return s +} + +// drawImage sends an image to the controller after setting up the registers. +func drawImage(ctrl controller, opts *drawOpts) { + s := opts.spec() + + if s.MemRect.Empty() { + return + } + + img := image1bit.NewVerticalLSB(image.Rectangle{Max: s.BufferSize}) + draw.Src.Draw(img, s.BufferRect, opts.src, opts.srcPts) + + setMemoryArea(ctrl, s.MemRect) + + ctrl.sendCommand(opts.cmd) + + rowData := make([]byte, s.MemRect.Dx()) + + for y := 0; y < img.Bounds().Dy(); y++ { + for x := 0; x < len(rowData); x++ { + rowData[x] = 0 + + for bit := 0; bit < 8; bit++ { + if img.BitAt((x*8)+bit, y) { + rowData[x] |= 0x80 >> bit + } + } + } + + ctrl.sendData(rowData) + } +} diff --git a/waveshare2in13v2/drawing_test.go b/waveshare2in13v2/drawing_test.go new file mode 100644 index 0000000..7c5d62e --- /dev/null +++ b/waveshare2in13v2/drawing_test.go @@ -0,0 +1,146 @@ +// Copyright 2021 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package waveshare2in13v2 + +import ( + "bytes" + "image" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "periph.io/x/devices/v3/ssd1306/image1bit" +) + +type record struct { + cmd byte + data []byte +} + +type fakeController []record + +func (r *fakeController) sendCommand(cmd byte) { + *r = append(*r, record{ + cmd: cmd, + }) +} + +func (r *fakeController) sendData(data []byte) { + cur := &(*r)[len(*r)-1] + cur.data = append(cur.data, data...) +} + +func TestDrawSpec(t *testing.T) { + for _, tc := range []struct { + name string + opts drawOpts + want drawSpec + }{ + { + name: "empty", + }, + { + name: "smaller than display", + opts: drawOpts{ + devSize: image.Pt(100, 200), + dstRect: image.Rect(17, 4, 25, 8), + }, + want: drawSpec{ + DstRect: image.Rect(17, 4, 25, 8), + MemRect: image.Rect(2, 4, 4, 8), + BufferSize: image.Pt(16, 4), + BufferRect: image.Rect(1, 0, 9, 4), + }, + }, + { + name: "larger than display", + opts: drawOpts{ + devSize: image.Pt(100, 200), + dstRect: image.Rect(-20, 50, 125, 300), + }, + want: drawSpec{ + DstRect: image.Rect(0, 50, 100, 200), + MemRect: image.Rect(0, 50, 13, 200), + BufferSize: image.Pt(13*8, 150), + BufferRect: image.Rect(0, 0, 100, 150), + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + got := tc.opts.spec() + + if diff := cmp.Diff(got, tc.want, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("spec() difference (-got +want):\n%s", diff) + } + }) + } +} + +func TestDrawImage(t *testing.T) { + for _, tc := range []struct { + name string + opts drawOpts + want []record + }{ + { + name: "empty", + opts: drawOpts{ + src: &image.Uniform{image1bit.On}, + }, + }, + { + name: "partial", + opts: drawOpts{ + cmd: writeRAMBW, + devSize: image.Pt(64, 64), + dstRect: image.Rect(17, 4, 41, 8), + src: &image.Uniform{image1bit.On}, + srcPts: image.Pt(0, 0), + }, + want: []record{ + {cmd: dataEntryModeSetting, data: []byte{0x3}}, + {cmd: setRAMXAddressStartEndPosition, data: []byte{2, 6 - 1}}, + {cmd: setRAMYAddressStartEndPosition, data: []byte{4, 0, 8 - 1, 0}}, + {cmd: setRAMXAddressCounter, data: []byte{2}}, + {cmd: setRAMYAddressCounter, data: []byte{4, 0}}, + { + cmd: writeRAMBW, + data: bytes.Repeat([]byte{0x7f, 0xff, 0xff, 0x80}, 4), + }, + }, + }, + { + name: "full", + opts: drawOpts{ + cmd: writeRAMBW, + devSize: image.Pt(80, 120), + dstRect: image.Rect(0, 0, 80, 120), + src: &image.Uniform{image1bit.On}, + srcPts: image.Pt(33, 44), + }, + want: []record{ + {cmd: dataEntryModeSetting, data: []byte{0x3}}, + {cmd: setRAMXAddressStartEndPosition, data: []byte{0, 10 - 1}}, + {cmd: setRAMYAddressStartEndPosition, data: []byte{0, 0, 120 - 1, 0}}, + {cmd: setRAMXAddressCounter, data: []byte{0}}, + {cmd: setRAMYAddressCounter, data: []byte{0, 0}}, + { + cmd: writeRAMBW, + data: bytes.Repeat([]byte{0xff}, 80/8*120), + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var got fakeController + + drawImage(&got, &tc.opts) + + if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" { + t.Errorf("drawImage() difference (-got +want):\n%s", diff) + } + }) + } +} diff --git a/waveshare2in13v2/waveshare213v2.go b/waveshare2in13v2/waveshare213v2.go index 39fe37c..d5fb815 100644 --- a/waveshare2in13v2/waveshare213v2.go +++ b/waveshare2in13v2/waveshare213v2.go @@ -9,7 +9,6 @@ import ( "fmt" "image" "image/color" - "image/draw" "time" "periph.io/x/conn/v3" @@ -124,12 +123,6 @@ var EPD2in13v2 = Opts{ }, } -// dataDimensions returns the size in terms of bytes needed to fill the -// display. -func dataDimensions(opts *Opts) (int, int) { - return opts.Height, (opts.Width + 7) / 8 -} - // New creates new handler which is used to access the display. func New(p spi.Port, dc, cs, rst gpio.PinOut, busy gpio.PinIO, opts *Opts) (*Dev, error) { c, err := p.Connect(5*physic.MegaHertz, spi.Mode0, 8) @@ -257,26 +250,28 @@ func (d *Dev) Init(partialUpdate PartialUpdate) error { // Clear clears the display. func (d *Dev) Clear(color byte) error { - eh := errorHandler{d: *d} + spec := (&drawOpts{ + devSize: image.Pt(d.opts.Width, d.opts.Height), + dstRect: d.Bounds(), + }).spec() - if err := d.setMemoryArea(d.Bounds()); err != nil { - return err - } + eh := errorHandler{d: *d} - rows, cols := dataDimensions(d.opts) - data := bytes.Repeat([]byte{color}, cols) + setMemoryArea(&eh, spec.MemRect) eh.sendCommand(writeRAMBW) - for y := 0; y < rows; y++ { + data := bytes.Repeat([]byte{color}, spec.MemRect.Dy()) + + for y := 0; y < spec.MemRect.Max.Y; y++ { eh.sendData(data) } - if eh.err != nil { - return eh.err + if eh.err == nil { + eh.err = d.turnOnDisplay() } - return d.turnOnDisplay() + return eh.err } // ColorModel returns a 1Bit color model. @@ -289,62 +284,53 @@ func (d *Dev) Bounds() image.Rectangle { return image.Rect(0, 0, d.opts.Width, d.opts.Height) } -func (d *Dev) sendImage(cmd byte, dstRect image.Rectangle, src *image1bit.VerticalLSB) error { - // TODO: Handle dstRect not matching the device bounds. - - if err := d.setMemoryArea(dstRect); err != nil { - return err +// Draw draws the given image to the display. +func (d *Dev) Draw(dstRect image.Rectangle, src image.Image, srcPts image.Point) error { + opts := drawOpts{ + cmd: writeRAMBW, + devSize: image.Pt(d.opts.Width, d.opts.Height), + dstRect: dstRect, + src: src, + srcPts: srcPts, } eh := errorHandler{d: *d} - eh.sendCommand(cmd) - - rows, cols := dataDimensions(d.opts) - data := make([]byte, cols) - for y := 0; y < rows; y++ { - for x := 0; x < cols; x++ { - data[x] = 0 + drawImage(&eh, &opts) - for bit := 0; bit < 8; bit++ { - if src.BitAt((x*8)+bit, y) { - data[x] |= 0x80 >> bit - } - } - } - - eh.sendData(data) + if eh.err == nil { + eh.err = d.turnOnDisplay() } return eh.err } -// Draw draws the given image to the display. -func (d *Dev) Draw(dstRect image.Rectangle, src image.Image, srcPts image.Point) error { - next := image1bit.NewVerticalLSB(dstRect) - draw.Src.Draw(next, dstRect, src, srcPts) - - if err := d.sendImage(writeRAMBW, dstRect, next); err != nil { - return err +// DrawPartial draws the given image to the display. Display will update only changed pixel. +func (d *Dev) DrawPartial(dstRect image.Rectangle, src image.Image, srcPts image.Point) error { + opts := drawOpts{ + devSize: image.Pt(d.opts.Width, d.opts.Height), + dstRect: dstRect, + src: src, + srcPts: srcPts, } - return d.turnOnDisplay() -} + eh := errorHandler{d: *d} -// DrawPartial draws the given image to the display. Display will update only changed pixel. -func (d *Dev) DrawPartial(dstRect image.Rectangle, src image.Image, srcPts image.Point) error { - next := image1bit.NewVerticalLSB(dstRect) - draw.Src.Draw(next, dstRect, src, srcPts) + for _, cmd := range []byte{writeRAMBW, writeRAMRed} { + opts.cmd = cmd - if err := d.sendImage(writeRAMBW, dstRect, next); err != nil { - return err + drawImage(&eh, &opts) + + if eh.err != nil { + break + } } - if err := d.sendImage(writeRAMRed, dstRect, next); err != nil { - return err + if eh.err == nil { + eh.err = d.turnOnDisplay() } - return d.turnOnDisplay() + return eh.err } // Halt clears the display. @@ -383,47 +369,4 @@ func (d *Dev) reset() error { return eh.err } -func (d *Dev) setMemoryArea(area image.Rectangle) error { - eh := errorHandler{d: *d} - - eh.sendCommand(dataEntryModeSetting) - eh.sendData([]byte{ - // Y increment, X increment; update address counter in X direction - 0b011, - }) - - eh.sendCommand(setRAMXAddressStartEndPosition) - eh.sendData([]byte{ - // Start - byte(area.Min.X / 8), - - // End - byte((area.Max.X - 1) / 8), - }) - - eh.sendCommand(setRAMYAddressStartEndPosition) - eh.sendData([]byte{ - // Start - byte(area.Min.Y % 0xFF), - byte(area.Min.Y / 0xFF), - - // End - byte((area.Max.Y - 1) % 0xFF), - byte((area.Max.Y - 1) / 0xFF), - }) - - eh.sendCommand(setRAMXAddressCounter) - eh.sendData([]byte{byte(area.Min.X / 8)}) - - eh.sendCommand(setRAMYAddressCounter) - eh.sendData([]byte{ - byte(area.Min.Y & 0xFF), - byte(area.Min.Y / 0xFF), - }) - - eh.waitUntilIdle() - - return eh.err -} - var _ display.Drawer = &Dev{} diff --git a/waveshare2in13v2/waveshare213v2_test.go b/waveshare2in13v2/waveshare213v2_test.go deleted file mode 100644 index bf21784..0000000 --- a/waveshare2in13v2/waveshare213v2_test.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2021 The Periph Authors. All rights reserved. -// Use of this source code is governed under the Apache License, Version 2.0 -// that can be found in the LICENSE file. - -package waveshare2in13v2 - -import ( - "fmt" - "testing" -) - -func TestDataDimensions(t *testing.T) { - for _, tc := range []struct { - opts *Opts - wantHeight int - wantWidth int - }{ - {opts: &Opts{Width: 0, Height: 0}}, - { - opts: &Opts{Height: 48, Width: 16}, - wantHeight: 48, - wantWidth: 2, - }, - { - opts: &Opts{Height: 250, Width: 122}, - wantHeight: 250, - wantWidth: 16, - }, - } { - t.Run(fmt.Sprintf("%+v", *tc.opts), func(t *testing.T) { - gotHeight, gotWidth := dataDimensions(tc.opts) - - if !(gotHeight == tc.wantHeight && gotWidth == tc.wantWidth) { - t.Errorf("dataDimensions(%#v) returned %d, %d; want %d, %d", tc.opts, gotHeight, gotWidth, tc.wantHeight, tc.wantWidth) - } - }) - } -}