adding v3 and v4 waveshare

pull/65/head
Adam Siegel 3 years ago
parent c14e02d418
commit 30a964cb86

@ -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)
}

@ -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)
}
})
}
}

@ -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

@ -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)
}

@ -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)
}
})
}
}

@ -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)
}

@ -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)
}
}

@ -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
}

@ -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)
}
}
}
})
}
}

@ -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)
}

@ -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)
}
})
}
}

@ -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

@ -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)
}

@ -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)
}
})
}
}

@ -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)
}

@ -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)
}
}

@ -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
}

@ -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)
}
}
}
})
}
}
Loading…
Cancel
Save