waveshare2in13v2: Support rotation of image (#46)

* waveshare2in13v2: Populate bounds only once

The forthcoming introduction of image orientation will make the
computation slightly more complicated. The bounds are fixed during the
lifetime of a "Dev" instance, so it's better to retain them.

* waveshare2in13v2: Make sending image part of drawOpts

* waveshare2in13v2: Unexport drawSpec members

The members of the "drawSpec" structure are only for internal use, so
rename them to start with a lower-case letter.

* waveshare2in13v2: Implement support for rotating image

A new option named "Origin" controls which of the four corners of the
display should be used as the (0,0) position for the image. The
in-memory buffer is kept in logical orientation. The actual rotation
happens only when sending updates.

By aligning the buffer accordingly the code is already prepared for
a more efficient sending of image data for the "TopRight" and
"BottomLeft" origins. See the TODO comment on "drawSpec.BufferDstOffset"
for details.

Signed-off-by: Michael Hanselmann <public@hansmi.ch>
pull/47/head
hansmi 4 years ago committed by GitHub
parent 394037d235
commit 0012149b5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -44,6 +44,7 @@ func setMemoryArea(ctrl controller, area image.Rectangle) {
type drawOpts struct { type drawOpts struct {
commands []byte commands []byte
devSize image.Point devSize image.Point
origin Corner
buffer *image1bit.VerticalLSB buffer *image1bit.VerticalLSB
dstRect image.Rectangle dstRect image.Rectangle
src image.Image src image.Image
@ -51,47 +52,142 @@ type drawOpts struct {
} }
type drawSpec struct { type drawSpec struct {
// Destination on display in pixels, normalized to fit into actual size. // Amount by which buffer contents must be moved to align with the physical
DstRect image.Rectangle // top-left corner of the display.
//
// TODO: The offset shifts the buffer contents to be aligned such that the
// translated position of the physical, on-display (0,0) location is at
// a multiple of 8 on the equivalent to the physical X axis. With a bit of
// additional work transfers for the TopRight and BottomLeft origins should
// not require per-pixel processing by exploiting image1bit.VerticalLSB's
// underlying pixel storage format.
bufferDstOffset image.Point
// Destination in buffer in pixels.
bufferDstRect image.Rectangle
// Destination in device RAM, rotated and shifted to match the origin.
memDstRect image.Rectangle
// Area to send to device; horizontally in bytes (thus aligned to // Area to send to device; horizontally in bytes (thus aligned to
// 8 pixels), vertically in pixels. // 8 pixels), vertically in pixels. Computed from memDstRect.
MemRect image.Rectangle memRect image.Rectangle
} }
// spec pre-computes the various offsets required for sending image updates to
// the device.
func (o *drawOpts) spec() drawSpec { func (o *drawOpts) spec() drawSpec {
s := drawSpec{ s := drawSpec{
DstRect: image.Rectangle{Max: o.devSize}.Intersect(o.dstRect), bufferDstRect: image.Rectangle{Max: o.devSize}.Intersect(o.dstRect),
} }
s.MemRect = image.Rect( switch o.origin {
s.DstRect.Min.X/8, s.DstRect.Min.Y, case TopRight:
(s.DstRect.Max.X+7)/8, s.DstRect.Max.Y, s.bufferDstOffset.Y = o.buffer.Bounds().Dy() - o.devSize.Y
) case BottomRight:
s.bufferDstOffset.Y = o.buffer.Bounds().Dy() - o.devSize.Y
s.bufferDstOffset.X = o.buffer.Bounds().Dx() - o.devSize.X
case BottomLeft:
s.bufferDstOffset.Y = o.buffer.Bounds().Dy() - o.devSize.Y
s.bufferDstOffset.X = o.buffer.Bounds().Dx() - o.devSize.X
}
if !s.bufferDstRect.Empty() {
switch o.origin {
case TopLeft:
s.memDstRect = s.bufferDstRect
case TopRight:
s.memDstRect.Min.X = o.devSize.Y - s.bufferDstRect.Max.Y
s.memDstRect.Max.X = o.devSize.Y - s.bufferDstRect.Min.Y
s.memDstRect.Min.Y = s.bufferDstRect.Min.X
s.memDstRect.Max.Y = s.bufferDstRect.Max.X
case BottomRight:
s.memDstRect.Min.X = o.devSize.X - s.bufferDstRect.Max.X
s.memDstRect.Max.X = o.devSize.X - s.bufferDstRect.Min.X
s.memDstRect.Min.Y = o.devSize.Y - s.bufferDstRect.Max.Y
s.memDstRect.Max.Y = o.devSize.Y - s.bufferDstRect.Min.Y
case BottomLeft:
s.memDstRect.Min.X = s.bufferDstRect.Min.Y
s.memDstRect.Max.X = s.bufferDstRect.Max.Y
s.memDstRect.Min.Y = o.devSize.X - s.bufferDstRect.Max.X
s.memDstRect.Max.Y = o.devSize.X - s.bufferDstRect.Min.X
}
s.bufferDstRect = s.bufferDstRect.Add(s.bufferDstOffset)
s.memRect.Min.X = s.memDstRect.Min.X / 8
s.memRect.Max.X = (s.memDstRect.Max.X + 7) / 8
s.memRect.Min.Y = s.memDstRect.Min.Y
s.memRect.Max.Y = s.memDstRect.Max.Y
}
return s return s
} }
// sendImage sends an image to the controller after setting up the registers. // sendImage sends an image to the controller after setting up the registers.
// The area is in bytes on the horizontal axis. func (o *drawOpts) sendImage(ctrl controller, cmd byte, spec *drawSpec) {
func sendImage(ctrl controller, cmd byte, area image.Rectangle, img *image1bit.VerticalLSB) { if spec.memRect.Empty() {
if area.Empty() {
return return
} }
setMemoryArea(ctrl, area) setMemoryArea(ctrl, spec.memRect)
ctrl.sendCommand(cmd) ctrl.sendCommand(cmd)
rowData := make([]byte, area.Dx()) var posFor func(destY, destX, bit int) image.Point
for y := area.Min.Y; y < area.Max.Y; y++ { switch o.origin {
for x := 0; x < len(rowData); x++ { case TopLeft:
rowData[x] = 0 posFor = func(destY, destX, bit int) image.Point {
return image.Point{
X: destX + bit,
Y: destY,
}
}
case TopRight:
posFor = func(destY, destX, bit int) image.Point {
return image.Point{
X: destY,
Y: o.devSize.Y - destX - bit - 1,
}
}
case BottomRight:
posFor = func(destY, destX, bit int) image.Point {
return image.Point{
X: o.devSize.X - destX - bit - 1,
Y: o.devSize.Y - destY - 1,
}
}
case BottomLeft:
posFor = func(destY, destX, bit int) image.Point {
return image.Point{
X: o.devSize.X - destY - 1,
Y: destX + bit,
}
}
}
rowData := make([]byte, spec.memRect.Dx())
for destY := spec.memRect.Min.Y; destY < spec.memRect.Max.Y; destY++ {
for destX := 0; destX < len(rowData); destX++ {
rowData[destX] = 0
for bit := 0; bit < 8; bit++ { for bit := 0; bit < 8; bit++ {
if img.BitAt(((area.Min.X+x)*8)+bit, y) { bufPos := posFor(destY, (spec.memRect.Min.X+destX)*8, bit)
rowData[x] |= 0x80 >> bit bufPos = bufPos.Add(spec.bufferDstOffset)
if o.buffer.BitAt(bufPos.X, bufPos.Y) {
rowData[destX] |= 0x80 >> bit
} }
} }
} }
@ -103,11 +199,13 @@ func sendImage(ctrl controller, cmd byte, area image.Rectangle, img *image1bit.V
func drawImage(ctrl controller, opts *drawOpts) { func drawImage(ctrl controller, opts *drawOpts) {
s := opts.spec() s := opts.spec()
if s.MemRect.Empty() { if s.memRect.Empty() {
return return
} }
draw.Src.Draw(opts.buffer, s.DstRect, opts.src, opts.srcPts) // The buffer is kept in logical orientation. Rotation and alignment with
// the origin happens while sending the image data.
draw.Src.Draw(opts.buffer, s.bufferDstRect, opts.src, opts.srcPts)
commands := opts.commands commands := opts.commands
@ -117,6 +215,6 @@ func drawImage(ctrl controller, opts *drawOpts) {
// Keep the two buffers in sync. // Keep the two buffers in sync.
for _, cmd := range commands { for _, cmd := range commands {
sendImage(ctrl, cmd, s.MemRect, opts.buffer) opts.sendImage(ctrl, cmd, &s)
} }
} }

@ -15,14 +15,25 @@ import (
"periph.io/x/devices/v3/ssd1306/image1bit" "periph.io/x/devices/v3/ssd1306/image1bit"
) )
func checkRectCanon(t *testing.T, got image.Rectangle) {
if diff := cmp.Diff(got, got.Canon()); diff != "" {
t.Errorf("Rectangle is not canonical (-got +want):\n%s", diff)
}
}
func TestDrawSpec(t *testing.T) { func TestDrawSpec(t *testing.T) {
for _, tc := range []struct { type testCase struct {
name string name string
opts drawOpts opts drawOpts
want drawSpec want drawSpec
}{ }
for _, tc := range []testCase{
{ {
name: "empty", name: "empty",
opts: drawOpts{
buffer: image1bit.NewVerticalLSB(image.Rectangle{}),
},
}, },
{ {
name: "smaller than display", name: "smaller than display",
@ -32,8 +43,9 @@ func TestDrawSpec(t *testing.T) {
dstRect: image.Rect(17, 4, 25, 8), dstRect: image.Rect(17, 4, 25, 8),
}, },
want: drawSpec{ want: drawSpec{
DstRect: image.Rect(17, 4, 25, 8), bufferDstRect: image.Rect(17, 4, 25, 8),
MemRect: image.Rect(2, 4, 4, 8), memDstRect: image.Rect(17, 4, 25, 8),
memRect: image.Rect(2, 4, 4, 8),
}, },
}, },
{ {
@ -44,15 +56,261 @@ func TestDrawSpec(t *testing.T) {
dstRect: image.Rect(-20, 50, 125, 300), dstRect: image.Rect(-20, 50, 125, 300),
}, },
want: drawSpec{ want: drawSpec{
DstRect: image.Rect(0, 50, 100, 200), bufferDstRect: image.Rect(0, 50, 100, 200),
MemRect: image.Rect(0, 50, 13, 200), memDstRect: image.Rect(0, 50, 100, 200),
memRect: image.Rect(0, 50, 13, 200),
}, },
}, },
func() testCase {
tc := testCase{
name: "origin top left full",
opts: drawOpts{
devSize: image.Pt(48, 96),
origin: TopLeft,
buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 6*8, 12*8)),
dstRect: image.Rect(0, 0, 48, 96),
},
}
tc.want.bufferDstRect.Max = image.Pt(48, 96)
tc.want.memDstRect.Max = image.Pt(48, 96)
tc.want.memRect.Max = image.Pt(6, 96)
return tc
}(),
func() testCase {
tc := testCase{
name: "origin top right, empty dest",
opts: drawOpts{
devSize: image.Pt(105, 50),
origin: TopRight,
buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 12*8, 8*8)),
},
}
tc.want.bufferDstOffset.Y = tc.opts.buffer.Bounds().Dy() - tc.opts.devSize.Y
return tc
}(),
func() testCase {
tc := testCase{
name: "origin top right",
opts: drawOpts{
devSize: image.Pt(100, 50),
origin: TopRight,
buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 12*8, 8*8)),
dstRect: image.Rect(0, 0, 20, 30),
},
}
tc.want.bufferDstOffset.Y = tc.opts.buffer.Bounds().Dy() - tc.opts.devSize.Y
tc.want.bufferDstRect = image.Rectangle{
Min: tc.want.bufferDstOffset,
Max: image.Point{
X: tc.opts.dstRect.Max.X,
Y: tc.want.bufferDstOffset.Y + tc.opts.dstRect.Max.Y,
},
}
tc.want.memDstRect = image.Rectangle{
Min: image.Point{
X: tc.opts.devSize.Y - tc.opts.dstRect.Max.Y,
},
Max: image.Point{
X: tc.opts.devSize.Y,
Y: tc.opts.dstRect.Max.X,
},
}
tc.want.memRect = image.Rectangle{
Min: image.Pt(2, tc.want.memDstRect.Min.Y),
Max: image.Pt(7, tc.want.memDstRect.Max.Y),
}
return tc
}(),
func() testCase {
tc := testCase{
name: "origin top right full",
opts: drawOpts{
devSize: image.Pt(48, 96),
origin: TopRight,
buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 6*8, 12*8)),
dstRect: image.Rect(0, 0, 48, 96),
},
}
tc.want.bufferDstRect.Max = image.Pt(48, 96)
tc.want.memDstRect.Max = image.Pt(96, 48)
tc.want.memRect.Max = image.Pt(12, 48)
return tc
}(),
func() testCase {
tc := testCase{
name: "origin top right with offset",
opts: drawOpts{
devSize: image.Pt(101, 83),
origin: TopRight,
buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 14*8, 11*8)),
dstRect: image.Rect(9, 17, 19, 27),
},
}
tc.want.bufferDstOffset.Y = tc.opts.buffer.Bounds().Dy() - tc.opts.devSize.Y
tc.want.bufferDstRect = image.Rectangle{
Min: image.Point{
X: tc.opts.dstRect.Min.X,
Y: tc.want.bufferDstOffset.Y + tc.opts.dstRect.Min.Y,
},
Max: image.Point{
X: tc.opts.dstRect.Max.X,
Y: tc.want.bufferDstOffset.Y + tc.opts.dstRect.Max.Y,
},
}
tc.want.memDstRect = image.Rectangle{
Min: image.Point{
X: tc.opts.devSize.Y - tc.opts.dstRect.Max.Y,
Y: tc.opts.dstRect.Min.X,
},
Max: image.Point{
X: tc.opts.devSize.Y - tc.opts.dstRect.Min.Y,
Y: tc.opts.dstRect.Max.X,
},
}
tc.want.memRect = image.Rectangle{
Min: image.Pt(7, tc.want.memDstRect.Min.Y),
Max: image.Pt(9, tc.want.memDstRect.Max.Y),
}
return tc
}(),
func() testCase {
tc := testCase{
name: "origin bottom right full",
opts: drawOpts{
devSize: image.Pt(48, 96),
origin: BottomRight,
buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 6*8, 12*8)),
dstRect: image.Rect(0, 0, 48, 96),
},
}
tc.want.bufferDstRect.Max = image.Pt(48, 96)
tc.want.memDstRect.Max = image.Pt(48, 96)
tc.want.memRect.Max = image.Pt(6, 96)
return tc
}(),
func() testCase {
tc := testCase{
name: "origin bottom right with offset",
opts: drawOpts{
devSize: image.Pt(75, 103),
origin: BottomRight,
buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 10*8, 14*8)),
dstRect: image.Rect(9, 17, 19, 49),
},
}
tc.want.bufferDstOffset = image.Point{
X: tc.opts.buffer.Bounds().Dx() - tc.opts.devSize.X,
Y: tc.opts.buffer.Bounds().Dy() - tc.opts.devSize.Y,
}
tc.want.bufferDstRect = image.Rectangle{
Min: image.Point{
X: tc.want.bufferDstOffset.X + tc.opts.dstRect.Min.X,
Y: tc.want.bufferDstOffset.Y + tc.opts.dstRect.Min.Y,
},
Max: image.Point{
X: tc.want.bufferDstOffset.X + tc.opts.dstRect.Max.X,
Y: tc.want.bufferDstOffset.Y + tc.opts.dstRect.Max.Y,
},
}
tc.want.memDstRect = image.Rectangle{
Min: image.Point{
X: tc.opts.devSize.X - tc.opts.dstRect.Max.X,
Y: tc.opts.devSize.Y - tc.opts.dstRect.Max.Y,
},
Max: image.Point{
X: tc.opts.devSize.X - tc.opts.dstRect.Min.X,
Y: tc.opts.devSize.Y - tc.opts.dstRect.Min.Y,
},
}
tc.want.memRect = image.Rectangle{
Min: image.Pt(7, tc.want.memDstRect.Min.Y),
Max: image.Pt(9, tc.want.memDstRect.Max.Y),
}
return tc
}(),
func() testCase {
tc := testCase{
name: "origin bottom left full",
opts: drawOpts{
devSize: image.Pt(48, 96),
origin: BottomLeft,
buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 6*8, 12*8)),
dstRect: image.Rect(0, 0, 48, 96),
},
}
tc.want.bufferDstRect.Max = image.Pt(48, 96)
tc.want.memDstRect.Max = image.Pt(96, 48)
tc.want.memRect.Max = image.Pt(12, 48)
return tc
}(),
func() testCase {
tc := testCase{
name: "origin bottom left with offset",
opts: drawOpts{
devSize: image.Pt(101, 81),
origin: BottomLeft,
buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 15*8, 11*8)),
dstRect: image.Rect(9, 17, 21, 49),
},
}
tc.want.bufferDstOffset = image.Point{
X: tc.opts.buffer.Bounds().Dx() - tc.opts.devSize.X,
Y: tc.opts.buffer.Bounds().Dy() - tc.opts.devSize.Y,
}
tc.want.bufferDstRect = image.Rectangle{
Min: image.Point{
X: tc.want.bufferDstOffset.X + tc.opts.dstRect.Min.X,
Y: tc.want.bufferDstOffset.Y + tc.opts.dstRect.Min.Y,
},
Max: image.Point{
X: tc.want.bufferDstOffset.X + tc.opts.dstRect.Max.X,
Y: tc.want.bufferDstOffset.Y + tc.opts.dstRect.Max.Y,
},
}
tc.want.memDstRect = image.Rectangle{
Min: image.Point{
X: tc.opts.dstRect.Min.Y,
Y: tc.opts.devSize.X - tc.opts.dstRect.Max.X,
},
Max: image.Point{
X: tc.opts.dstRect.Max.Y,
Y: tc.opts.devSize.X - tc.opts.dstRect.Min.X,
},
}
tc.want.memRect = image.Rectangle{
Min: image.Pt(2, tc.want.memDstRect.Min.Y),
Max: image.Pt(7, tc.want.memDstRect.Max.Y),
}
return tc
}(),
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
checkRectCanon(t, tc.opts.dstRect)
got := tc.opts.spec() got := tc.opts.spec()
if diff := cmp.Diff(got, tc.want, cmpopts.EquateEmpty()); diff != "" { checkRectCanon(t, got.bufferDstRect)
checkRectCanon(t, got.memRect)
if diff := cmp.Diff(got, tc.want, cmp.AllowUnexported(drawSpec{})); diff != "" {
t.Errorf("spec() difference (-got +want):\n%s", diff) t.Errorf("spec() difference (-got +want):\n%s", diff)
} }
}) })
@ -63,18 +321,23 @@ func TestSendImage(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
name string name string
cmd byte cmd byte
area image.Rectangle opts drawOpts
img *image1bit.VerticalLSB
want []record want []record
}{ }{
{ {
name: "empty", name: "empty",
opts: drawOpts{
buffer: image1bit.NewVerticalLSB(image.Rectangle{}),
},
}, },
{ {
name: "partial", name: "partial",
cmd: writeRAMBW, cmd: writeRAMBW,
area: image.Rect(2, 20, 4, 40), opts: drawOpts{
img: image1bit.NewVerticalLSB(image.Rect(0, 0, 64, 64)), devSize: image.Pt(64, 64),
dstRect: image.Rect(16, 20, 32, 40),
buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 64, 64)),
},
want: []record{ want: []record{
{cmd: dataEntryModeSetting, data: []byte{0x3}}, {cmd: dataEntryModeSetting, data: []byte{0x3}},
{cmd: setRAMXAddressStartEndPosition, data: []byte{2, 4 - 1}}, {cmd: setRAMXAddressStartEndPosition, data: []byte{2, 4 - 1}},
@ -90,12 +353,15 @@ func TestSendImage(t *testing.T) {
{ {
name: "partial non-aligned", name: "partial non-aligned",
cmd: writeRAMRed, cmd: writeRAMRed,
area: image.Rect(2, 4, 6, 8), opts: drawOpts{
img: func() *image1bit.VerticalLSB { devSize: image.Pt(100, 64),
img := image1bit.NewVerticalLSB(image.Rect(0, 0, 64, 64)) dstRect: image.Rect(17, 4, 41, 8),
draw.Src.Draw(img, image.Rect(17, 4, 41, 8), &image.Uniform{image1bit.On}, image.Point{}) buffer: func() *image1bit.VerticalLSB {
return img img := image1bit.NewVerticalLSB(image.Rect(0, 0, 64, 64))
}(), draw.Src.Draw(img, image.Rect(17, 4, 41, 8), &image.Uniform{image1bit.On}, image.Point{})
return img
}(),
},
want: []record{ want: []record{
{cmd: dataEntryModeSetting, data: []byte{0x3}}, {cmd: dataEntryModeSetting, data: []byte{0x3}},
{cmd: setRAMXAddressStartEndPosition, data: []byte{2, 6 - 1}}, {cmd: setRAMXAddressStartEndPosition, data: []byte{2, 6 - 1}},
@ -111,12 +377,15 @@ func TestSendImage(t *testing.T) {
{ {
name: "full", name: "full",
cmd: writeRAMBW, cmd: writeRAMBW,
area: image.Rect(0, 0, 10, 120), opts: drawOpts{
img: func() *image1bit.VerticalLSB { devSize: image.Pt(80, 120),
img := image1bit.NewVerticalLSB(image.Rect(0, 0, 80, 120)) dstRect: image.Rect(0, 0, 80, 120),
draw.Src.Draw(img, image.Rect(0, 0, 80, 120), &image.Uniform{image1bit.On}, image.Point{}) buffer: func() *image1bit.VerticalLSB {
return img img := image1bit.NewVerticalLSB(image.Rect(0, 0, 80, 120))
}(), draw.Src.Draw(img, image.Rect(0, 0, 80, 120), &image.Uniform{image1bit.On}, image.Point{})
return img
}(),
},
want: []record{ want: []record{
{cmd: dataEntryModeSetting, data: []byte{0x3}}, {cmd: dataEntryModeSetting, data: []byte{0x3}},
{cmd: setRAMXAddressStartEndPosition, data: []byte{0, 10 - 1}}, {cmd: setRAMXAddressStartEndPosition, data: []byte{0, 10 - 1}},
@ -129,11 +398,166 @@ func TestSendImage(t *testing.T) {
}, },
}, },
}, },
{
name: "top left",
cmd: writeRAMBW,
opts: drawOpts{
devSize: image.Pt(100, 40),
dstRect: image.Rect(20, 17-5, 44, 29+5),
origin: TopLeft,
buffer: func() *image1bit.VerticalLSB {
img := image1bit.NewVerticalLSB(image.Rect(0, 0, 100, 40))
draw.Src.Draw(img, image.Rect(20, 17, 44, 29), &image.Uniform{image1bit.On}, image.Point{})
return img
}(),
},
want: []record{
{cmd: dataEntryModeSetting, data: []byte{0x3}},
{cmd: setRAMXAddressStartEndPosition, data: []byte{2, 5}},
{cmd: setRAMYAddressStartEndPosition, data: []byte{17 - 5, 0, 29 + 5 - 1, 0}},
{cmd: setRAMXAddressCounter, data: []byte{2}},
{cmd: setRAMYAddressCounter, data: []byte{12, 0}},
{
cmd: writeRAMBW,
data: append(
append(
bytes.Repeat([]byte{0x00, 0x00, 0x00, 0x00}, 5),
bytes.Repeat([]byte{0x0f, 0xff, 0xff, 0xf0}, 29-17)...),
bytes.Repeat([]byte{0x00, 0x00, 0x00, 0x00}, 5)...,
),
},
},
},
{
name: "top right",
cmd: writeRAMBW,
opts: drawOpts{
devSize: image.Pt(64, 48),
dstRect: image.Rect(15-5, 16, 30+5, 40),
origin: TopRight,
buffer: func() *image1bit.VerticalLSB {
img := image1bit.NewVerticalLSB(image.Rect(0, 0, 64, 48))
draw.Src.Draw(img, image.Rect(15, 20, 30, 36), &image.Uniform{image1bit.On}, image.Point{})
return img
}(),
},
want: []record{
{cmd: dataEntryModeSetting, data: []byte{0x3}},
{cmd: setRAMXAddressStartEndPosition, data: []byte{(48 - 40) / 8, ((48 - 16 + 7) / 8) - 1}},
{cmd: setRAMYAddressStartEndPosition, data: []byte{15 - 5, 0, (30 + 5) - 1, 0}},
{cmd: setRAMXAddressCounter, data: []byte{1}},
{cmd: setRAMYAddressCounter, data: []byte{10, 0}},
{
cmd: writeRAMBW,
data: append(
append(
bytes.Repeat([]byte{0x00, 0x00, 0x00}, 5),
bytes.Repeat([]byte{0x0f, 0xff, 0xf0}, 30-15)...),
bytes.Repeat([]byte{0x00, 0x00, 0x00}, 5)...,
),
},
},
},
{
name: "top right uneven size",
cmd: writeRAMBW,
opts: drawOpts{
devSize: image.Pt(61, 53),
dstRect: image.Rect(15-5, 16, 30+5, 36),
origin: TopRight,
buffer: func() *image1bit.VerticalLSB {
img := image1bit.NewVerticalLSB(image.Rect(0, 0, 64, 99))
yoff := img.Bounds().Dy() - 53 + 1
draw.Src.Draw(img, image.Rect(15, yoff+16, 30, yoff+32), &image.Uniform{image1bit.On}, image.Point{})
return img
}(),
},
want: []record{
{cmd: dataEntryModeSetting, data: []byte{0x3}},
{cmd: setRAMXAddressStartEndPosition, data: []byte{(53 - 32) / 8, ((53 - 16 + 7) / 8) - 1}},
{cmd: setRAMYAddressStartEndPosition, data: []byte{15 - 5, 0, (30 + 5) - 1, 0}},
{cmd: setRAMXAddressCounter, data: []byte{2}},
{cmd: setRAMYAddressCounter, data: []byte{10, 0}},
{
cmd: writeRAMBW,
data: append(
append(
bytes.Repeat([]byte{0x00, 0x00, 0x00}, 5),
bytes.Repeat([]byte{0x0f, 0xff, 0xf0}, 30-15)...),
bytes.Repeat([]byte{0x00, 0x00, 0x00}, 5)...,
),
},
},
},
{
name: "bottom right",
cmd: writeRAMRed,
opts: drawOpts{
devSize: image.Pt(64, 48),
dstRect: image.Rect(16, 15-5, 40, 30+5),
origin: BottomRight,
buffer: func() *image1bit.VerticalLSB {
img := image1bit.NewVerticalLSB(image.Rect(0, 0, 64, 48))
draw.Src.Draw(img, image.Rect(20, 15, 36, 30), &image.Uniform{image1bit.On}, image.Point{})
return img
}(),
},
want: []record{
{cmd: dataEntryModeSetting, data: []byte{0x3}},
{cmd: setRAMXAddressStartEndPosition, data: []byte{(64 - 40) / 8, ((64 - 16 + 7) / 8) - 1}},
{cmd: setRAMYAddressStartEndPosition, data: []byte{48 - (30 + 5), 0, 48 - (15 - 5) - 1, 0}},
{cmd: setRAMXAddressCounter, data: []byte{3}},
{cmd: setRAMYAddressCounter, data: []byte{48 - (30 + 5), 0}},
{
cmd: writeRAMRed,
data: append(
append(
bytes.Repeat([]byte{0x00, 0x00, 0x00}, 5),
bytes.Repeat([]byte{0x0f, 0xff, 0xf0}, 30-15)...),
bytes.Repeat([]byte{0x00, 0x00, 0x00}, 5)...,
),
},
},
},
{
name: "bottom left",
cmd: writeRAMRed,
opts: drawOpts{
devSize: image.Pt(64, 48),
dstRect: image.Rect(15-5, 16, 30+5, 40),
origin: BottomLeft,
buffer: func() *image1bit.VerticalLSB {
img := image1bit.NewVerticalLSB(image.Rect(0, 0, 64, 48))
draw.Src.Draw(img, image.Rect(15, 20, 30, 36), &image.Uniform{image1bit.On}, image.Point{})
return img
}(),
},
want: []record{
{cmd: dataEntryModeSetting, data: []byte{0x3}},
{cmd: setRAMXAddressStartEndPosition, data: []byte{16 / 8, ((40 + 7) / 8) - 1}},
{cmd: setRAMYAddressStartEndPosition, data: []byte{64 - (30 + 5), 0, 64 - (15 - 5) - 1, 0}},
{cmd: setRAMXAddressCounter, data: []byte{2}},
{cmd: setRAMYAddressCounter, data: []byte{64 - (30 + 5), 0}},
{
cmd: writeRAMRed,
data: append(
append(
bytes.Repeat([]byte{0x00, 0x00, 0x00}, 5),
bytes.Repeat([]byte{0x0f, 0xff, 0xf0}, 30-15)...),
bytes.Repeat([]byte{0x00, 0x00, 0x00}, 5)...,
),
},
},
},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
var got fakeController var got fakeController
sendImage(&got, tc.cmd, tc.area, tc.img) checkRectCanon(t, tc.opts.dstRect)
spec := tc.opts.spec()
tc.opts.sendImage(&got, tc.cmd, &spec)
if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" { if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" {
t.Errorf("sendImage() difference (-got +want):\n%s", diff) t.Errorf("sendImage() difference (-got +want):\n%s", diff)
@ -150,6 +574,9 @@ func TestDrawImage(t *testing.T) {
}{ }{
{ {
name: "empty", name: "empty",
opts: drawOpts{
buffer: image1bit.NewVerticalLSB(image.Rectangle{}),
},
}, },
{ {
name: "partial", name: "partial",

@ -77,12 +77,24 @@ type Dev struct {
rst gpio.PinOut rst gpio.PinOut
busy gpio.PinIn busy gpio.PinIn
bounds image.Rectangle
buffer *image1bit.VerticalLSB buffer *image1bit.VerticalLSB
mode PartialUpdate mode PartialUpdate
opts *Opts opts *Opts
} }
// Corner describes a corner on the physical device and is used to define the
// origin for drawing operations.
type Corner uint8
const (
TopLeft Corner = iota
TopRight
BottomRight
BottomLeft
)
// LUT contains the waveform that is used to program the display. // LUT contains the waveform that is used to program the display.
type LUT []byte type LUT []byte
@ -90,6 +102,7 @@ type LUT []byte
type Opts struct { type Opts struct {
Width int Width int
Height int Height int
Origin Corner
FullUpdate LUT FullUpdate LUT
PartialUpdate LUT PartialUpdate LUT
} }
@ -140,6 +153,11 @@ var EPD2in13v2 = Opts{
}, },
} }
// flipPt returns a new image.Point with the X and Y coordinates exchanged.
func flipPt(pt image.Point) image.Point {
return image.Point{X: pt.Y, Y: pt.X}
}
// New creates new handler which is used to access the display. // New creates new handler which is used to access the display.
func New(p spi.Port, dc, cs, rst gpio.PinOut, busy gpio.PinIn, opts *Opts) (*Dev, error) { func New(p spi.Port, dc, cs, rst gpio.PinOut, busy gpio.PinIn, opts *Opts) (*Dev, error) {
c, err := p.Connect(5*physic.MegaHertz, spi.Mode0, 8) c, err := p.Connect(5*physic.MegaHertz, spi.Mode0, 8)
@ -151,14 +169,30 @@ func New(p spi.Port, dc, cs, rst gpio.PinOut, busy gpio.PinIn, opts *Opts) (*Dev
return nil, err return nil, err
} }
displaySize := image.Pt(opts.Width, opts.Height)
// The physical X axis is sized to have one-byte alignment on the (0,0)
// on-display position after rotation.
bufferSize := image.Pt((opts.Width+7)/8*8, opts.Height)
switch opts.Origin {
case TopLeft, BottomRight:
case TopRight, BottomLeft:
displaySize = flipPt(displaySize)
bufferSize = flipPt(bufferSize)
default:
return nil, fmt.Errorf("unknown corner %v", opts.Origin)
}
d := &Dev{ d := &Dev{
c: c, c: c,
dc: dc, dc: dc,
cs: cs, cs: cs,
rst: rst, rst: rst,
busy: busy, busy: busy,
bounds: image.Rectangle{Max: displaySize},
buffer: image1bit.NewVerticalLSB(image.Rectangle{ buffer: image1bit.NewVerticalLSB(image.Rectangle{
Max: image.Pt((opts.Width+7)/8*8, opts.Height), Max: bufferSize,
}), }),
mode: Full, mode: Full,
opts: opts, opts: opts,
@ -241,7 +275,7 @@ func (d *Dev) ColorModel() color.Model {
// Bounds returns the bounds for the configurated display. // Bounds returns the bounds for the configurated display.
func (d *Dev) Bounds() image.Rectangle { func (d *Dev) Bounds() image.Rectangle {
return image.Rect(0, 0, d.opts.Width, d.opts.Height) return d.bounds
} }
// Draw draws the given image to the display. Only the destination area is // Draw draws the given image to the display. Only the destination area is
@ -249,7 +283,8 @@ func (d *Dev) Bounds() image.Rectangle {
// area is refreshed. // area is refreshed.
func (d *Dev) Draw(dstRect image.Rectangle, src image.Image, srcPts image.Point) error { func (d *Dev) Draw(dstRect image.Rectangle, src image.Image, srcPts image.Point) error {
opts := drawOpts{ opts := drawOpts{
devSize: image.Pt(d.opts.Width, d.opts.Height), devSize: d.bounds.Max,
origin: d.opts.Origin,
buffer: d.buffer, buffer: d.buffer,
dstRect: dstRect, dstRect: dstRect,
src: src, src: src,
@ -281,9 +316,7 @@ func (d *Dev) Halt() error {
// String returns a string containing configuration information. // String returns a string containing configuration information.
func (d *Dev) String() string { func (d *Dev) String() string {
bounds := d.Bounds() return fmt.Sprintf("epd.Dev{%s, %s, Width: %d, Height: %d}", d.c, d.dc, d.bounds.Dx(), d.bounds.Dy())
return fmt.Sprintf("epd.Dev{%s, %s, Width: %d, Height: %d}", d.c, d.dc, bounds.Dx(), bounds.Dy())
} }
// Sleep makes the controller enter deep sleep mode. It can be woken up by // Sleep makes the controller enter deep sleep mode. It can be woken up by

@ -34,6 +34,28 @@ func TestNew(t *testing.T) {
wantBufferBounds: image.Rect(0, 0, 128, 250), wantBufferBounds: image.Rect(0, 0, 128, 250),
wantString: "epd.Dev{playback, (0), Width: 122, Height: 250}", wantString: "epd.Dev{playback, (0), Width: 122, Height: 250}",
}, },
{
name: "EPD2in13v2, top right",
opts: func() Opts {
opts := EPD2in13v2
opts.Origin = TopRight
return opts
}(),
wantBounds: image.Rect(0, 0, 250, 122),
wantBufferBounds: image.Rect(0, 0, 250, 128),
wantString: "epd.Dev{playback, (0), Width: 250, Height: 122}",
},
{
name: "EPD2in13v2, bottom left",
opts: func() Opts {
opts := EPD2in13v2
opts.Origin = BottomLeft
return opts
}(),
wantBounds: image.Rect(0, 0, 250, 122),
wantBufferBounds: image.Rect(0, 0, 250, 128),
wantString: "epd.Dev{playback, (0), Width: 250, Height: 122}",
},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
dev, err := New(&spitest.Playback{}, &gpiotest.Pin{}, &gpiotest.Pin{}, &gpiotest.Pin{}, &gpiotest.Pin{ dev, err := New(&spitest.Playback{}, &gpiotest.Pin{}, &gpiotest.Pin{}, &gpiotest.Pin{}, &gpiotest.Pin{

Loading…
Cancel
Save