mirror of https://github.com/periph/devices
adding v3 and v4 waveshare
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,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,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…
Reference in New Issue