diff --git a/go.mod b/go.mod index 86c7a55..e2752cd 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index eea72f7..bd3956d 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/waveshare2in13v2/drawing.go b/waveshare2in13v2/drawing.go new file mode 100644 index 0000000..ce7d2f0 --- /dev/null +++ b/waveshare2in13v2/drawing.go @@ -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) + } +} diff --git a/waveshare2in13v2/drawing_test.go b/waveshare2in13v2/drawing_test.go new file mode 100644 index 0000000..7c5d62e --- /dev/null +++ b/waveshare2in13v2/drawing_test.go @@ -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) + } + }) + } +} diff --git a/waveshare2in13v2/waveshare213v2.go b/waveshare2in13v2/waveshare213v2.go index 39fe37c..d5fb815 100644 --- a/waveshare2in13v2/waveshare213v2.go +++ b/waveshare2in13v2/waveshare213v2.go @@ -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{} diff --git a/waveshare2in13v2/waveshare213v2_test.go b/waveshare2in13v2/waveshare213v2_test.go deleted file mode 100644 index bf21784..0000000 --- a/waveshare2in13v2/waveshare213v2_test.go +++ /dev/null @@ -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) - } - }) - } -}