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 <public@hansmi.ch>
pull/38/head
Michael Hanselmann 4 years ago
parent 1ade23a46c
commit fae68919b9

@ -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

@ -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=

@ -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)
}
}

@ -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)
}
})
}
}

@ -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{}

@ -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)
}
})
}
}
Loading…
Cancel
Save