From 0012149b5d7404dfc564676e8dbaf12c132834aa Mon Sep 17 00:00:00 2001 From: hansmi Date: Sun, 23 Jan 2022 22:53:55 +0100 Subject: [PATCH] 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 --- waveshare2in13v2/drawing.go | 142 +++++-- waveshare2in13v2/drawing_test.go | 475 ++++++++++++++++++++++-- waveshare2in13v2/waveshare213v2.go | 55 ++- waveshare2in13v2/waveshare213v2_test.go | 22 ++ 4 files changed, 637 insertions(+), 57 deletions(-) diff --git a/waveshare2in13v2/drawing.go b/waveshare2in13v2/drawing.go index 7f8f963..e9728b8 100644 --- a/waveshare2in13v2/drawing.go +++ b/waveshare2in13v2/drawing.go @@ -44,6 +44,7 @@ func setMemoryArea(ctrl controller, area image.Rectangle) { type drawOpts struct { commands []byte devSize image.Point + origin Corner buffer *image1bit.VerticalLSB dstRect image.Rectangle src image.Image @@ -51,47 +52,142 @@ type drawOpts struct { } type drawSpec struct { - // Destination on display in pixels, normalized to fit into actual size. - DstRect image.Rectangle + // Amount by which buffer contents must be moved to align with the physical + // 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 - // 8 pixels), vertically in pixels. - MemRect image.Rectangle + // 8 pixels), vertically in pixels. Computed from memDstRect. + memRect image.Rectangle } +// spec pre-computes the various offsets required for sending image updates to +// the device. func (o *drawOpts) spec() 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( - s.DstRect.Min.X/8, s.DstRect.Min.Y, - (s.DstRect.Max.X+7)/8, s.DstRect.Max.Y, - ) + switch o.origin { + case TopRight: + 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 } // sendImage sends an image to the controller after setting up the registers. -// The area is in bytes on the horizontal axis. -func sendImage(ctrl controller, cmd byte, area image.Rectangle, img *image1bit.VerticalLSB) { - if area.Empty() { +func (o *drawOpts) sendImage(ctrl controller, cmd byte, spec *drawSpec) { + if spec.memRect.Empty() { return } - setMemoryArea(ctrl, area) + setMemoryArea(ctrl, spec.memRect) 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++ { - for x := 0; x < len(rowData); x++ { - rowData[x] = 0 + switch o.origin { + case TopLeft: + 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++ { - if img.BitAt(((area.Min.X+x)*8)+bit, y) { - rowData[x] |= 0x80 >> bit + bufPos := posFor(destY, (spec.memRect.Min.X+destX)*8, 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) { s := opts.spec() - if s.MemRect.Empty() { + if s.memRect.Empty() { 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 @@ -117,6 +215,6 @@ func drawImage(ctrl controller, opts *drawOpts) { // Keep the two buffers in sync. for _, cmd := range commands { - sendImage(ctrl, cmd, s.MemRect, opts.buffer) + opts.sendImage(ctrl, cmd, &s) } } diff --git a/waveshare2in13v2/drawing_test.go b/waveshare2in13v2/drawing_test.go index 30feb32..a8c6205 100644 --- a/waveshare2in13v2/drawing_test.go +++ b/waveshare2in13v2/drawing_test.go @@ -15,14 +15,25 @@ import ( "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) { - for _, tc := range []struct { + type testCase struct { name string opts drawOpts want drawSpec - }{ + } + + for _, tc := range []testCase{ { name: "empty", + opts: drawOpts{ + buffer: image1bit.NewVerticalLSB(image.Rectangle{}), + }, }, { name: "smaller than display", @@ -32,8 +43,9 @@ func TestDrawSpec(t *testing.T) { dstRect: image.Rect(17, 4, 25, 8), }, want: drawSpec{ - DstRect: image.Rect(17, 4, 25, 8), - MemRect: image.Rect(2, 4, 4, 8), + bufferDstRect: image.Rect(17, 4, 25, 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), }, want: drawSpec{ - DstRect: image.Rect(0, 50, 100, 200), - MemRect: image.Rect(0, 50, 13, 200), + bufferDstRect: image.Rect(0, 50, 100, 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) { + checkRectCanon(t, tc.opts.dstRect) + 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) } }) @@ -63,18 +321,23 @@ func TestSendImage(t *testing.T) { for _, tc := range []struct { name string cmd byte - area image.Rectangle - img *image1bit.VerticalLSB + opts drawOpts want []record }{ { name: "empty", + opts: drawOpts{ + buffer: image1bit.NewVerticalLSB(image.Rectangle{}), + }, }, { name: "partial", cmd: writeRAMBW, - area: image.Rect(2, 20, 4, 40), - img: image1bit.NewVerticalLSB(image.Rect(0, 0, 64, 64)), + opts: drawOpts{ + devSize: image.Pt(64, 64), + dstRect: image.Rect(16, 20, 32, 40), + buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 64, 64)), + }, want: []record{ {cmd: dataEntryModeSetting, data: []byte{0x3}}, {cmd: setRAMXAddressStartEndPosition, data: []byte{2, 4 - 1}}, @@ -90,12 +353,15 @@ func TestSendImage(t *testing.T) { { name: "partial non-aligned", cmd: writeRAMRed, - area: image.Rect(2, 4, 6, 8), - img: func() *image1bit.VerticalLSB { - 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 - }(), + opts: drawOpts{ + devSize: image.Pt(100, 64), + dstRect: image.Rect(17, 4, 41, 8), + buffer: func() *image1bit.VerticalLSB { + 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{ {cmd: dataEntryModeSetting, data: []byte{0x3}}, {cmd: setRAMXAddressStartEndPosition, data: []byte{2, 6 - 1}}, @@ -111,12 +377,15 @@ func TestSendImage(t *testing.T) { { name: "full", cmd: writeRAMBW, - area: image.Rect(0, 0, 10, 120), - img: func() *image1bit.VerticalLSB { - 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 - }(), + opts: drawOpts{ + devSize: image.Pt(80, 120), + dstRect: image.Rect(0, 0, 80, 120), + buffer: func() *image1bit.VerticalLSB { + 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{ {cmd: dataEntryModeSetting, data: []byte{0x3}}, {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) { 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 != "" { t.Errorf("sendImage() difference (-got +want):\n%s", diff) @@ -150,6 +574,9 @@ func TestDrawImage(t *testing.T) { }{ { name: "empty", + opts: drawOpts{ + buffer: image1bit.NewVerticalLSB(image.Rectangle{}), + }, }, { name: "partial", diff --git a/waveshare2in13v2/waveshare213v2.go b/waveshare2in13v2/waveshare213v2.go index 4bc988a..e3cf181 100644 --- a/waveshare2in13v2/waveshare213v2.go +++ b/waveshare2in13v2/waveshare213v2.go @@ -77,12 +77,24 @@ type Dev struct { rst gpio.PinOut busy gpio.PinIn + bounds image.Rectangle buffer *image1bit.VerticalLSB mode PartialUpdate 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. type LUT []byte @@ -90,6 +102,7 @@ type LUT []byte type Opts struct { Width int Height int + Origin Corner FullUpdate 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. 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) @@ -151,14 +169,30 @@ func New(p spi.Port, dc, cs, rst gpio.PinOut, busy gpio.PinIn, opts *Opts) (*Dev 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{ - c: c, - dc: dc, - cs: cs, - rst: rst, - busy: busy, + c: c, + dc: dc, + cs: cs, + rst: rst, + busy: busy, + bounds: image.Rectangle{Max: displaySize}, buffer: image1bit.NewVerticalLSB(image.Rectangle{ - Max: image.Pt((opts.Width+7)/8*8, opts.Height), + Max: bufferSize, }), mode: Full, opts: opts, @@ -241,7 +275,7 @@ func (d *Dev) ColorModel() color.Model { // Bounds returns the bounds for the configurated display. 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 @@ -249,7 +283,8 @@ func (d *Dev) Bounds() image.Rectangle { // area is refreshed. func (d *Dev) Draw(dstRect image.Rectangle, src image.Image, srcPts image.Point) error { opts := drawOpts{ - devSize: image.Pt(d.opts.Width, d.opts.Height), + devSize: d.bounds.Max, + origin: d.opts.Origin, buffer: d.buffer, dstRect: dstRect, src: src, @@ -281,9 +316,7 @@ func (d *Dev) Halt() error { // String returns a string containing configuration information. func (d *Dev) String() string { - bounds := d.Bounds() - - return fmt.Sprintf("epd.Dev{%s, %s, Width: %d, Height: %d}", d.c, d.dc, bounds.Dx(), bounds.Dy()) + return fmt.Sprintf("epd.Dev{%s, %s, Width: %d, Height: %d}", d.c, d.dc, d.bounds.Dx(), d.bounds.Dy()) } // Sleep makes the controller enter deep sleep mode. It can be woken up by diff --git a/waveshare2in13v2/waveshare213v2_test.go b/waveshare2in13v2/waveshare213v2_test.go index 2a5782d..e7277a4 100644 --- a/waveshare2in13v2/waveshare213v2_test.go +++ b/waveshare2in13v2/waveshare213v2_test.go @@ -34,6 +34,28 @@ func TestNew(t *testing.T) { wantBufferBounds: image.Rect(0, 0, 128, 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) { dev, err := New(&spitest.Playback{}, &gpiotest.Pin{}, &gpiotest.Pin{}, &gpiotest.Pin{}, &gpiotest.Pin{