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