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