mirror of https://github.com/periph/devices
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
parent
1ade23a46c
commit
fae68919b9
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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…
Reference in New Issue