From 30a964cb861883c67224954addba657273d26382 Mon Sep 17 00:00:00 2001 From: Adam Siegel Date: Mon, 20 Nov 2023 13:20:47 -0800 Subject: [PATCH 1/2] adding v3 and v4 waveshare --- waveshare2in13v3/controller.go | 165 ++++++ waveshare2in13v3/controller_test.go | 196 ++++++++ waveshare2in13v3/doc.go | 19 + waveshare2in13v3/drawing.go | 223 +++++++++ waveshare2in13v3/drawing_test.go | 640 ++++++++++++++++++++++++ waveshare2in13v3/errorhandler.go | 73 +++ waveshare2in13v3/example_test.go | 130 +++++ waveshare2in13v3/waveshare213v3.go | 373 ++++++++++++++ waveshare2in13v3/waveshare213v3_test.go | 95 ++++ waveshare2in13v4/controller.go | 195 ++++++++ waveshare2in13v4/controller_test.go | 237 +++++++++ waveshare2in13v4/doc.go | 20 + waveshare2in13v4/drawing.go | 191 +++++++ waveshare2in13v4/drawing_test.go | 590 ++++++++++++++++++++++ waveshare2in13v4/errorhandler.go | 84 ++++ waveshare2in13v4/example_test.go | 130 +++++ waveshare2in13v4/waveshare213v4.go | 348 +++++++++++++ waveshare2in13v4/waveshare213v4_test.go | 95 ++++ 18 files changed, 3804 insertions(+) create mode 100644 waveshare2in13v3/controller.go create mode 100644 waveshare2in13v3/controller_test.go create mode 100644 waveshare2in13v3/doc.go create mode 100644 waveshare2in13v3/drawing.go create mode 100644 waveshare2in13v3/drawing_test.go create mode 100644 waveshare2in13v3/errorhandler.go create mode 100644 waveshare2in13v3/example_test.go create mode 100644 waveshare2in13v3/waveshare213v3.go create mode 100644 waveshare2in13v3/waveshare213v3_test.go create mode 100644 waveshare2in13v4/controller.go create mode 100644 waveshare2in13v4/controller_test.go create mode 100644 waveshare2in13v4/doc.go create mode 100644 waveshare2in13v4/drawing.go create mode 100644 waveshare2in13v4/drawing_test.go create mode 100644 waveshare2in13v4/errorhandler.go create mode 100644 waveshare2in13v4/example_test.go create mode 100644 waveshare2in13v4/waveshare213v4.go create mode 100644 waveshare2in13v4/waveshare213v4_test.go diff --git a/waveshare2in13v3/controller.go b/waveshare2in13v3/controller.go new file mode 100644 index 0000000..98acb97 --- /dev/null +++ b/waveshare2in13v3/controller.go @@ -0,0 +1,165 @@ +// 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 waveshare2in13v3 + +type controller interface { + sendCommand(byte) + sendData([]byte) + waitUntilIdle() +} + +func initDisplay(ctrl controller, opts *Opts) { + ctrl.waitUntilIdle() + ctrl.sendCommand(swReset) + ctrl.waitUntilIdle() + + ctrl.sendCommand(driverOutputControl) + ctrl.sendData([]byte{0xf9, 0x00, 0x00}) + + ctrl.sendCommand(dataEntryModeSetting) + ctrl.sendData([]byte{0x03}) + + setWindow(ctrl, 0, 0, opts.Width-1, opts.Height-1) + setCursor(ctrl, 0, 0) + + ctrl.sendCommand(borderWaveformControl) + ctrl.sendData([]byte{0x05}) + + ctrl.sendCommand(displayUpdateControl1) + ctrl.sendData([]byte{0x00, 0x80}) + + ctrl.sendCommand(tempSensorSelect) + ctrl.sendData([]byte{0x80}) + + ctrl.waitUntilIdle() + + setLut(ctrl, opts.FullUpdate) +} + +func configDisplayMode(ctrl controller, mode PartialUpdate, lut LUT) { + var vcom byte + var borderWaveformControlValue byte + + switch mode { + case Full: + vcom = 0x55 + borderWaveformControlValue = 0x03 + case Partial: + vcom = 0x24 + borderWaveformControlValue = 0x01 + } + + ctrl.sendCommand(writeVcomRegister) + ctrl.sendData([]byte{vcom}) + + ctrl.sendCommand(borderWaveformControl) + ctrl.sendData([]byte{borderWaveformControlValue}) + + ctrl.sendCommand(writeLutRegister) + ctrl.sendData(lut[:70]) + + ctrl.sendCommand(writeDisplayOptionRegister) + ctrl.sendData([]byte{0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00}) + + // Start up the parts likely used by a draw operation soon. + ctrl.sendCommand(displayUpdateControl2) + ctrl.sendData([]byte{displayUpdateEnableClock | displayUpdateEnableAnalog}) + + ctrl.sendCommand(masterActivation) + ctrl.waitUntilIdle() +} + +func updateDisplay(ctrl controller, mode PartialUpdate) { + var displayUpdateFlags byte + + if mode == Partial { + // Make use of red buffer + displayUpdateFlags = 0b1000_0000 + } + + ctrl.sendCommand(displayUpdateControl1) + ctrl.sendData([]byte{displayUpdateFlags}) + + ctrl.sendCommand(displayUpdateControl2) + ctrl.sendData([]byte{ + displayUpdateDisableClock | + displayUpdateDisableAnalog | + displayUpdateDisplay | + displayUpdateEnableClock | + displayUpdateEnableAnalog, + }) + + ctrl.sendCommand(masterActivation) + ctrl.waitUntilIdle() +} + +// new + +// turnOnDisplay turns on the display if mode = true it does a partial display +func turnOnDisplay(ctrl controller, mode PartialUpdate) { + var upMode byte = 0xC7 + if mode { + upMode = 0x0f + } + ctrl.sendCommand(displayUpdateControl2) + ctrl.sendData([]byte{upMode}) + ctrl.sendCommand(masterActivation) + ctrl.waitUntilIdle() +} + +func lookUpTable(ctrl controller, lut LUT) { + ctrl.sendCommand(writeLutRegister) + ctrl.sendData(lut[:153]) + ctrl.waitUntilIdle() +} + +func setLut(ctrl controller, lut LUT) { + lookUpTable(ctrl, lut) + ctrl.sendCommand(endOptionEOPT) + ctrl.sendData([]byte{lut[153]}) + ctrl.sendCommand(gateDrivingVoltageControl) + ctrl.sendData([]byte{lut[154]}) + ctrl.sendCommand(sourceDrivingVoltageControl) + ctrl.sendData(lut[155:157]) + ctrl.sendCommand(writeVcomRegister) + ctrl.sendData([]byte{lut[158]}) +} + +func setWindow(ctrl controller, x_start int, y_start int, x_end int, y_end int) { + ctrl.sendCommand(setRAMXAddressStartEndPosition) + ctrl.sendData([]byte{byte((x_start >> 3) & 0xFF), byte((x_end >> 3) & 0xFF)}) + + ctrl.sendCommand(setRAMYAddressStartEndPosition) + ctrl.sendData([]byte{byte(y_start & 0xFF), byte((y_start >> 8) & 0xFF), byte(y_end & 0xFF), byte((y_end >> 8) & 0xFF)}) +} + +func setCursor(ctrl controller, x int, y int) { + ctrl.sendCommand(setRAMXAddressCounter) + // x point must be the multiple of 8 or the last 3 bits will be ignored + ctrl.sendData([]byte{byte(x & 0xFF)}) + + ctrl.sendCommand(setRAMYAddressCounter) + ctrl.sendData([]byte{byte(y & 0xFF), byte((y >> 8) & 0xFF)}) +} + +func clear(ctrl controller, color byte, opts *Opts) { + var linewidth int + if opts.Width%8 == 0 { + linewidth = int(opts.Width / 8) + } else { + linewidth = int(opts.Width/8) + 1 + } + + var buff []byte + ctrl.sendCommand(writeRAMBW) + for j := 0; j < opts.Height; j++ { + for i := 0; i < linewidth; i++ { + buff = append(buff, color) + } + } + ctrl.sendData(buff) + + turnOnDisplay(ctrl, false) +} diff --git a/waveshare2in13v3/controller_test.go b/waveshare2in13v3/controller_test.go new file mode 100644 index 0000000..c3273b8 --- /dev/null +++ b/waveshare2in13v3/controller_test.go @@ -0,0 +1,196 @@ +// 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 waveshare2in13v3 + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +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 (*fakeController) waitUntilIdle() { +} + +func TestInitDisplay(t *testing.T) { + for _, tc := range []struct { + name string + opts Opts + want []record + }{ + { + name: "epd2in13v3", + opts: EPD2in13v3, + want: []record{ + {cmd: swReset}, + { + cmd: driverOutputControl, + data: []byte{250 - 1, 0, 0}, + }, + {cmd: dataEntryModeSetting, data: []uint8{0x03}}, + {cmd: setRAMXAddressStartEndPosition, data: []uint8{0x00, 0x0f}}, + {cmd: setRAMYAddressStartEndPosition, data: []uint8{0x00, 0x00, 0xf9, 0x00}}, + {cmd: setRAMXAddressCounter, data: []uint8{0x00}}, + {cmd: setRAMYAddressCounter, data: []uint8{0x00, 0x00}}, + {cmd: borderWaveformControl, data: []uint8{0x05}}, + {cmd: displayUpdateControl1, data: []uint8{0x00, 0x80}}, + {cmd: tempSensorSelect, data: []uint8{0x80}}, + {cmd: writeLutRegister, data: EPD2in13v3.FullUpdate[:153]}, + {cmd: endOptionEOPT, data: []uint8{EPD2in13v3.FullUpdate[153]}}, + {cmd: gateDrivingVoltageControl, data: []uint8{EPD2in13v3.FullUpdate[154]}}, + {cmd: sourceDrivingVoltageControl, data: EPD2in13v3.FullUpdate[155:157]}, + {cmd: writeVcomRegister, data: []uint8{EPD2in13v3.FullUpdate[158]}}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var got fakeController + + initDisplay(&got, &tc.opts) + + if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" { + t.Errorf("initDisplay() difference (-got +want):\n%s", diff) + } + }) + } +} + +func TestConfigDisplayMode(t *testing.T) { + for _, tc := range []struct { + name string + mode PartialUpdate + lut LUT + want []record + }{ + { + name: "full", + mode: Full, + lut: bytes.Repeat([]byte{'F'}, 100), + want: []record{ + {cmd: writeVcomRegister, data: []byte{0x55}}, + {cmd: borderWaveformControl, data: []byte{0x03}}, + {cmd: writeLutRegister, data: bytes.Repeat([]byte{'F'}, 70)}, + {cmd: 0x37, data: []byte{0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00}}, + {cmd: displayUpdateControl2, data: []byte{0xc0}}, + {cmd: masterActivation}, + }, + }, + { + name: "partial", + mode: Partial, + lut: bytes.Repeat([]byte{'P'}, 70), + want: []record{ + {cmd: writeVcomRegister, data: []byte{0x24}}, + {cmd: borderWaveformControl, data: []byte{0x01}}, + {cmd: writeLutRegister, data: bytes.Repeat([]byte{'P'}, 70)}, + {cmd: 0x37, data: []byte{0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00}}, + {cmd: displayUpdateControl2, data: []byte{0xc0}}, + {cmd: masterActivation}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var got fakeController + + configDisplayMode(&got, tc.mode, tc.lut) + + if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" { + t.Errorf("configDisplayMode() difference (-got +want):\n%s", diff) + } + }) + } +} + +func TestUpdateDisplay(t *testing.T) { + for _, tc := range []struct { + name string + mode PartialUpdate + want []record + }{ + { + name: "full", + mode: Full, + want: []record{ + {cmd: displayUpdateControl1, data: []byte{0}}, + {cmd: displayUpdateControl2, data: []byte{0xc7}}, + {cmd: masterActivation}, + }, + }, + { + name: "partial", + mode: Partial, + want: []record{ + {cmd: displayUpdateControl1, data: []byte{0x80}}, + {cmd: displayUpdateControl2, data: []byte{0xc7}}, + {cmd: masterActivation}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var got fakeController + + updateDisplay(&got, tc.mode) + + if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" { + t.Errorf("updateDisplay() difference (-got +want):\n%s", diff) + } + }) + } +} + +func TestClear(t *testing.T) { + var buff []byte + const linewidth = int(122/8) + 1 + for j := 0; j < 250; j++ { + for i := 0; i < linewidth; i++ { + buff = append(buff, 0x00) + } + } + for _, tc := range []struct { + name string + opts Opts + color byte + want []record + }{ + { + name: "clear", + opts: EPD2in13v3, + want: []record{ + {cmd: writeRAMBW, data: buff}, + {cmd: displayUpdateControl2, data: []byte{0xC7}}, + {cmd: masterActivation}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var got fakeController + + clear(&got, tc.color, &tc.opts) + + if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" { + t.Errorf("updateDisplay() difference (-got +want):\n%s", diff) + } + }) + } +} diff --git a/waveshare2in13v3/doc.go b/waveshare2in13v3/doc.go new file mode 100644 index 0000000..9680291 --- /dev/null +++ b/waveshare2in13v3/doc.go @@ -0,0 +1,19 @@ +// 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 waveshare2in13v3 controls Waveshare 2.13 v3 e-paper displays. +// +// Datasheet: +// https://files.waveshare.com/upload/5/59/2.13inch_e-Paper_V3_Specificition.pdf +// +// Product page: +// 2.13 inch version 3: https://www.waveshare.com/wiki/2.13inch_e-Paper_HAT_Manual#Resources +// This display is an Active Matrix Electrophoretic Display (AM EPD), with +// interface and a reference system design. The display is capable to display +// imagesat 1-bit white, black full display capabilities. The 2.13inch active area +// contains 250×122 pixels. The module is a TFT-array driving electrophoresis +// display, withintegrated circuits including gate driver, source driver, MCU +// interface, timingcontroller, oscillator, DC-DC, SRAM, LUT, VCOM. Module can be +// used in portableelectronic devices, such as Electronic Shelf Label (ESL) System. +package waveshare2in13v3 diff --git a/waveshare2in13v3/drawing.go b/waveshare2in13v3/drawing.go new file mode 100644 index 0000000..e8dc907 --- /dev/null +++ b/waveshare2in13v3/drawing.go @@ -0,0 +1,223 @@ +// 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 waveshare2in13v3 + +import ( + "encoding/binary" + "image" + "image/draw" + + "periph.io/x/devices/v3/ssd1306/image1bit" +) + +// setMemoryArea configures the target drawing area (horizontal is in bytes, +// vertical in pixels). +func setMemoryArea(ctrl controller, area image.Rectangle) { + startX, endX := uint8(area.Min.X), uint8(area.Max.X-1) + startY, endY := uint16(area.Min.Y), uint16(area.Max.Y-1) + + startEndY := [4]byte{} + binary.LittleEndian.PutUint16(startEndY[0:], startY) + binary.LittleEndian.PutUint16(startEndY[2:], endY) + + ctrl.sendCommand(dataEntryModeSetting) + ctrl.sendData([]byte{ + // Y increment, X increment; update address counter in X direction + 0b011, + }) + + ctrl.sendCommand(setRAMXAddressStartEndPosition) + ctrl.sendData([]byte{startX, endX}) + + ctrl.sendCommand(setRAMYAddressStartEndPosition) + ctrl.sendData(startEndY[:4]) + + ctrl.sendCommand(setRAMXAddressCounter) + ctrl.sendData([]byte{startX}) + + ctrl.sendCommand(setRAMYAddressCounter) + ctrl.sendData(startEndY[:2]) +} + +type drawOpts struct { + commands []byte + devSize image.Point + origin Corner + buffer *image1bit.VerticalLSB + dstRect image.Rectangle + src image.Image + srcPts image.Point +} + +type drawSpec struct { + // 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. 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{ + bufferDstRect: image.Rectangle{Max: o.devSize}.Intersect(o.dstRect), + } + + 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. +func (o *drawOpts) sendImage(ctrl controller, cmd byte, spec *drawSpec) { + if spec.memRect.Empty() { + return + } + + setMemoryArea(ctrl, spec.memRect) + + 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 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++ { + 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 + } + } + } + + ctrl.sendData(rowData) + } + +} + +func drawImage(ctrl controller, opts *drawOpts, mode PartialUpdate) { + s := opts.spec() + + if s.memRect.Empty() { + return + } + + // 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 + + if len(commands) == 0 { + commands = []byte{writeRAMBW, writeRAMRed} + } + + // Keep the two buffers in sync. + for _, cmd := range commands { + opts.sendImage(ctrl, cmd, &s) + } + + turnOnDisplay(ctrl, mode) +} diff --git a/waveshare2in13v3/drawing_test.go b/waveshare2in13v3/drawing_test.go new file mode 100644 index 0000000..ba8a4c6 --- /dev/null +++ b/waveshare2in13v3/drawing_test.go @@ -0,0 +1,640 @@ +// 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 waveshare2in13v3 + +import ( + "bytes" + "image" + "image/draw" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "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) { + 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", + opts: drawOpts{ + devSize: image.Pt(100, 200), + buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 120, 210)), + dstRect: image.Rect(17, 4, 25, 8), + }, + want: drawSpec{ + bufferDstRect: image.Rect(17, 4, 25, 8), + memDstRect: image.Rect(17, 4, 25, 8), + memRect: image.Rect(2, 4, 4, 8), + }, + }, + { + name: "larger than display", + opts: drawOpts{ + devSize: image.Pt(100, 200), + buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 100, 200)), + dstRect: image.Rect(-20, 50, 125, 300), + }, + want: drawSpec{ + 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) + } + }) + } +} + +func TestSendImage(t *testing.T) { + for _, tc := range []struct { + name string + cmd byte + opts drawOpts + want []record + }{ + { + name: "empty", + opts: drawOpts{ + buffer: image1bit.NewVerticalLSB(image.Rectangle{}), + }, + }, + { + name: "partial", + cmd: writeRAMBW, + 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}}, + {cmd: setRAMYAddressStartEndPosition, data: []byte{20, 0, 40 - 1, 0}}, + {cmd: setRAMXAddressCounter, data: []byte{2}}, + {cmd: setRAMYAddressCounter, data: []byte{20, 0}}, + { + cmd: writeRAMBW, + data: bytes.Repeat([]byte{0}, 2*(30-10)), + }, + }, + }, + { + name: "partial non-aligned", + cmd: writeRAMRed, + 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}}, + {cmd: setRAMYAddressStartEndPosition, data: []byte{4, 0, 8 - 1, 0}}, + {cmd: setRAMXAddressCounter, data: []byte{2}}, + {cmd: setRAMYAddressCounter, data: []byte{4, 0}}, + { + cmd: writeRAMRed, + data: bytes.Repeat([]byte{0x7f, 0xff, 0xff, 0x80}, 4), + }, + }, + }, + { + name: "full", + cmd: writeRAMBW, + 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}}, + {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), + }, + }, + }, + { + 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) + + if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" { + t.Errorf("sendImage() 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{ + buffer: image1bit.NewVerticalLSB(image.Rectangle{}), + }, + }, + { + name: "partial", + opts: drawOpts{ + commands: []byte{writeRAMBW}, + devSize: image.Pt(64, 64), + buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 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), + }, + {cmd: displayUpdateControl2, data: []byte{0x0f}}, + {cmd: masterActivation}, + }, + }, + { + name: "full", + opts: drawOpts{ + commands: []byte{writeRAMRed}, + devSize: image.Pt(80, 120), + buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 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: writeRAMRed, + data: bytes.Repeat([]byte{0xff}, 80/8*120), + }, + {cmd: displayUpdateControl2, data: []byte{0x0f}}, + {cmd: masterActivation}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var got fakeController + + drawImage(&got, &tc.opts, true) + + 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/waveshare2in13v3/errorhandler.go b/waveshare2in13v3/errorhandler.go new file mode 100644 index 0000000..6a260e5 --- /dev/null +++ b/waveshare2in13v3/errorhandler.go @@ -0,0 +1,73 @@ +// 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 waveshare2in13v3 + +import ( + "time" + + "periph.io/x/conn/v3/gpio" +) + +// errorHandler is a wrapper for error management. +type errorHandler struct { + d Dev + err error +} + +func (eh *errorHandler) rstOut(l gpio.Level) { + if eh.err != nil { + return + } + eh.err = eh.d.rst.Out(l) +} + +func (eh *errorHandler) cTx(w []byte, r []byte) { + if eh.err != nil { + return + } + eh.err = eh.d.c.Tx(w, r) +} + +func (eh *errorHandler) dcOut(l gpio.Level) { + if eh.err != nil { + return + } + eh.err = eh.d.dc.Out(l) +} + +func (eh *errorHandler) csOut(l gpio.Level) { + if eh.err != nil { + return + } + eh.err = eh.d.cs.Out(l) +} + +func (eh *errorHandler) waitUntilIdle() { + for busy := eh.d.busy; busy.Read() == gpio.High; { + busy.WaitForEdge(10 * time.Millisecond) + } +} + +func (eh *errorHandler) sendCommand(cmd byte) { + if eh.err != nil { + return + } + + eh.dcOut(gpio.Low) + eh.csOut(gpio.Low) + eh.cTx([]byte{cmd}, nil) + eh.csOut(gpio.High) +} + +func (eh *errorHandler) sendData(data []byte) { + if eh.err != nil { + return + } + + eh.dcOut(gpio.High) + eh.csOut(gpio.Low) + eh.cTx(data, nil) + eh.csOut(gpio.High) +} diff --git a/waveshare2in13v3/example_test.go b/waveshare2in13v3/example_test.go new file mode 100644 index 0000000..62eaef4 --- /dev/null +++ b/waveshare2in13v3/example_test.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 waveshare2in13v3_test + +import ( + "image" + "image/draw" + "log" + + "golang.org/x/image/font" + "golang.org/x/image/font/basicfont" + "golang.org/x/image/math/fixed" + + "periph.io/x/conn/v3/spi/spireg" + "periph.io/x/devices/v3/ssd1306/image1bit" + "periph.io/x/devices/v3/waveshare2in13v3" + "periph.io/x/host/v3" +) + +func Example() { + // Make sure periph is initialized. + if _, err := host.Init(); err != nil { + log.Fatal(err) + } + + // Use spireg SPI bus registry to find the first available SPI bus. + b, err := spireg.Open("") + if err != nil { + log.Fatal(err) + } + defer b.Close() + + dev, err := waveshare2in13v3.NewHat(b, &waveshare2in13v3.EPD2in13v3) // Display config and size + if err != nil { + log.Fatalf("Failed to initialize driver: %v", err) + } + + err = dev.Init() + if err != nil { + log.Fatalf("Failed to initialize display: %v", err) + } + + // Draw on it. Black text on a white background. + img := image1bit.NewVerticalLSB(dev.Bounds()) + draw.Draw(img, img.Bounds(), &image.Uniform{image1bit.On}, image.Point{}, draw.Src) + f := basicfont.Face7x13 + drawer := font.Drawer{ + Dst: img, + Src: &image.Uniform{image1bit.Off}, + Face: f, + Dot: fixed.P(0, img.Bounds().Dy()-1-f.Descent), + } + drawer.DrawString("Hello from periph!") + + if err := dev.Draw(dev.Bounds(), img, image.Point{}); err != nil { + log.Fatal(err) + } +} + +func Example_other() { + // Make sure periph is initialized. + if _, err := host.Init(); err != nil { + log.Fatal(err) + } + + // Use spireg SPI bus registry to find the first available SPI bus. + b, err := spireg.Open("") + if err != nil { + log.Fatal(err) + } + defer b.Close() + + dev, err := waveshare2in13v3.NewHat(b, &waveshare2in13v3.EPD2in13v3) // Display config and size + if err != nil { + log.Fatalf("Failed to initialize driver: %v", err) + } + + err = dev.Init() + if err != nil { + log.Fatalf("Failed to initialize display: %v", err) + } + + var img image.Image + // Note: this code is commented out so periph does not depend on: + // "github.com/fogleman/gg" + // "github.com/golang/freetype/truetype" + // "golang.org/x/image/font/gofont/goregular" + // bounds := dev.Bounds() + // w := bounds.Dx() + // h := bounds.Dy() + // dc := gg.NewContext(w, h) + // im, err := gg.LoadPNG("gopher.png") + // if err != nil { + // panic(err) + // } + // dc.SetRGB(1, 1, 1) + // dc.Clear() + // dc.SetRGB(0, 0, 0) + // dc.Rotate(gg.Radians(90)) + // dc.Translate(0.0, -float64(h/2)) + // font, err := truetype.Parse(goregular.TTF) + // if err != nil { + // panic(err) + // } + // face := truetype.NewFace(font, &truetype.Options{ + // Size: 16, + // }) + // dc.SetFontFace(face) + // text := "Hello from periph!" + // tw, th := dc.MeasureString(text) + // dc.DrawImage(im, 120, 30) + // padding := 8.0 + // dc.DrawRoundedRectangle(padding*2, padding*2, tw+padding*2, th+padding, 10) + // dc.Stroke() + // dc.DrawString(text, padding*3, padding*2+th) + // for i := 0; i < 10; i++ { + // dc.DrawCircle(float64(30+(10*i)), 100, 5) + // } + // for i := 0; i < 10; i++ { + // dc.DrawRectangle(float64(30+(10*i)), 80, 5, 5) + // } + // dc.Fill() + // img = dc.Image() + + if err := dev.Draw(dev.Bounds(), img, image.Point{}); err != nil { + log.Fatal(err) + } +} diff --git a/waveshare2in13v3/waveshare213v3.go b/waveshare2in13v3/waveshare213v3.go new file mode 100644 index 0000000..1b34142 --- /dev/null +++ b/waveshare2in13v3/waveshare213v3.go @@ -0,0 +1,373 @@ +// 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 waveshare2in13v3 + +import ( + "fmt" + "image" + "image/color" + "image/draw" + "time" + + "periph.io/x/conn/v3" + "periph.io/x/conn/v3/display" + "periph.io/x/conn/v3/gpio" + "periph.io/x/conn/v3/physic" + "periph.io/x/conn/v3/spi" + "periph.io/x/devices/v3/ssd1306/image1bit" + "periph.io/x/host/v3/rpi" +) + +// Commands +const ( + driverOutputControl byte = 0x01 + gateDrivingVoltageControl byte = 0x03 + sourceDrivingVoltageControl byte = 0x04 + programInitialControlSetting byte = 0x08 + readRegisterForInitialCodeSetting byte = 0x0A + boosterSoftStartControl byte = 0x0C + deepSleepMode byte = 0x10 + dataEntryModeSetting byte = 0x11 + swReset byte = 0x12 + tempSensorSelect byte = 0x18 + tempSensorRegWrite byte = 0x1A + masterActivation byte = 0x20 + displayUpdateControl1 byte = 0x21 + displayUpdateControl2 byte = 0x22 + writeRAMBW byte = 0x24 + writeRAMRed byte = 0x26 + writeVcomRegister byte = 0x2C + otpReadRegisterDisplayOpt byte = 0x2D + statusBitRead byte = 0x2F + otpProgramWaveformSetting byte = 0x30 + writeLutRegister byte = 0x32 + writeDisplayOptionRegister byte = 0x37 + otpProgramMode byte = 0x39 + setDummyLinePeriod byte = 0x3A + setGateTime byte = 0x3B + borderWaveformControl byte = 0x3C + endOptionEOPT byte = 0x3F // undocumented in v3 spec sheet + setRAMXAddressStartEndPosition byte = 0x44 + setRAMYAddressStartEndPosition byte = 0x45 + setRAMXAddressCounter byte = 0x4E + setRAMYAddressCounter byte = 0x4F + setAnalogBlockControl byte = 0x74 + setDigitalBlockControl byte = 0x7E +) + +// Register values +const ( + gateDrivingVoltage19V = 0x15 + + sourceDrivingVoltageVSH1_15V = 0x41 + sourceDrivingVoltageVSH2_5V = 0xA8 + sourceDrivingVoltageVSL_neg15V = 0x32 +) + +// Flags for the displayUpdateControl2 command +const ( + displayUpdateDisableClock byte = 1 << iota + displayUpdateDisableAnalog + displayUpdateDisplay + displayUpdateMode2 + displayUpdateLoadLUTFromOTP + displayUpdateLoadTemperature + displayUpdateEnableClock + displayUpdateEnableAnalog +) + +// Dev defines the handler which is used to access the display. +type Dev struct { + c conn.Conn + + dc gpio.PinOut + cs gpio.PinOut + 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 + +// Opts definies the structure of the display configuration. +type Opts struct { + Width int + Height int + Origin Corner + FullUpdate LUT + PartialUpdate LUT +} + +// PartialUpdate defines if the display should do a full update or just a partial update. +type PartialUpdate bool + +const ( + // Full should update the complete display. + Full PartialUpdate = false + // Partial should update only partial parts of the display. + Partial PartialUpdate = true +) + +// 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(4*physic.MegaHertz, spi.Mode0, 8) + if err != nil { + return nil, err + } + + if err := busy.In(gpio.Float, gpio.FallingEdge); err != nil { + 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.Rectangle{Max: displaySize}, + buffer: image1bit.NewVerticalLSB(image.Rectangle{ + Max: bufferSize, + }), + mode: Full, + opts: opts, + } + + // Default color + draw.Src.Draw(d.buffer, d.buffer.Bounds(), &image.Uniform{image1bit.On}, image.Point{}) + + return d, nil +} + +// NewHat creates new handler which is used to access the display. Default Waveshare Hat configuration is used. +func NewHat(p spi.Port, opts *Opts) (*Dev, error) { + dc := rpi.P1_22 + cs := rpi.P1_24 + rst := rpi.P1_11 + busy := rpi.P1_18 + return New(p, dc, cs, rst, busy, opts) +} + +func (d *Dev) configMode(ctrl controller) { + var lut LUT + + if d.mode == Full { + lut = d.opts.FullUpdate + } else { + lut = d.opts.PartialUpdate + } + + configDisplayMode(ctrl, d.mode, lut) +} + +// Init configures the display for usage through the other functions. +func (d *Dev) Init() error { + // Hardware Reset + if err := d.Reset(); err != nil { + return err + } + + eh := errorHandler{d: *d} + + initDisplay(&eh, d.opts) + + if eh.err == nil { + d.configMode(&eh) + } + + return eh.err +} + +// SetUpdateMode changes the way updates to the displayed image are applied. In +// Full mode (the default) a full refresh is done with all pixels cleared and +// re-applied. In Partial mode only the changed pixels are updated (aligned to +// multiples of 8 on the horizontal axis), potentially leaving behind small +// optical artifacts due to the way e-paper displays work. +// +// The vendor datasheet recommends a full update at least once every 24 hours. +// When using partial updates the Clear function can be used for the purpose, +// followed by re-drawing. +func (d *Dev) SetUpdateMode(mode PartialUpdate) error { + d.mode = mode + + eh := errorHandler{d: *d} + d.configMode(&eh) + + return eh.err +} + +// Clear clears the display. +func (d *Dev) Clear(color color.Color) error { + return d.Draw(d.buffer.Bounds(), &image.Uniform{ + C: image1bit.BitModel.Convert(color).(image1bit.Bit), + }, image.Point{}) +} + +// ColorModel returns a 1Bit color model. +func (d *Dev) ColorModel() color.Model { + return image1bit.BitModel +} + +// Bounds returns the bounds for the configurated display. +func (d *Dev) Bounds() image.Rectangle { + return d.bounds +} + +// Draw draws the given image to the display. Only the destination area is +// uploaded. Depending on the update mode the whole display or the destination +// area is refreshed. +func (d *Dev) Draw(dstRect image.Rectangle, src image.Image, srcPts image.Point) error { + opts := drawOpts{ + devSize: d.bounds.Max, + origin: d.opts.Origin, + buffer: d.buffer, + dstRect: dstRect, + src: src, + srcPts: srcPts, + } + + eh := errorHandler{d: *d} + + drawImage(&eh, &opts, d.mode) + + if eh.err == nil { + updateDisplay(&eh, d.mode) + } + + return eh.err +} + +// DrawPartial draws the given image to the display. +// +// Deprecated: Use Draw instead. DrawPartial merely forwards all calls. +func (d *Dev) DrawPartial(dstRect image.Rectangle, src image.Image, srcPts image.Point) error { + return d.Draw(dstRect, src, srcPts) +} + +// Halt clears the display. +func (d *Dev) Halt() error { + return d.Clear(image1bit.On) +} + +// String returns a string containing configuration information. +func (d *Dev) String() string { + 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 +// calling Init again. +func (d *Dev) Sleep() error { + eh := errorHandler{d: *d} + + // Turn off DC/DC converter, clock, output load and MCU. RAM content is + // retained. + eh.sendCommand(deepSleepMode) + eh.sendData([]byte{0x01}) + + return eh.err +} + +var _ display.Drawer = &Dev{} + +// refactored + +// EPD2in13v3 cointains display configuration for the Waveshare 2in13v2. +var EPD2in13v3 = Opts{ + Width: 122, + Height: 250, + FullUpdate: LUT{ + 0x80, 0x4A, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x4A, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x80, 0x4A, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x4A, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0xF, 0x0, 0x0, 0x2, + 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + 0x22, 0x17, 0x41, 0x0, 0x32, 0x36, + }, + PartialUpdate: LUT{ + 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x14, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + 0x22, 0x17, 0x41, 0x00, 0x32, 0x36, + }, +} + +// Reset the hardware. +func (d *Dev) Reset() error { + eh := errorHandler{d: *d} + + eh.rstOut(gpio.High) + time.Sleep(20 * time.Millisecond) + eh.rstOut(gpio.Low) + time.Sleep(2 * time.Millisecond) + eh.rstOut(gpio.High) + time.Sleep(20 * time.Millisecond) + + return eh.err +} diff --git a/waveshare2in13v3/waveshare213v3_test.go b/waveshare2in13v3/waveshare213v3_test.go new file mode 100644 index 0000000..c2123ae --- /dev/null +++ b/waveshare2in13v3/waveshare213v3_test.go @@ -0,0 +1,95 @@ +// Copyright 2022 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 waveshare2in13v3 + +import ( + "image" + "testing" + + "github.com/google/go-cmp/cmp" + "periph.io/x/conn/v3/gpio" + "periph.io/x/conn/v3/gpio/gpiotest" + "periph.io/x/conn/v3/spi/spitest" + "periph.io/x/devices/v3/ssd1306/image1bit" +) + +func TestNew(t *testing.T) { + for _, tc := range []struct { + name string + opts Opts + wantString string + wantBounds image.Rectangle + wantBufferBounds image.Rectangle + }{ + { + name: "empty", + wantString: "epd.Dev{playback, (0), Width: 0, Height: 0}", + }, + { + name: "EPD2in13v3", + opts: EPD2in13v3, + wantBounds: image.Rect(0, 0, 122, 250), + wantBufferBounds: image.Rect(0, 0, 128, 250), + wantString: "epd.Dev{playback, (0), Width: 122, Height: 250}", + }, + { + name: "EPD2in13v3, top right", + opts: func() Opts { + opts := EPD2in13v3 + 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 := EPD2in13v3 + 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{ + EdgesChan: make(chan gpio.Level, 1), + }, &tc.opts) + if err != nil { + t.Errorf("New() failed: %v", err) + } + + if diff := cmp.Diff(dev.String(), tc.wantString); diff != "" { + t.Errorf("String() difference (-got +want):\n%s", diff) + } + + if diff := cmp.Diff(dev.Bounds(), tc.wantBounds); diff != "" { + t.Errorf("Bounds() difference (-got +want):\n%s", diff) + } + + if diff := cmp.Diff(dev.buffer.Bounds(), tc.wantBufferBounds); diff != "" { + t.Errorf("buffer.Bounds() difference (-got +want):\n%s", diff) + } + + if !dev.buffer.Bounds().Empty() { + for _, pos := range []image.Point{ + image.Pt(0, 0), + image.Pt(dev.buffer.Bounds().Max.X-1, 0), + image.Pt(dev.buffer.Bounds().Max.X-1, dev.buffer.Bounds().Max.Y-1), + image.Pt(0, dev.buffer.Bounds().Max.Y-1), + image.Pt(dev.buffer.Bounds().Dx()/2, dev.buffer.Bounds().Dy()/2), + } { + if diff := cmp.Diff(dev.buffer.BitAt(pos.X, pos.Y), image1bit.On); diff != "" { + t.Errorf("buffer.BitAt(%v) difference (-got +want):\n%s", pos, diff) + } + } + } + }) + } +} diff --git a/waveshare2in13v4/controller.go b/waveshare2in13v4/controller.go new file mode 100644 index 0000000..36b34de --- /dev/null +++ b/waveshare2in13v4/controller.go @@ -0,0 +1,195 @@ +// 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 waveshare2in13v4 + +type controller interface { + sendCommand(byte) + sendData([]byte) + sendByte(byte) + readBusy() +} + +func initDisplay(ctrl controller, opts *Opts) { + // self.ReadBusy() + ctrl.readBusy() + // self.send_command(0x12) #SWRESET + ctrl.sendCommand(swReset) + // self.ReadBusy() + ctrl.readBusy() + + // self.send_command(0x01) #Driver output control + ctrl.sendCommand(driverOutputControl) + // self.send_data(0xf9) + // self.send_data(0x00) + // self.send_data(0x00) + ctrl.sendData([]byte{0xF9, 0x00, 0x00}) + + // self.send_command(0x11) #data entry mode + ctrl.sendCommand(dataEntryModeSetting) + // self.send_data(0x03) + ctrl.sendByte(0x03) + + // self.SetWindow(0, 0, self.width-1, self.height-1) + setWindow(ctrl, 0, 0, opts.Width-1, opts.Height-1) + // self.SetCursor(0, 0) + setCursor(ctrl, 0, 0) + + // self.send_command(0x3c) + ctrl.sendCommand(borderWaveformControl) + // self.send_data(0x05) + ctrl.sendByte(0x05) + + // self.send_command(0x21) # Display update control + ctrl.sendCommand(displayUpdateControl1) + // self.send_data(0x00) + // self.send_data(0x80) + ctrl.sendData([]byte{0x80, 0x80}) + + // self.send_command(0x18) + ctrl.sendCommand(tempSensorSelect) + // self.send_data(0x80) + ctrl.sendByte(0x80) + + // self.ReadBusy() + ctrl.readBusy() +} + +func initDisplayFast(ctrl controller, opts *Opts) { + // self.send_command(0x12) #SWRESET + ctrl.sendCommand(swReset) + // self.ReadBusy() + ctrl.readBusy() + + // self.send_command(0x18) + ctrl.sendCommand(tempSensorSelect) + // self.send_data(0x80) + ctrl.sendByte(0x80) + + // self.send_command(0x11) #data entry mode + ctrl.sendCommand(dataEntryModeSetting) + // self.send_data(0x03) + ctrl.sendByte(0x03) + + // self.SetWindow(0, 0, self.width-1, self.height-1) + setWindow(ctrl, 0, 0, opts.Width-1, opts.Height-1) + // self.SetCursor(0, 0) + setCursor(ctrl, 0, 0) + + // self.send_command(0x22) # Load temperature value + ctrl.sendCommand(displayUpdateControl2) + // self.send_data(0xB1) + ctrl.sendByte(0x81) + // self.send_command(0x20) + ctrl.sendCommand(masterActivation) + // self.ReadBusy() + ctrl.readBusy() + + // self.send_command(0x1A) # Write to temperature register + ctrl.sendCommand(tempSensorRegWrite) + // self.send_data(0x64) + // self.send_data(0x00) + ctrl.sendData([]byte{0x64, 0x00}) + + // self.send_command(0x22) # Load temperature value + ctrl.sendCommand(displayUpdateControl2) + // self.send_data(0x91) + ctrl.sendByte(0x91) + // self.send_command(0x20) + ctrl.sendCommand(masterActivation) + // self.ReadBusy() + ctrl.readBusy() +} + +func updateDisplay(ctrl controller, mode PartialUpdate) { + var displayUpdateFlags byte + + if mode == Partial { + // Make use of red buffer + displayUpdateFlags = 0b1000_0000 + } + + ctrl.sendCommand(displayUpdateControl1) + ctrl.sendData([]byte{displayUpdateFlags}) + + ctrl.sendCommand(displayUpdateControl2) + ctrl.sendData([]byte{ + displayUpdateDisableClock | + displayUpdateDisableAnalog | + displayUpdateDisplay | + displayUpdateEnableClock | + displayUpdateEnableAnalog, + }) + + ctrl.sendCommand(masterActivation) + ctrl.readBusy() +} + +// new + +// turnOnDisplay turns on the display. +func turnOnDisplay(ctrl controller) { + ctrl.sendCommand(displayUpdateControl2) + ctrl.sendByte(0xf7) + ctrl.sendCommand(masterActivation) + ctrl.readBusy() +} + +// turnOnDisplayFast turns on the display fast. +func turnOnDisplayFast(ctrl controller) { + ctrl.sendCommand(displayUpdateControl2) + ctrl.sendByte(0xC7) + ctrl.sendCommand(masterActivation) + ctrl.readBusy() +} + +// turnOnDisplayPart turns on the display for a partial update. +func turnOnDisplayPart(ctrl controller) { + ctrl.sendCommand(displayUpdateControl2) + ctrl.sendByte(0xFF) + ctrl.sendCommand(masterActivation) + ctrl.readBusy() +} + +// setWindow sets the display window size. +func setWindow(ctrl controller, x_start int, y_start int, x_end int, y_end int) { + ctrl.sendCommand(setRAMXAddressStartEndPosition) + ctrl.sendData([]byte{byte((x_start >> 3) & 0xFF), byte((x_end >> 3) & 0xFF)}) + + ctrl.sendCommand(setRAMYAddressStartEndPosition) + ctrl.sendData([]byte{byte(y_start & 0xFF), byte((y_start >> 8) & 0xFF), byte(y_end & 0xFF), byte((y_end >> 8) & 0xFF)}) +} + +// setCursor positions the cursor. +func setCursor(ctrl controller, x int, y int) { + ctrl.sendCommand(setRAMXAddressCounter) + // x point must be the multiple of 8 or the last 3 bits will be ignored + ctrl.sendData([]byte{byte(x & 0xFF)}) + + ctrl.sendCommand(setRAMYAddressCounter) + ctrl.sendData([]byte{byte(y & 0xFF), byte((y >> 8) & 0xFF)}) +} + +func clear(ctrl controller, color byte, opts *Opts) { + var linewidth int + if opts.Width%8 == 0 { + linewidth = int(opts.Width / 8) + } else { + linewidth = int(opts.Width/8) + 1 + } + + var buff []byte + ctrl.sendCommand(writeRAMBW) + for j := 0; j < opts.Height; j++ { + for i := 0; i < linewidth; i++ { + buff = append(buff, color) + } + } + ctrl.sendData(buff) + // new + ctrl.sendCommand(writeRAMRed) + ctrl.sendData(buff) + + turnOnDisplay(ctrl) +} diff --git a/waveshare2in13v4/controller_test.go b/waveshare2in13v4/controller_test.go new file mode 100644 index 0000000..4c9b1e8 --- /dev/null +++ b/waveshare2in13v4/controller_test.go @@ -0,0 +1,237 @@ +// 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 waveshare2in13v4 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +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 (r *fakeController) sendByte(data byte) { + cur := &(*r)[len(*r)-1] + cur.data = append(cur.data, data) +} + +func (*fakeController) readBusy() { +} + +func TestInitDisplay(t *testing.T) { + for _, tc := range []struct { + name string + opts Opts + want []record + }{ + { + name: "epd2in13v4", + opts: EPD2in13v4, + want: []record{ + {cmd: swReset}, + { + cmd: driverOutputControl, + data: []byte{250 - 1, 0, 0}, + }, + {cmd: dataEntryModeSetting, data: []byte{0x03}}, + {cmd: setRAMXAddressStartEndPosition, data: []uint8{0x00, 0x0f}}, + {cmd: setRAMYAddressStartEndPosition, data: []uint8{0x00, 0x00, 0xf9, 0x00}}, + {cmd: setRAMXAddressCounter, data: []uint8{0x00}}, + {cmd: setRAMYAddressCounter, data: []uint8{0x00, 0x00}}, + {cmd: borderWaveformControl, data: []uint8{0x05}}, + {cmd: displayUpdateControl1, data: []uint8{0x80, 0x80}}, + {cmd: tempSensorSelect, data: []uint8{0x80}}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var got fakeController + + initDisplay(&got, &tc.opts) + + if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" { + t.Errorf("initDisplay() difference (-got +want):\n%s", diff) + } + }) + } +} + +func TestInitDisplayFast(t *testing.T) { + for _, tc := range []struct { + name string + opts Opts + want []record + }{ + { + name: "epd2in13v4", + opts: EPD2in13v4, + want: []record{ + {cmd: swReset}, + {cmd: tempSensorSelect, data: []uint8{0x80}}, + {cmd: dataEntryModeSetting, data: []byte{0x03}}, + {cmd: setRAMXAddressStartEndPosition, data: []uint8{0x00, 0x0f}}, + {cmd: setRAMYAddressStartEndPosition, data: []uint8{0x00, 0x00, 0xf9, 0x00}}, + {cmd: setRAMXAddressCounter, data: []uint8{0x00}}, + {cmd: setRAMYAddressCounter, data: []uint8{0x00, 0x00}}, + {cmd: displayUpdateControl2, data: []uint8{0x81}}, + {cmd: masterActivation}, + {cmd: tempSensorRegWrite, data: []uint8{0x64, 0x00}}, + {cmd: displayUpdateControl2, data: []uint8{0x91}}, + {cmd: masterActivation}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var got fakeController + + initDisplayFast(&got, &tc.opts) + + if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" { + t.Errorf("initDisplay() difference (-got +want):\n%s", diff) + } + }) + } +} + +func TestUpdateDisplay(t *testing.T) { + for _, tc := range []struct { + name string + mode PartialUpdate + want []record + }{ + { + name: "full", + mode: Full, + want: []record{ + {cmd: displayUpdateControl1, data: []byte{0}}, + {cmd: displayUpdateControl2, data: []byte{0xc7}}, + {cmd: masterActivation}, + }, + }, + { + name: "partial", + mode: Partial, + want: []record{ + {cmd: displayUpdateControl1, data: []byte{0x80}}, + {cmd: displayUpdateControl2, data: []byte{0xc7}}, + {cmd: masterActivation}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var got fakeController + + updateDisplay(&got, tc.mode) + + if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" { + t.Errorf("updateDisplay() difference (-got +want):\n%s", diff) + } + }) + } +} + +func TestTurnOnDisplayFast(t *testing.T) { + for _, tc := range []struct { + name string + want []record + }{ + { + name: "Fast", + want: []record{ + {cmd: displayUpdateControl2, data: []byte{0xC7}}, + {cmd: masterActivation}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var got fakeController + + turnOnDisplayFast(&got) + + if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" { + t.Errorf("updateDisplay() difference (-got +want):\n%s", diff) + } + }) + } +} + +func TestTurnOnDisplayPart(t *testing.T) { + for _, tc := range []struct { + name string + want []record + }{ + { + name: "Part", + want: []record{ + {cmd: displayUpdateControl2, data: []byte{0xFF}}, + {cmd: masterActivation}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var got fakeController + + turnOnDisplayPart(&got) + + if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" { + t.Errorf("updateDisplay() difference (-got +want):\n%s", diff) + } + }) + } +} + +func TestClear(t *testing.T) { + var buff []byte + const linewidth = int(122/8) + 1 + for j := 0; j < 250; j++ { + for i := 0; i < linewidth; i++ { + buff = append(buff, 0x00) + } + } + for _, tc := range []struct { + name string + opts Opts + color byte + want []record + }{ + { + name: "clear", + opts: EPD2in13v4, + want: []record{ + {cmd: writeRAMBW, data: buff}, + {cmd: writeRAMRed, data: buff}, + {cmd: displayUpdateControl2, data: []byte{0xf7}}, + {cmd: masterActivation}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var got fakeController + + clear(&got, tc.color, &tc.opts) + + if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" { + t.Errorf("updateDisplay() difference (-got +want):\n%s", diff) + } + }) + } +} diff --git a/waveshare2in13v4/doc.go b/waveshare2in13v4/doc.go new file mode 100644 index 0000000..5eb47a3 --- /dev/null +++ b/waveshare2in13v4/doc.go @@ -0,0 +1,20 @@ +// Copyright 2023 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 waveshare2in13v3 controls Waveshare 2.13 v3 e-paper displays. +// +// Datasheet: +// https://files.waveshare.com/upload/5/59/2.13inch_e-Paper_V3_Specificition.pdf +// +// Product page: +// 2.13 inch version 4: https://www.waveshare.com/wiki/2.13inch_e-Paper_HAT_Manual#Resources +// This display is an Active Matrix Electrophoretic Display (AM EPD), with +// interface and a reference system design. The display is capable to display +// imagesat 1-bit white, black full display capabilities. The 2.13inch active area +// contains 250×122 pixels. The module is a TFT-array driving electrophoresis +// display, withintegrated circuits including gate driver, source driver, MCU +// interface, timingcontroller, oscillator, DC-DC, SRAM, LUT, VCOM. Module can be +// used in portableelectronic devices, such as Electronic Shelf Label (ESL) System. +// v4 is fully compatible with version 3 however v4 features fast refresh capabilities v3 doesn't +package waveshare2in13v4 diff --git a/waveshare2in13v4/drawing.go b/waveshare2in13v4/drawing.go new file mode 100644 index 0000000..bf76b8d --- /dev/null +++ b/waveshare2in13v4/drawing.go @@ -0,0 +1,191 @@ +// 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 waveshare2in13v4 + +import ( + "image" + "image/draw" + + "periph.io/x/devices/v3/ssd1306/image1bit" +) + +type drawOpts struct { + commands []byte + devSize image.Point + origin Corner + buffer *image1bit.VerticalLSB + dstRect image.Rectangle + src image.Image + srcPts image.Point +} + +type drawSpec struct { + // 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. 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{ + bufferDstRect: image.Rectangle{Max: o.devSize}.Intersect(o.dstRect), + } + + 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. +func (o *drawOpts) sendImage(ctrl controller, cmd byte, spec *drawSpec) { + if spec.memRect.Empty() { + return + } + + 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 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++ { + 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 + } + } + } + + ctrl.sendData(rowData) + } + +} + +func drawImage(ctrl controller, opts *drawOpts, mode PartialUpdate) { + s := opts.spec() + + if s.memRect.Empty() { + return + } + + // 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 + + if len(commands) == 0 { + commands = []byte{writeRAMBW, writeRAMRed} + } + + // Keep the two buffers in sync. + for _, cmd := range commands { + opts.sendImage(ctrl, cmd, &s) + } + + turnOnDisplay(ctrl) +} diff --git a/waveshare2in13v4/drawing_test.go b/waveshare2in13v4/drawing_test.go new file mode 100644 index 0000000..b055f0f --- /dev/null +++ b/waveshare2in13v4/drawing_test.go @@ -0,0 +1,590 @@ +// 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 waveshare2in13v4 + +import ( + "bytes" + "image" + "image/draw" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "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) { + 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", + opts: drawOpts{ + devSize: image.Pt(100, 200), + buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 120, 210)), + dstRect: image.Rect(17, 4, 25, 8), + }, + want: drawSpec{ + bufferDstRect: image.Rect(17, 4, 25, 8), + memDstRect: image.Rect(17, 4, 25, 8), + memRect: image.Rect(2, 4, 4, 8), + }, + }, + { + name: "larger than display", + opts: drawOpts{ + devSize: image.Pt(100, 200), + buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 100, 200)), + dstRect: image.Rect(-20, 50, 125, 300), + }, + want: drawSpec{ + 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) + } + }) + } +} + +func TestSendImage(t *testing.T) { + for _, tc := range []struct { + name string + cmd byte + opts drawOpts + want []record + }{ + { + name: "empty", + opts: drawOpts{ + buffer: image1bit.NewVerticalLSB(image.Rectangle{}), + }, + }, + { + name: "partial", + cmd: writeRAMBW, + 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: writeRAMBW, + data: bytes.Repeat([]byte{0}, 2*(30-10)), + }, + }, + }, + { + name: "partial non-aligned", + cmd: writeRAMRed, + 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: writeRAMRed, + data: bytes.Repeat([]byte{0x7f, 0xff, 0xff, 0x80}, 4), + }, + }, + }, + { + name: "full", + cmd: writeRAMBW, + 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: writeRAMBW, + data: bytes.Repeat([]byte{0xff}, 80/8*120), + }, + }, + }, + { + 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: 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: 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: 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: 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: 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) + + if diff := cmp.Diff([]record(got), tc.want, cmpopts.EquateEmpty(), cmp.AllowUnexported(record{})); diff != "" { + t.Errorf("sendImage() 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{ + buffer: image1bit.NewVerticalLSB(image.Rectangle{}), + }, + }, + { + name: "partial", + opts: drawOpts{ + commands: []byte{writeRAMBW}, + devSize: image.Pt(64, 64), + buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 64, 64)), + dstRect: image.Rect(17, 4, 41, 8), + src: &image.Uniform{image1bit.On}, + srcPts: image.Pt(0, 0), + }, + want: []record{ + { + cmd: writeRAMBW, + data: bytes.Repeat([]byte{0x7f, 0xff, 0xff, 0x80}, 4), + }, + {cmd: displayUpdateControl2, data: []byte{0xf7}}, + {cmd: masterActivation}, + }, + }, + { + name: "full", + opts: drawOpts{ + commands: []byte{writeRAMRed}, + devSize: image.Pt(80, 120), + buffer: image1bit.NewVerticalLSB(image.Rect(0, 0, 80, 120)), + dstRect: image.Rect(0, 0, 80, 120), + src: &image.Uniform{image1bit.On}, + srcPts: image.Pt(33, 44), + }, + want: []record{ + { + cmd: writeRAMRed, + data: bytes.Repeat([]byte{0xff}, 80/8*120), + }, + {cmd: displayUpdateControl2, data: []byte{0xf7}}, + {cmd: masterActivation}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var got fakeController + + drawImage(&got, &tc.opts, true) + + 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/waveshare2in13v4/errorhandler.go b/waveshare2in13v4/errorhandler.go new file mode 100644 index 0000000..2839b6b --- /dev/null +++ b/waveshare2in13v4/errorhandler.go @@ -0,0 +1,84 @@ +// 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 waveshare2in13v4 + +import ( + "time" + + "periph.io/x/conn/v3/gpio" +) + +// errorHandler is a wrapper for error management. +type errorHandler struct { + d Dev + err error +} + +func (eh *errorHandler) rstOut(l gpio.Level) { + if eh.err != nil { + return + } + eh.err = eh.d.rst.Out(l) +} + +func (eh *errorHandler) cTx(w []byte, r []byte) { + if eh.err != nil { + return + } + eh.err = eh.d.c.Tx(w, r) +} + +func (eh *errorHandler) dcOut(l gpio.Level) { + if eh.err != nil { + return + } + eh.err = eh.d.dc.Out(l) +} + +func (eh *errorHandler) csOut(l gpio.Level) { + if eh.err != nil { + return + } + eh.err = eh.d.cs.Out(l) +} + +func (eh *errorHandler) readBusy() { + for busy := eh.d.busy; busy.Read() == gpio.High; { + busy.WaitForEdge(10 * time.Millisecond) + } +} + +func (eh *errorHandler) sendCommand(cmd byte) { + if eh.err != nil { + return + } + + eh.dcOut(gpio.Low) + eh.csOut(gpio.Low) + eh.cTx([]byte{cmd}, nil) + eh.csOut(gpio.High) +} + +func (eh *errorHandler) sendByte(data byte) { + if eh.err != nil { + return + } + + eh.dcOut(gpio.High) + eh.csOut(gpio.Low) + eh.cTx([]byte{data}, nil) + eh.csOut(gpio.High) +} + +func (eh *errorHandler) sendData(data []byte) { + if eh.err != nil { + return + } + + eh.dcOut(gpio.High) + eh.csOut(gpio.Low) + eh.cTx(data, nil) + eh.csOut(gpio.High) +} diff --git a/waveshare2in13v4/example_test.go b/waveshare2in13v4/example_test.go new file mode 100644 index 0000000..abe9699 --- /dev/null +++ b/waveshare2in13v4/example_test.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 waveshare2in13v4_test + +import ( + "image" + "image/draw" + "log" + + "golang.org/x/image/font" + "golang.org/x/image/font/basicfont" + "golang.org/x/image/math/fixed" + + "periph.io/x/conn/v3/spi/spireg" + "periph.io/x/devices/v3/ssd1306/image1bit" + "periph.io/x/devices/v3/waveshare2in13v3" + "periph.io/x/host/v3" +) + +func Example() { + // Make sure periph is initialized. + if _, err := host.Init(); err != nil { + log.Fatal(err) + } + + // Use spireg SPI bus registry to find the first available SPI bus. + b, err := spireg.Open("") + if err != nil { + log.Fatal(err) + } + defer b.Close() + + dev, err := waveshare2in13v3.NewHat(b, &waveshare2in13v3.EPD2in13v3) // Display config and size + if err != nil { + log.Fatalf("Failed to initialize driver: %v", err) + } + + err = dev.Init() + if err != nil { + log.Fatalf("Failed to initialize display: %v", err) + } + + // Draw on it. Black text on a white background. + img := image1bit.NewVerticalLSB(dev.Bounds()) + draw.Draw(img, img.Bounds(), &image.Uniform{image1bit.On}, image.Point{}, draw.Src) + f := basicfont.Face7x13 + drawer := font.Drawer{ + Dst: img, + Src: &image.Uniform{image1bit.Off}, + Face: f, + Dot: fixed.P(0, img.Bounds().Dy()-1-f.Descent), + } + drawer.DrawString("Hello from periph!") + + if err := dev.Draw(dev.Bounds(), img, image.Point{}); err != nil { + log.Fatal(err) + } +} + +func Example_other() { + // Make sure periph is initialized. + if _, err := host.Init(); err != nil { + log.Fatal(err) + } + + // Use spireg SPI bus registry to find the first available SPI bus. + b, err := spireg.Open("") + if err != nil { + log.Fatal(err) + } + defer b.Close() + + dev, err := waveshare2in13v3.NewHat(b, &waveshare2in13v3.EPD2in13v3) // Display config and size + if err != nil { + log.Fatalf("Failed to initialize driver: %v", err) + } + + err = dev.Init() + if err != nil { + log.Fatalf("Failed to initialize display: %v", err) + } + + var img image.Image + // Note: this code is commented out so periph does not depend on: + // "github.com/fogleman/gg" + // "github.com/golang/freetype/truetype" + // "golang.org/x/image/font/gofont/goregular" + // bounds := dev.Bounds() + // w := bounds.Dx() + // h := bounds.Dy() + // dc := gg.NewContext(w, h) + // im, err := gg.LoadPNG("gopher.png") + // if err != nil { + // panic(err) + // } + // dc.SetRGB(1, 1, 1) + // dc.Clear() + // dc.SetRGB(0, 0, 0) + // dc.Rotate(gg.Radians(90)) + // dc.Translate(0.0, -float64(h/2)) + // font, err := truetype.Parse(goregular.TTF) + // if err != nil { + // panic(err) + // } + // face := truetype.NewFace(font, &truetype.Options{ + // Size: 16, + // }) + // dc.SetFontFace(face) + // text := "Hello from periph!" + // tw, th := dc.MeasureString(text) + // dc.DrawImage(im, 120, 30) + // padding := 8.0 + // dc.DrawRoundedRectangle(padding*2, padding*2, tw+padding*2, th+padding, 10) + // dc.Stroke() + // dc.DrawString(text, padding*3, padding*2+th) + // for i := 0; i < 10; i++ { + // dc.DrawCircle(float64(30+(10*i)), 100, 5) + // } + // for i := 0; i < 10; i++ { + // dc.DrawRectangle(float64(30+(10*i)), 80, 5, 5) + // } + // dc.Fill() + // img = dc.Image() + + if err := dev.Draw(dev.Bounds(), img, image.Point{}); err != nil { + log.Fatal(err) + } +} diff --git a/waveshare2in13v4/waveshare213v4.go b/waveshare2in13v4/waveshare213v4.go new file mode 100644 index 0000000..7725072 --- /dev/null +++ b/waveshare2in13v4/waveshare213v4.go @@ -0,0 +1,348 @@ +// 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 waveshare2in13v4 + +import ( + "fmt" + "image" + "image/color" + "image/draw" + "time" + + "periph.io/x/conn/v3" + "periph.io/x/conn/v3/display" + "periph.io/x/conn/v3/gpio" + "periph.io/x/conn/v3/physic" + "periph.io/x/conn/v3/spi" + "periph.io/x/devices/v3/ssd1306/image1bit" + "periph.io/x/host/v3/rpi" +) + +// Commands +const ( + driverOutputControl byte = 0x01 + gateDrivingVoltageControl byte = 0x03 + sourceDrivingVoltageControl byte = 0x04 + initialCodeSettingOTPProgram byte = 0x08 + writeRegisterForInitialCodeSetting byte = 0x09 + readRegisterForInitialCodeSetting byte = 0x0A + boosterSoftStartControl byte = 0x0C + deepSleepMode byte = 0x10 + dataEntryModeSetting byte = 0x11 + swReset byte = 0x12 + hvReadyDetection byte = 0x14 + vciDetection byte = 0x15 + tempSensorSelect byte = 0x18 + tempSensorRegWrite byte = 0x1A + tempSensorRegRead byte = 0x1B + tempSensorExtWrite byte = 0x1C + masterActivation byte = 0x20 + displayUpdateControl1 byte = 0x21 + displayUpdateControl2 byte = 0x22 + writeRAMBW byte = 0x24 + writeRAMRed byte = 0x26 + readRAM byte = 0x27 + vcomSense byte = 0x28 + vcomDuration byte = 0x29 + vcomProgramOTP byte = 0x2A + vcomWriteRegisterControl byte = 0x2B + vcomRegisterWrite byte = 0x2C + otpReadRegisterDisplayOpt byte = 0x2D + userIDRead byte = 0x2E + statusBitRead byte = 0x2F + otpProgramWaveformSetting byte = 0x30 + otpLoadWaveformSetting byte = 0x31 + writeLutRegister byte = 0x32 + crcCalculation byte = 0x34 + crcStatusRead byte = 0x35 + otpProgramSelect byte = 0x36 + writeRegisterForDisplayOption byte = 0x37 + writeRegisterForUserID byte = 0x38 + otpProgramMode byte = 0x39 + borderWaveformControl byte = 0x3C + endOptionEOPT byte = 0x3F + readRAMOpt byte = 0x41 + setRAMXAddressStartEndPosition byte = 0x44 + setRAMYAddressStartEndPosition byte = 0x45 + autoWriteRedRAMRegpattern byte = 0x46 + autoWriteBWRamRegPattern byte = 0x47 + setRAMXAddressCounter byte = 0x4E + setRAMYAddressCounter byte = 0x4F + nop byte = 0x7F +) + +// Register values +const ( + gateDrivingVoltage19V = 0x15 + + sourceDrivingVoltageVSH1_15V = 0x41 + sourceDrivingVoltageVSH2_5V = 0xA8 + sourceDrivingVoltageVSL_neg15V = 0x32 +) + +// Flags for the displayUpdateControl2 command +const ( + displayUpdateDisableClock byte = 1 << iota + displayUpdateDisableAnalog + displayUpdateDisplay + displayUpdateMode2 + displayUpdateLoadLUTFromOTP + displayUpdateLoadTemperature + displayUpdateEnableClock + displayUpdateEnableAnalog +) + +// Dev defines the handler which is used to access the display. +type Dev struct { + c conn.Conn + + dc gpio.PinOut + cs gpio.PinOut + 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 + +// Opts definies the structure of the display configuration. +type Opts struct { + Width int + Height int + Origin Corner +} + +// PartialUpdate defines if the display should do a full update or just a partial update. +type PartialUpdate bool + +const ( + // Full should update the complete display. + Full PartialUpdate = false + // Partial should update only partial parts of the display. + Partial PartialUpdate = true +) + +// FastDisplay defines if the display can display in fast mode. +type FastDisplay bool + +const ( + // Updates the display fast. + Fast PartialUpdate = true + // Updates the display at normal speed. + Normal PartialUpdate = false +) + +// 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(4*physic.MegaHertz, spi.Mode0, 8) + if err != nil { + return nil, err + } + + if err := busy.In(gpio.Float, gpio.FallingEdge); err != nil { + 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.Rectangle{Max: displaySize}, + buffer: image1bit.NewVerticalLSB(image.Rectangle{ + Max: bufferSize, + }), + mode: Full, + opts: opts, + } + + // Default color + draw.Src.Draw(d.buffer, d.buffer.Bounds(), &image.Uniform{image1bit.On}, image.Point{}) + + return d, nil +} + +// NewHat creates new handler which is used to access the display. Default Waveshare Hat configuration is used. +func NewHat(p spi.Port, opts *Opts) (*Dev, error) { + dc := rpi.P1_22 + cs := rpi.P1_24 + rst := rpi.P1_11 + busy := rpi.P1_18 + return New(p, dc, cs, rst, busy, opts) +} + +// func (d *Dev) configMode(ctrl controller) { + +// configDisplayMode(ctrl, d.mode) +// } + +// Init configures the display for usage through the other functions. +func (d *Dev) Init() error { + // Hardware Reset + if err := d.Reset(); err != nil { + return err + } + + eh := errorHandler{d: *d} + + initDisplay(&eh, d.opts) + + // if eh.err == nil { + // d.configMode(&eh) + // } + + return eh.err +} + +// // SetUpdateMode changes the way updates to the displayed image are applied. In +// // Full mode (the default) a full refresh is done with all pixels cleared and +// // re-applied. In Partial mode only the changed pixels are updated (aligned to +// // multiples of 8 on the horizontal axis), potentially leaving behind small +// // optical artifacts due to the way e-paper displays work. +// // +// // The vendor datasheet recommends a full update at least once every 24 hours. +// // When using partial updates the Clear function can be used for the purpose, +// // followed by re-drawing. +// func (d *Dev) SetUpdateMode(mode PartialUpdate) error { +// d.mode = mode + +// eh := errorHandler{d: *d} +// d.configMode(&eh) + +// return eh.err +// } + +// Clear clears the display. +func (d *Dev) Clear(color color.Color) error { + return d.Draw(d.buffer.Bounds(), &image.Uniform{ + C: image1bit.BitModel.Convert(color).(image1bit.Bit), + }, image.Point{}) +} + +// ColorModel returns a 1Bit color model. +func (d *Dev) ColorModel() color.Model { + return image1bit.BitModel +} + +// Bounds returns the bounds for the configurated display. +func (d *Dev) Bounds() image.Rectangle { + return d.bounds +} + +// Draw draws the given image to the display. Only the destination area is +// uploaded. Depending on the update mode the whole display or the destination +// area is refreshed. +func (d *Dev) Draw(dstRect image.Rectangle, src image.Image, srcPts image.Point) error { + opts := drawOpts{ + devSize: d.bounds.Max, + origin: d.opts.Origin, + buffer: d.buffer, + dstRect: dstRect, + src: src, + srcPts: srcPts, + } + + eh := errorHandler{d: *d} + + drawImage(&eh, &opts, d.mode) + + if eh.err == nil { + updateDisplay(&eh, d.mode) + } + + return eh.err +} + +// DrawPartial draws the given image to the display. +// +// Deprecated: Use Draw instead. DrawPartial merely forwards all calls. +func (d *Dev) DrawPartial(dstRect image.Rectangle, src image.Image, srcPts image.Point) error { + return d.Draw(dstRect, src, srcPts) +} + +// Halt clears the display. +func (d *Dev) Halt() error { + return d.Clear(image1bit.On) +} + +// String returns a string containing configuration information. +func (d *Dev) String() string { + 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 +// calling Init again. +func (d *Dev) Sleep() error { + eh := errorHandler{d: *d} + + // Turn off DC/DC converter, clock, output load and MCU. RAM content is + // retained. + eh.sendCommand(deepSleepMode) + eh.sendData([]byte{0x01}) + + return eh.err +} + +var _ display.Drawer = &Dev{} + +// refactored + +// EPD2in13v4 cointains display configuration for the Waveshare 2in13v2. +var EPD2in13v4 = Opts{ + Width: 122, + Height: 250, +} + +// Reset the hardware. +func (d *Dev) Reset() error { + eh := errorHandler{d: *d} + + eh.rstOut(gpio.High) + time.Sleep(20 * time.Millisecond) + eh.rstOut(gpio.Low) + time.Sleep(2 * time.Millisecond) + eh.rstOut(gpio.High) + time.Sleep(20 * time.Millisecond) + + return eh.err +} diff --git a/waveshare2in13v4/waveshare213v4_test.go b/waveshare2in13v4/waveshare213v4_test.go new file mode 100644 index 0000000..4e85b55 --- /dev/null +++ b/waveshare2in13v4/waveshare213v4_test.go @@ -0,0 +1,95 @@ +// Copyright 2022 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 waveshare2in13v4 + +import ( + "image" + "testing" + + "github.com/google/go-cmp/cmp" + "periph.io/x/conn/v3/gpio" + "periph.io/x/conn/v3/gpio/gpiotest" + "periph.io/x/conn/v3/spi/spitest" + "periph.io/x/devices/v3/ssd1306/image1bit" +) + +func TestNew(t *testing.T) { + for _, tc := range []struct { + name string + opts Opts + wantString string + wantBounds image.Rectangle + wantBufferBounds image.Rectangle + }{ + { + name: "empty", + wantString: "epd.Dev{playback, (0), Width: 0, Height: 0}", + }, + { + name: "EPD2in13v4", + opts: EPD2in13v4, + wantBounds: image.Rect(0, 0, 122, 250), + wantBufferBounds: image.Rect(0, 0, 128, 250), + wantString: "epd.Dev{playback, (0), Width: 122, Height: 250}", + }, + { + name: "EPD2in13v3, top right", + opts: func() Opts { + opts := EPD2in13v4 + 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 := EPD2in13v4 + 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{ + EdgesChan: make(chan gpio.Level, 1), + }, &tc.opts) + if err != nil { + t.Errorf("New() failed: %v", err) + } + + if diff := cmp.Diff(dev.String(), tc.wantString); diff != "" { + t.Errorf("String() difference (-got +want):\n%s", diff) + } + + if diff := cmp.Diff(dev.Bounds(), tc.wantBounds); diff != "" { + t.Errorf("Bounds() difference (-got +want):\n%s", diff) + } + + if diff := cmp.Diff(dev.buffer.Bounds(), tc.wantBufferBounds); diff != "" { + t.Errorf("buffer.Bounds() difference (-got +want):\n%s", diff) + } + + if !dev.buffer.Bounds().Empty() { + for _, pos := range []image.Point{ + image.Pt(0, 0), + image.Pt(dev.buffer.Bounds().Max.X-1, 0), + image.Pt(dev.buffer.Bounds().Max.X-1, dev.buffer.Bounds().Max.Y-1), + image.Pt(0, dev.buffer.Bounds().Max.Y-1), + image.Pt(dev.buffer.Bounds().Dx()/2, dev.buffer.Bounds().Dy()/2), + } { + if diff := cmp.Diff(dev.buffer.BitAt(pos.X, pos.Y), image1bit.On); diff != "" { + t.Errorf("buffer.BitAt(%v) difference (-got +want):\n%s", pos, diff) + } + } + } + }) + } +} From 86e0892a653921983f4b7a8ea7f6b645bffe3faa Mon Sep 17 00:00:00 2001 From: Adam Siegel Date: Mon, 20 Nov 2023 18:15:49 -0800 Subject: [PATCH 2/2] updating file --- waveshare2in13v4/example_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/waveshare2in13v4/example_test.go b/waveshare2in13v4/example_test.go index abe9699..5ffc522 100644 --- a/waveshare2in13v4/example_test.go +++ b/waveshare2in13v4/example_test.go @@ -15,7 +15,7 @@ import ( "periph.io/x/conn/v3/spi/spireg" "periph.io/x/devices/v3/ssd1306/image1bit" - "periph.io/x/devices/v3/waveshare2in13v3" + "periph.io/x/devices/v3/waveshare2in13v4" "periph.io/x/host/v3" ) @@ -32,7 +32,7 @@ func Example() { } defer b.Close() - dev, err := waveshare2in13v3.NewHat(b, &waveshare2in13v3.EPD2in13v3) // Display config and size + dev, err := waveshare2in13v4.NewHat(b, &waveshare2in13v4.EPD2in13v4) // Display config and size if err != nil { log.Fatalf("Failed to initialize driver: %v", err) } @@ -72,7 +72,7 @@ func Example_other() { } defer b.Close() - dev, err := waveshare2in13v3.NewHat(b, &waveshare2in13v3.EPD2in13v3) // Display config and size + dev, err := waveshare2in13v4.NewHat(b, &waveshare2in13v4.EPD2in13v4) // Display config and size if err != nil { log.Fatalf("Failed to initialize driver: %v", err) }