conn/display: simplify Drawer

display: move devices.Display as display.Drawer

This removes the last interface from devices, which was misplaced due to
historical accident. The new package display will also be the location for an
interface for text output only devices.

Remove io.Writer from this interface. While it's a good performance optimization
for some drivers, it shouldn't be required.

Change Draw():
- Return an error, so that communication erorr can be surfaced correctly,
  instead of an adhoc driver specific Err() method.
pull/1/head
Marc-Antoine Ruel 8 years ago
parent f7d0d4e3d5
commit 501558c627

@ -10,10 +10,9 @@ import (
"image"
"image/color"
"periph.io/x/periph/conn"
"periph.io/x/periph/conn/display"
"periph.io/x/periph/conn/physic"
"periph.io/x/periph/conn/spi"
"periph.io/x/periph/devices"
)
// ToRGB converts a slice of color.NRGBA to a byte stream of RGB pixels.
@ -109,23 +108,25 @@ func (d *Dev) String() string {
return fmt.Sprintf("APA102{I:%d, T:%dK, %dLEDs, %s}", d.Intensity, d.Temperature, d.numPixels, d.s)
}
// ColorModel implements devices.Display. There's no surprise, it is
// ColorModel implements display.Drawer. There's no surprise, it is
// color.NRGBAModel.
func (d *Dev) ColorModel() color.Model {
return color.NRGBAModel
}
// Bounds implements devices.Display. Min is guaranteed to be {0, 0}.
// Bounds implements display.Drawer. Min is guaranteed to be {0, 0}.
func (d *Dev) Bounds() image.Rectangle {
return d.rect
}
// Draw implements devices.Display.
// Draw implements display.Drawer.
//
// Using something else than image.NRGBA is 10x slower. When using image.NRGBA,
// the alpha channel is ignored.
func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) {
r = r.Intersect(d.rect)
func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) error {
if r = r.Intersect(d.rect); r.Empty() {
return nil
}
srcR := src.Bounds()
srcR.Min = srcR.Min.Add(sp)
if dX := r.Dx(); dX < srcR.Dx() {
@ -134,9 +135,12 @@ func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) {
if dY := r.Dy(); dY < srcR.Dy() {
srcR.Max.Y = srcR.Min.Y + dY
}
if srcR.Empty() {
return nil
}
d.l.init(d.Intensity, d.Temperature)
d.l.rasterImg(d.pixels, r, src, srcR)
_ = d.s.Tx(d.rawBuf, nil)
return d.s.Tx(d.rawBuf, nil)
}
// Write accepts a stream of raw RGB pixels and sends it as APA102 encoded
@ -301,10 +305,10 @@ func (l *lut) raster(dst []byte, src []byte) {
}
// rasterImg is the generic version of raster.
func (l *lut) rasterImg(dst []byte, r image.Rectangle, src image.Image, srcR image.Rectangle) {
func (l *lut) rasterImg(dst []byte, rect image.Rectangle, src image.Image, srcR image.Rectangle) {
// Render directly into the buffer for maximum performance and to keep
// untouched sections intact.
deltaX4 := 4 * (r.Min.X - srcR.Min.X)
deltaX4 := 4 * (rect.Min.X - srcR.Min.X)
if img, ok := src.(*image.NRGBA); ok {
// Fast path for image.NRGBA.
pix := img.Pix[srcR.Min.Y*img.Stride:]
@ -354,6 +358,4 @@ func (l *lut) rasterImg(dst []byte, r image.Rectangle, src image.Image, srcR ima
}
}
var _ conn.Resource = &Dev{}
var _ devices.Display = &Dev{}
var _ fmt.Stringer = &Dev{}
var _ display.Drawer = &Dev{}

@ -536,7 +536,9 @@ func TestDrawNRGBA(t *testing.T) {
o.Intensity = 250
o.Temperature = 5000
d, _ := New(spitest.NewRecordRaw(&buf), &o)
d.Draw(d.Bounds(), img, image.Point{})
if err := d.Draw(d.Bounds(), img, image.Point{}); err != nil {
t.Fatal(err)
}
if !bytes.Equal(expectedi250t5000, buf.Bytes()) {
t.Fatalf("%#v != %#v", expectedi250t5000, buf.Bytes())
}
@ -554,7 +556,9 @@ func TestDrawNRGBA_wide(t *testing.T) {
o.Intensity = 250
o.Temperature = 6500
d, _ := New(spitest.NewRecordRaw(&buf), &o)
d.Draw(d.Bounds(), img, image.Point{})
if err := d.Draw(d.Bounds(), img, image.Point{}); err != nil {
t.Fatal(err)
}
if !bytes.Equal(expectedi250t6500, buf.Bytes()) {
t.Fatalf("%#v != %#v", expectedi250t5000, buf.Bytes())
}
@ -575,7 +579,9 @@ func TestDrawRGBA(t *testing.T) {
o.Intensity = 250
o.Temperature = 5000
d, _ := New(spitest.NewRecordRaw(&buf), &o)
d.Draw(d.Bounds(), img, image.Point{})
if err := d.Draw(d.Bounds(), img, image.Point{}); err != nil {
t.Fatal(err)
}
if !bytes.Equal(expectedi250t5000, buf.Bytes()) {
t.Fatalf("%#v != %#v", expectedi250t5000, buf.Bytes())
}
@ -692,10 +698,14 @@ func BenchmarkDrawNRGBAColorful(b *testing.B) {
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &o)
r := d.Bounds()
p := image.Point{}
d.Draw(r, img, p)
if err := d.Draw(r, img, p); err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
d.Draw(r, img, p)
if err := d.Draw(r, img, p); err != nil {
b.Fatal(err)
}
}
}
@ -713,10 +723,14 @@ func BenchmarkDrawRGBAColorful(b *testing.B) {
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &o)
r := d.Bounds()
p := image.Point{}
d.Draw(r, img, p)
if err := d.Draw(r, img, p); err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
d.Draw(r, img, p)
if err := d.Draw(r, img, p); err != nil {
b.Fatal(err)
}
}
}

@ -41,5 +41,7 @@ func Example() {
for x := 0; x < img.Rect.Max.X; x++ {
img.SetNRGBA(x, 0, color.NRGBA{uint8(x), uint8(255 - x), 0, 255})
}
dev.Draw(dev.Bounds(), img, image.Point{})
if err := dev.Draw(dev.Bounds(), img, image.Point{}); err != nil {
log.Fatal(err)
}
}

@ -1,42 +0,0 @@
// Copyright 2016 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 devices
import (
"image"
"image/color"
"io"
"periph.io/x/periph/conn"
)
// Display represents a pixel output device. It is a write-only interface.
//
// What Display represents can be as varied as a 1 bit OLED display or a strip
// of LED lights.
type Display interface {
conn.Resource
// Writer can be used when the native display pixel format is known. Each
// write must cover exactly the whole screen as a single packed stream of
// pixels.
io.Writer
// ColorModel returns the device native color model.
//
// It is generally color.NRGBA for a color display.
ColorModel() color.Model
// Bounds returns the size of the output device.
//
// Generally displays should have Min at {0, 0} but this is not guaranteed in
// multiple displays setup or when an instance of this interface represents a
// section of a larger logical display.
Bounds() image.Rectangle
// Draw updates the display with this image starting at 'sp' offset into the
// display into 'r'. The code will likely be faster if the image is in the
// display's native color format.
//
// To be compatible with draw.Drawer, this function doesn't return an error.
Draw(r image.Rectangle, src image.Image, sp image.Point)
}

@ -1,58 +0,0 @@
// Copyright 2016 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 devicestest
import (
"errors"
"fmt"
"image"
"image/color"
"image/draw"
"periph.io/x/periph/conn"
"periph.io/x/periph/devices"
)
// Display is a fake devices.Display.
type Display struct {
Img *image.NRGBA
}
func (d *Display) String() string {
return "Display"
}
// Halt implements conn.Resource. It is a noop.
func (d *Display) Halt() error {
return nil
}
// Write implements devices.Display.
func (d *Display) Write(pixels []byte) (int, error) {
if len(pixels)%3 != 0 {
return 0, errors.New("devicetest: invalid RGB stream length")
}
copy(d.Img.Pix, pixels)
return len(pixels), nil
}
// ColorModel implements image.Image.
func (d *Display) ColorModel() color.Model {
return d.Img.ColorModel()
}
// Bounds implements image.Image.
func (d *Display) Bounds() image.Rectangle {
return d.Img.Bounds()
}
// Draw implements draw.Image.
func (d *Display) Draw(r image.Rectangle, src image.Image, sp image.Point) {
draw.Draw(d.Img, r, src, sp, draw.Src)
}
var _ conn.Resource = &Display{}
var _ devices.Display = &Display{}
var _ fmt.Stringer = &Display{}

@ -1,7 +0,0 @@
// Copyright 2016 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 devicestest contains non-hardware devices implementations for
// testing or emulation purpose.
package devicestest

@ -2,10 +2,8 @@
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
// Package devices contains interfaces for classes of devices.
// Package devices is a container for device drivers.
//
// Subpackages contain the concrete implementations. Devices accept port
// interface, constructors return concrete type.
//
// Subpackage devicestest contains fake implementations for testing.
package devices

@ -47,8 +47,7 @@ func Example() {
// Dot: fixed.P(0, img.Bounds().Dy()-1-f.Descent),
// }
// drawer.DrawString("Hello from periph!")
dev.Draw(dev.Bounds(), img, image.Point{})
if err := dev.Err(); err != nil {
if err := dev.Draw(dev.Bounds(), img, image.Point{}); err != nil {
log.Fatal(err)
}
}

@ -43,11 +43,11 @@ import (
"image/draw"
"periph.io/x/periph/conn"
"periph.io/x/periph/conn/display"
"periph.io/x/periph/conn/gpio"
"periph.io/x/periph/conn/i2c"
"periph.io/x/periph/conn/physic"
"periph.io/x/periph/conn/spi"
"periph.io/x/periph/devices"
"periph.io/x/periph/devices/ssd1306/image1bit"
)
@ -158,7 +158,6 @@ type Dev struct {
startCol, endCol int
scrolled bool
halted bool
err error
}
func (d *Dev) String() string {
@ -168,28 +167,26 @@ func (d *Dev) String() string {
return fmt.Sprintf("ssd1360.Dev{%s, %s}", d.c, d.rect.Max)
}
// ColorModel implements devices.Display.
// ColorModel implements display.Drawer.
//
// It is a one bit color model, as implemented by image1bit.Bit.
func (d *Dev) ColorModel() color.Model {
return image1bit.BitModel
}
// Bounds implements devices.Display. Min is guaranteed to be {0, 0}.
// Bounds implements display.Drawer. Min is guaranteed to be {0, 0}.
func (d *Dev) Bounds() image.Rectangle {
return d.rect
}
// Draw implements devices.Display.
// Draw implements display.Drawer.
//
// It draws synchronously, once this function returns, the display is updated.
// It means that on slow bus (I²C), it may be preferable to defer Draw() calls
// to a background goroutine.
//
// It discards any failure.
func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) {
func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) error {
var next []byte
if img, ok := src.(*image1bit.VerticalLSB); ok && r == d.rect && src.Bounds() == d.rect && sp.X == 0 && sp.Y == 0 {
if img, ok := src.(*image1bit.VerticalLSB); ok && r == d.rect && img.Rect == d.rect && sp.X == 0 && sp.Y == 0 {
// Exact size, full frame, image1bit encoding: fast path!
next = img.Pix
} else {
@ -200,12 +197,7 @@ func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) {
next = d.next.Pix
draw.Src.Draw(d.next, r, src, sp)
}
d.err = d.drawInternal(next)
}
// Err returns the last error that occurred
func (d *Dev) Err() error {
return d.err
return d.drawInternal(next)
}
// Write writes a buffer of pixels to the display.
@ -500,6 +492,4 @@ const (
i2cData = 0x40 // I²C transaction has stream of data bytes
)
var _ conn.Resource = &Dev{}
var _ devices.Display = &Dev{}
var _ fmt.Stringer = &Dev{}
var _ display.Drawer = &Dev{}

@ -84,8 +84,7 @@ func TestI2C_Draw_VerticalLSD_fast(t *testing.T) {
}
img := image1bit.NewVerticalLSB(dev.Bounds())
img.Pix[22] = 1
dev.Draw(dev.Bounds(), img, image.Point{})
if err := dev.Err(); err != nil {
if err := dev.Draw(dev.Bounds(), img, image.Point{}); err != nil {
t.Fatal(err)
}
if err := bus.Close(); err != nil {
@ -203,8 +202,7 @@ func TestI2C_Draw_fail(t *testing.T) {
if err != nil {
t.Fatal(err)
}
dev.Draw(dev.Bounds(), makeGrayCheckboard(dev.Bounds()), image.Point{})
if err := dev.Err(); !conntest.IsErr(err) {
if err := dev.Draw(dev.Bounds(), makeGrayCheckboard(dev.Bounds()), image.Point{}); !conntest.IsErr(err) {
t.Fatalf("expected conntest error: %v", err)
}
if err := bus.Close(); err != nil {
@ -225,13 +223,11 @@ func TestI2C_DrawGray(t *testing.T) {
if err != nil {
t.Fatal(err)
}
dev.Draw(dev.Bounds(), makeGrayCheckboard(dev.Bounds()), image.Point{0, 0})
if err := dev.Err(); err != nil {
if err := dev.Draw(dev.Bounds(), makeGrayCheckboard(dev.Bounds()), image.Point{}); err != nil {
t.Fatal(err)
}
// No-op (skip path).
dev.Draw(dev.Bounds(), makeGrayCheckboard(dev.Bounds()), image.Point{0, 0})
if err := dev.Err(); err != nil {
if err := dev.Draw(dev.Bounds(), makeGrayCheckboard(dev.Bounds()), image.Point{}); err != nil {
t.Fatal(err)
}
if err := bus.Close(); err != nil {

@ -196,8 +196,7 @@ func (s *SmokeTest) run(i2cBus i2c.Bus, spiPort spi.PortCloser, dc gpio.PinOut,
for i, d := range s.devices {
start := time.Now()
d.Draw(d.Bounds(), imgBunnyNRGBA, image.Point{})
if err := d.Err(); err != nil {
if err := d.Draw(d.Bounds(), imgBunnyNRGBA, image.Point{}); err != nil {
return err
}
s.timings[i] = time.Since(start)
@ -206,8 +205,7 @@ func (s *SmokeTest) run(i2cBus i2c.Bus, spiPort spi.PortCloser, dc gpio.PinOut,
for i, d := range s.devices {
start := time.Now()
d.Draw(d.Bounds(), imgBunny1bitLarge, image.Point{})
if err := d.Err(); err != nil {
if err := d.Draw(d.Bounds(), imgBunny1bitLarge, image.Point{}); err != nil {
return err
}
s.timings[i] = time.Since(start)
@ -216,8 +214,7 @@ func (s *SmokeTest) run(i2cBus i2c.Bus, spiPort spi.PortCloser, dc gpio.PinOut,
for i, d := range s.devices {
start := time.Now()
d.Draw(d.Bounds(), imgBunny1bit, image.Point{})
if err := d.Err(); err != nil {
if err := d.Draw(d.Bounds(), imgBunny1bit, image.Point{}); err != nil {
return err
}
s.timings[i] = time.Since(start)
@ -289,8 +286,7 @@ func (s *SmokeTest) run(i2cBus i2c.Bus, spiPort spi.PortCloser, dc gpio.PinOut,
for i, d := range s.devices {
start := time.Now()
d.Draw(d.Bounds(), imgBunny1bitLarge, image.Point{})
if err := d.Err(); err != nil {
if err := d.Draw(d.Bounds(), imgBunny1bitLarge, image.Point{}); err != nil {
return err
}
s.timings[i] = time.Since(start)
@ -406,8 +402,7 @@ func (s *SmokeTest) run(i2cBus i2c.Bus, spiPort spi.PortCloser, dc gpio.PinOut,
draw.DrawMask(bmp, r, &image.Uniform{C: image1bit.On}, image.Point{}, &periphImg, image.Point{}, draw.Over)
for i, d := range s.devices {
start := time.Now()
d.Draw(d.Bounds(), bmp, image.Point{})
if err := d.Err(); err != nil {
if err := d.Draw(d.Bounds(), bmp, image.Point{}); err != nil {
return err
}
s.timings[i] = time.Since(start)

@ -10,10 +10,9 @@ import (
"image"
"image/color"
"periph.io/x/periph/conn"
"periph.io/x/periph/conn/display"
"periph.io/x/periph/conn/gpio/gpiostream"
"periph.io/x/periph/conn/physic"
"periph.io/x/periph/devices"
)
// NRZ converts a byte into the MSB-first Non-Return-to-Zero encoded 24 bits.
@ -119,19 +118,19 @@ func (d *Dev) Halt() error {
return nil
}
// ColorModel implements devices.Display.
// ColorModel implements display.Drawer.
//
// It is color.NRGBAModel.
func (d *Dev) ColorModel() color.Model {
return color.NRGBAModel
}
// Bounds implements devices.Display. Min is guaranteed to be {0, 0}.
// Bounds implements display.Drawer. Min is guaranteed to be {0, 0}.
func (d *Dev) Bounds() image.Rectangle {
return d.rect
}
// Draw implements devices.Display.
// Draw implements display.Drawer.
//
// Using something else than image.NRGBA is 10x slower and is not recommended.
// When using image.NRGBA, the alpha channel is ignored in RGB mode and used as
@ -139,8 +138,10 @@ func (d *Dev) Bounds() image.Rectangle {
//
// A back buffer is kept so that partial updates are supported, albeit the full
// LED strip is updated synchronously.
func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) {
r = r.Intersect(d.rect)
func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) error {
if r = r.Intersect(d.rect); r.Empty() {
return nil
}
srcR := src.Bounds()
srcR.Min = srcR.Min.Add(sp)
if dX := r.Dx(); dX < srcR.Dx() {
@ -180,7 +181,7 @@ func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) {
}
}
}
_ = d.p.StreamOut(&d.b)
return d.p.StreamOut(&d.b)
}
// Write accepts a stream of raw RGB/RGBW pixels and sends it as NRZ encoded
@ -237,6 +238,4 @@ func put(out []byte, v byte) {
out[2] = byte(w)
}
var _ conn.Resource = &Dev{}
var _ devices.Display = &Dev{}
var _ fmt.Stringer = &Dev{}
var _ display.Drawer = &Dev{}

@ -92,7 +92,9 @@ func TestDraw_NRGBA_3(t *testing.T) {
d, _ := New(&g, &opts)
img := image.NewNRGBA(d.Bounds())
copy(img.Pix, getRGBW())
d.Draw(d.Bounds(), img, image.Point{})
if err := d.Draw(d.Bounds(), img, image.Point{}); err != nil {
t.Fatal(err)
}
if err := g.Close(); err != nil {
t.Fatal(err)
}
@ -120,7 +122,9 @@ func TestDraw_RGBA_3(t *testing.T) {
d, _ := New(&g, &opts)
img := image.NewRGBA(d.Bounds())
copy(img.Pix, getRGBW())
d.Draw(d.Bounds(), img, image.Point{})
if err := d.Draw(d.Bounds(), img, image.Point{}); err != nil {
t.Fatal(err)
}
if err := g.Close(); err != nil {
t.Fatal(err)
}
@ -151,7 +155,9 @@ func TestDraw_RGBA_4(t *testing.T) {
d, _ := New(&g, &opts)
img := image.NewRGBA(d.Bounds())
copy(img.Pix, getRGBW())
d.Draw(d.Bounds(), img, image.Point{})
if err := d.Draw(d.Bounds(), img, image.Point{}); err != nil {
t.Fatal(err)
}
if err := g.Close(); err != nil {
t.Fatal(err)
}
@ -179,7 +185,9 @@ func TestDraw_Limits(t *testing.T) {
d, _ := New(&g, &opts)
img := image.NewRGBA(image.Rect(-1, -1, 20, 20))
copy(img.Pix, getRGBW())
d.Draw(d.Bounds(), img, image.Point{})
if err := d.Draw(d.Bounds(), img, image.Point{}); err != nil {
t.Fatal(err)
}
if err := g.Close(); err != nil {
t.Fatal(err)
}

Loading…
Cancel
Save