// Package epd7in3e drives the Waveshare 7.3" e-Paper (E) "Spectra 6" // panel (epd7in3e, 800x480, 6 colours) over SPI on a Raspberry Pi. package epd7in3e import ( "context" "errors" "fmt" "image" "sync" "time" ) // DefaultBusyTimeout bounds each waitIdle when Opts.BusyTimeout is zero. const DefaultBusyTimeout = 60 * time.Second // ErrAsleep is returned by Display, DisplayRaw and Clear when the // panel is in deep sleep. var ErrAsleep = errors.New("epd7in3e: device is asleep; call Init before displaying") // ErrBusyTimeout is returned by WaitIdle when the BUSY pin stays low // until the busy timeout elapses. var ErrBusyTimeout = errors.New("epd7in3e: panel still busy after the busy timeout") // ErrBadColor is returned by Clear for a value that is not one of the // six panel colours (code 0x4 does not exist on this panel). var ErrBadColor = errors.New("epd7in3e: not one of the six panel colours (code 0x4 does not exist)") // Opts configures a Dev. type Opts struct { // BusyTimeout bounds each waitIdle; zero uses DefaultBusyTimeout. BusyTimeout time.Duration // Rotate180 rotates packed frames 180 degrees. Rotate180 bool // Dither enables Floyd-Steinberg dithering in Display. Dithering // is on by default; pass Dither false to select nearest-colour // quantisation. Dither bool // Verbose logs every command, data write and busy-wait duration // to stderr. Verbose bool } // Dev drives one epd7in3e panel. Methods are serialised by a mutex // and are safe to call from multiple goroutines. type Dev struct { mu sync.Mutex b bus o Opts asleep bool } func newDev(b bus, o *Opts) *Dev { d := &Dev{b: b} if o != nil { d.o = *o } else { d.o.Dither = true } if d.o.BusyTimeout <= 0 { d.o.BusyTimeout = DefaultBusyTimeout } return d } func (c Color) isPanelColour() bool { return c <= Green && c != 4 } // reg is one command/data pair of the Init register sequence. type reg struct { cmd byte data []byte } var initRegisters = []reg{ {0xAA, []byte{0x49, 0x55, 0x20, 0x08, 0x09, 0x18}}, {0x01, []byte{0x3F}}, {0x00, []byte{0x5F, 0x69}}, {0x03, []byte{0x00, 0x54, 0x00, 0x44}}, {0x05, []byte{0x40, 0x1F, 0x1F, 0x2C}}, {0x06, []byte{0x6F, 0x1F, 0x17, 0x49}}, {0x08, []byte{0x6F, 0x1F, 0x1F, 0x22}}, {0x30, []byte{0x03}}, {0x50, []byte{0x3F}}, {0x60, []byte{0x02, 0x00}}, {0x61, []byte{0x03, 0x20, 0x01, 0xE0}}, // 800, 480 big-endian {0x84, []byte{0x01}}, {0xE3, []byte{0x2F}}, {0x04, nil}, // power on, no data } // secondBoost is the second 0x06 booster setting sent between // power-on and the refresh command. var secondBoost = []byte{0x6F, 0x1F, 0x17, 0x49} // Init puts the panel out of deep sleep and initialises it. It is // required before the first Display and after every Sleep. func (d *Dev) Init(ctx context.Context) error { d.mu.Lock() defer d.mu.Unlock() if err := d.initLocked(ctx); err != nil { return err } d.asleep = false return nil } func (d *Dev) initLocked(ctx context.Context) error { if err := d.b.Power(true); err != nil { return fmt.Errorf("epd7in3e: init: power on: %w", err) } if err := d.b.Reset(); err != nil { return fmt.Errorf("epd7in3e: init: reset: %w", err) } if err := d.waitIdle(ctx, "init"); err != nil { return err } d.b.Sleep(30 * time.Millisecond) for _, r := range initRegisters { if err := d.b.Cmd(r.cmd); err != nil { return fmt.Errorf("epd7in3e: init: command 0x%02X: %w", r.cmd, err) } if len(r.data) > 0 { if err := d.b.Data(r.data); err != nil { return fmt.Errorf("epd7in3e: init: data for 0x%02X: %w", r.cmd, err) } } } return d.waitIdle(ctx, "init (power on)") } // waitIdle waits for the BUSY pin to read HIGH, applying the busy // timeout on top of the caller's context. func (d *Dev) waitIdle(ctx context.Context, what string) error { ctx, cancel := context.WithTimeout(ctx, d.o.BusyTimeout) defer cancel() if err := d.b.WaitIdle(ctx); err != nil { return fmt.Errorf("epd7in3e: %s: %w", what, err) } return nil } // Display packs img and refreshes the panel with it. img must be // 800x480, or 480x800, which is rotated 90 degrees clockwise. By // default img is Floyd-Steinberg dithered to the panel palette. func (d *Dev) Display(ctx context.Context, img image.Image) error { d.mu.Lock() defer d.mu.Unlock() if d.asleep { return ErrAsleep } if d.o.Dither { img = Quantise(img, true) } buf := make([]byte, FrameBytes) if err := Pack(buf, img); err != nil { return fmt.Errorf("epd7in3e: display: %w", err) } return d.refreshLocked(ctx, buf) } // DisplayRaw refreshes the panel with a pre-packed frame buffer. buf // must be exactly FrameBytes long. func (d *Dev) DisplayRaw(ctx context.Context, buf []byte) error { d.mu.Lock() defer d.mu.Unlock() if d.asleep { return ErrAsleep } if len(buf) != FrameBytes { return fmt.Errorf("epd7in3e: display raw: buffer is %d bytes, want %d", len(buf), FrameBytes) } return d.refreshLocked(ctx, buf) } // Clear fills the panel with one solid colour. func (d *Dev) Clear(ctx context.Context, c Color) error { if !c.isPanelColour() { return fmt.Errorf("epd7in3e: clear: colour 0x%X: %w", c, ErrBadColor) } d.mu.Lock() defer d.mu.Unlock() if d.asleep { return ErrAsleep } buf := make([]byte, FrameBytes) v := byte(c)<<4 | byte(c) for i := range buf { buf[i] = v } return d.refreshLocked(ctx, buf) } // refreshLocked sends buf as the 0x10 frame data and runs the refresh // sequence: power on, the second 0x06 booster setting, refresh, power // off. The panel is left powered off. func (d *Dev) refreshLocked(ctx context.Context, buf []byte) error { if err := d.b.Cmd(0x10); err != nil { return fmt.Errorf("epd7in3e: refresh: command 0x10: %w", err) } if err := d.b.Data(buf); err != nil { return fmt.Errorf("epd7in3e: refresh: frame data: %w", err) } if err := d.b.Cmd(0x04); err != nil { return fmt.Errorf("epd7in3e: refresh: power on: %w", err) } if err := d.waitIdle(ctx, "refresh (power on)"); err != nil { return err } if err := d.b.Cmd(0x06); err != nil { return fmt.Errorf("epd7in3e: refresh: second booster: %w", err) } if err := d.b.Data(secondBoost); err != nil { return fmt.Errorf("epd7in3e: refresh: second booster data: %w", err) } if err := d.b.Cmd(0x12); err != nil { return fmt.Errorf("epd7in3e: refresh: refresh command: %w", err) } if err := d.b.Data([]byte{0x00}); err != nil { return fmt.Errorf("epd7in3e: refresh: refresh data: %w", err) } if err := d.waitIdle(ctx, "refresh"); err != nil { return err } if err := d.b.Cmd(0x02); err != nil { return fmt.Errorf("epd7in3e: refresh: power off: %w", err) } if err := d.b.Data([]byte{0x00}); err != nil { return fmt.Errorf("epd7in3e: refresh: power off data: %w", err) } return d.waitIdle(ctx, "refresh (power off)") } // Sleep puts the panel into deep sleep. The panel ignores commands // until the next Init. func (d *Dev) Sleep(ctx context.Context) error { d.mu.Lock() defer d.mu.Unlock() return d.sleepLocked(ctx) } // Bounds is the panel's logical frame: 800x480 at the origin. func (d *Dev) Bounds() image.Rectangle { return image.Rect(0, 0, Width, Height) } // Halt powers the panel off. If it is not already asleep, it first // enters deep sleep. A later Init brings the panel back. func (d *Dev) Halt() error { d.mu.Lock() defer d.mu.Unlock() if !d.asleep { if err := d.sleepLocked(context.Background()); err != nil { return fmt.Errorf("epd7in3e: halt: %w", err) } } if err := d.b.Power(false); err != nil { return fmt.Errorf("epd7in3e: halt: power off: %w", err) } return nil } func (d *Dev) sleepLocked(ctx context.Context) error { if err := d.b.Cmd(0x02); err != nil { return fmt.Errorf("sleep: power off: %w", err) } if err := d.b.Data([]byte{0x00}); err != nil { return fmt.Errorf("sleep: power off data: %w", err) } if err := d.waitIdle(ctx, "sleep (power off)"); err != nil { return err } if err := d.b.Cmd(0x07); err != nil { return fmt.Errorf("sleep: deep sleep: %w", err) } if err := d.b.Data([]byte{0xA5}); err != nil { return fmt.Errorf("sleep: deep sleep data: %w", err) } d.asleep = true return nil }