diff --git a/waveshare2in13v2/drawing.go b/waveshare2in13v2/drawing.go index f73192c..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,23 +52,80 @@ 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. + // 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 } @@ -82,15 +140,54 @@ func (o *drawOpts) sendImage(ctrl controller, cmd byte, spec *drawSpec) { ctrl.sendCommand(cmd) + var posFor func(destY, destX, bit int) image.Point + + 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 y := spec.memRect.Min.Y; y < spec.memRect.Max.Y; y++ { - for x := 0; x < len(rowData); x++ { - rowData[x] = 0 + 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 o.buffer.BitAt(((spec.memRect.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 } } } @@ -106,7 +203,9 @@ func drawImage(ctrl controller, opts *drawOpts) { 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 diff --git a/waveshare2in13v2/drawing_test.go b/waveshare2in13v2/drawing_test.go index da1c3e0..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,14 +56,260 @@ 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() + 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) } @@ -140,10 +398,163 @@ 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 + checkRectCanon(t, tc.opts.dstRect) + spec := tc.opts.spec() tc.opts.sendImage(&got, tc.cmd, &spec) @@ -163,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 bc1923f..e3cf181 100644 --- a/waveshare2in13v2/waveshare213v2.go +++ b/waveshare2in13v2/waveshare213v2.go @@ -84,6 +84,17 @@ type Dev struct { 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 @@ -91,6 +102,7 @@ type LUT []byte type Opts struct { Width int Height int + Origin Corner FullUpdate LUT PartialUpdate LUT } @@ -141,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) @@ -152,15 +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, - bounds: image.Rect(0, 0, opts.Width, opts.Height), + 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, @@ -251,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, 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{