From caf4bf432718191004aa6cbbb6c992dc66540f4d Mon Sep 17 00:00:00 2001 From: Marc-Antoine Ruel Date: Mon, 26 Mar 2018 10:24:37 -0400 Subject: [PATCH] apa102: switch to an Opts pattern While it may not look necessary to some drivers, it is useful to have a consistent pattern. The Opts never specify connectivity, only other parameters. Issue #194 --- devices/apa102/apa102.go | 59 +++++++++++++------ devices/apa102/apa102_test.go | 101 ++++++++++++++++++++++++++------- devices/apa102/example_test.go | 10 +++- 3 files changed, 130 insertions(+), 40 deletions(-) diff --git a/devices/apa102/apa102.go b/devices/apa102/apa102.go index e0a874d..2771721 100644 --- a/devices/apa102/apa102.go +++ b/devices/apa102/apa102.go @@ -26,18 +26,38 @@ func ToRGB(p []color.NRGBA) []byte { return b } +// DefaultOpts is the recommended default options. +var DefaultOpts = Opts{ + NumPixels: 150, // 150 LEDs is a common strip length. + Intensity: 255, // Full blinding power. + Temperature: 5000, // More pleasing white balance. +} + +// Opts defines the options for the device. +type Opts struct { + // NumPixels is the number of pixelsto control. If too short, the following + // pixels will be corrupted. If too long, the pixels will be drawn + // unnecessarily but not visible issue will occur. + NumPixels int + // Intensity is the maximum intensity level to use, on a logarithmic scale. + // This is useful to safely limit current draw. + Intensity uint8 + // Temperature declares the white color to use, specified in Kelvin. + // + // This driver assumes the LEDs are emitting a 6500K white color. + Temperature uint16 +} + // New returns a strip that communicates over SPI to APA102 LEDs. // // The SPI port speed should be high, at least in the Mhz range, as // there's 32 bits sent per LED, creating a staggered effect. See // https://cpldcpu.wordpress.com/2014/11/30/understanding-the-apa102-superled/ // -// Temperature is in Kelvin and a reasonable default value is 6500K. -// // As per APA102-C spec, the chip's max refresh rate is 400hz. // https://en.wikipedia.org/wiki/Flicker_fusion_threshold is a recommended // reading. -func New(p spi.Port, numPixels int, intensity uint8, temperature uint16) (*Dev, error) { +func New(p spi.Port, o *Opts) (*Dev, error) { c, err := p.Connect(20000000, spi.Mode3, 8) if err != nil { return nil, err @@ -45,18 +65,18 @@ func New(p spi.Port, numPixels int, intensity uint8, temperature uint16) (*Dev, // End frames are needed to be able to push enough SPI clock signals due to // internal half-delay of data signal from each individual LED. See // https://cpldcpu.wordpress.com/2014/11/30/understanding-the-apa102-superled/ - buf := make([]byte, 4*(numPixels+1)+numPixels/2/8+1) - tail := buf[4+4*numPixels:] + buf := make([]byte, 4*(o.NumPixels+1)+o.NumPixels/2/8+1) + tail := buf[4+4*o.NumPixels:] for i := range tail { tail[i] = 0xFF } return &Dev{ - Intensity: intensity, - Temperature: temperature, + Intensity: o.Intensity, + Temperature: o.Temperature, s: c, - numPixels: numPixels, + numPixels: o.NumPixels, rawBuf: buf, - pixels: buf[4 : 4+4*numPixels], + pixels: buf[4 : 4+4*o.NumPixels], }, nil } @@ -66,13 +86,20 @@ func New(p spi.Port, numPixels int, intensity uint8, temperature uint16) (*Dev, // // Includes intensity and temperature correction. type Dev struct { - Intensity uint8 // Set an intensity between 0 (off) and 255 (full brightness). - Temperature uint16 // In Kelvin. - s spi.Conn // - l lut // Updated at each .Write() call. - numPixels int // - rawBuf []byte // Raw buffer sent over SPI. Cached to reduce heap fragmentation. - pixels []byte // Double buffer of pixels, to enable partial painting via Draw(). Effectively points inside rawBuf. + // Intensity set the intensity between 0 (off) and 255 (full brightness). + // + // It can be changed, it will take effect on the next Draw() or Write() call. + Intensity uint8 + // Temperature is the white adjustment in °Kelvin. + // + // It can be changed, it will take effect on the next Draw() or Write() call. + Temperature uint16 + + s spi.Conn // + l lut // Updated at each .Write() call. + numPixels int // + rawBuf []byte // Raw buffer sent over SPI. Cached to reduce heap fragmentation. + pixels []byte // Double buffer of pixels, to enable partial painting via Draw(). Effectively points inside rawBuf. } func (d *Dev) String() string { diff --git a/devices/apa102/apa102_test.go b/devices/apa102/apa102_test.go index 99031d1..19cdd8b 100644 --- a/devices/apa102/apa102_test.go +++ b/devices/apa102/apa102_test.go @@ -326,27 +326,31 @@ func TestRampMonotonic(t *testing.T) { func TestDevEmpty(t *testing.T) { buf := bytes.Buffer{} - d, _ := New(spitest.NewRecordRaw(&buf), 0, 255, 6500) + o := DefaultOpts + o.NumPixels = 0 + d, _ := New(spitest.NewRecordRaw(&buf), &o) if n, err := d.Write([]byte{}); n != 0 || err != nil { t.Fatalf("%d %v", n, err) } if expected := []byte{0x0, 0x0, 0x0, 0x0, 0xFF}; !bytes.Equal(expected, buf.Bytes()) { t.Fatalf("%#v != %#v", expected, buf.Bytes()) } - if s := d.String(); s != "APA102{I:255, T:6500K, 0LEDs, recordraw}" { + if s := d.String(); s != "APA102{I:255, T:5000K, 0LEDs, recordraw}" { t.Fatal(s) } } func TestConnectFail(t *testing.T) { - if d, err := New(&configFail{}, 150, 255, 6500); d != nil || err == nil { + if d, err := New(&configFail{}, &DefaultOpts); d != nil || err == nil { t.Fatal("Connect() call have failed") } } func TestDevLen(t *testing.T) { buf := bytes.Buffer{} - d, _ := New(spitest.NewRecordRaw(&buf), 1, 255, 6500) + o := DefaultOpts + o.NumPixels = 1 + d, _ := New(spitest.NewRecordRaw(&buf), &o) if n, err := d.Write([]byte{0}); n != 0 || err == nil { t.Fatalf("%d %v", n, err) } @@ -369,7 +373,10 @@ func TestDev(t *testing.T) { {0x00, 0x00, 0x01, 0x00}, {0x00, 0x00, 0x00, 0x00}, } - d, _ := New(spitest.NewRecordRaw(&buf), len(colors), 255, 6500) + o := DefaultOpts + o.NumPixels = len(colors) + o.Temperature = 6500 + d, _ := New(spitest.NewRecordRaw(&buf), &o) if n, err := d.Write(ToRGB(colors)); n != len(colors)*3 || err != nil { t.Fatalf("%d %v", n, err) } @@ -412,7 +419,11 @@ func TestDevIntensity(t *testing.T) { {0x00, 0x00, 0x01, 0x00}, {0x00, 0x00, 0x00, 0x00}, } - d, _ := New(spitest.NewRecordRaw(&buf), len(colors), 127, 6500) + o := DefaultOpts + o.Intensity = 127 + o.NumPixels = len(colors) + o.Temperature = 6500 + d, _ := New(spitest.NewRecordRaw(&buf), &o) if n, err := d.Write(ToRGB(colors)); n != len(colors)*3 || err != nil { t.Fatalf("%d %v", n, err) } @@ -438,7 +449,9 @@ func TestDevIntensity(t *testing.T) { func TestDevLong(t *testing.T) { buf := bytes.Buffer{} colors := make([]color.NRGBA, 256) - d, _ := New(spitest.NewRecordRaw(&buf), len(colors), 255, 6500) + o := DefaultOpts + o.NumPixels = len(colors) + d, _ := New(spitest.NewRecordRaw(&buf), &o) if n, err := d.Write(ToRGB(colors)); n != len(colors)*3 || err != nil { t.Fatalf("%d %v", n, err) } @@ -457,7 +470,9 @@ func TestDevLong(t *testing.T) { func TestDevWrite_Long(t *testing.T) { buf := bytes.Buffer{} - d, _ := New(spitest.NewRecordRaw(&buf), 1, 250, 6500) + o := DefaultOpts + o.NumPixels = 1 + d, _ := New(spitest.NewRecordRaw(&buf), &o) if n, err := d.Write([]byte{0, 0, 0, 1, 1, 1}); n != 0 || err == nil { t.Fatal(n, err) } @@ -492,7 +507,11 @@ func TestDevTemperatureWarm(t *testing.T) { // Test all intensity code paths. pixels[i] = uint8(i << 2) } - d, _ := New(spitest.NewRecordRaw(&buf), len(pixels)/3, 250, 5000) + o := DefaultOpts + o.NumPixels = len(pixels) / 3 + o.Intensity = 250 + o.Temperature = 5000 + d, _ := New(spitest.NewRecordRaw(&buf), &o) if n, err := d.Write(pixels[:]); n != len(pixels) || err != nil { t.Fatalf("%d %v", n, err) } @@ -511,7 +530,11 @@ func TestDrawNRGBA(t *testing.T) { img.Pix[4*i+3] = 0 } buf := bytes.Buffer{} - d, _ := New(spitest.NewRecordRaw(&buf), 16, 250, 5000) + o := DefaultOpts + o.NumPixels = 16 + o.Intensity = 250 + o.Temperature = 5000 + d, _ := New(spitest.NewRecordRaw(&buf), &o) d.Draw(d.Bounds(), img, image.Point{}) if !bytes.Equal(expectedi250t5000, buf.Bytes()) { t.Fatalf("%#v != %#v", expectedi250t5000, buf.Bytes()) @@ -525,7 +548,11 @@ func TestDrawNRGBA_wide(t *testing.T) { img.SetNRGBA(x, 0, color.NRGBA{uint8((3 * x) << 2), uint8((3*x + 1) << 2), uint8((3*x + 2) << 2), 0}) } buf := bytes.Buffer{} - d, _ := New(spitest.NewRecordRaw(&buf), 16, 250, 6500) + o := DefaultOpts + o.NumPixels = 16 + o.Intensity = 250 + o.Temperature = 6500 + d, _ := New(spitest.NewRecordRaw(&buf), &o) d.Draw(d.Bounds(), img, image.Point{}) if !bytes.Equal(expectedi250t6500, buf.Bytes()) { t.Fatalf("%#v != %#v", expectedi250t5000, buf.Bytes()) @@ -542,7 +569,11 @@ func TestDrawRGBA(t *testing.T) { img.Pix[4*i+3] = 0xFF } buf := bytes.Buffer{} - d, _ := New(spitest.NewRecordRaw(&buf), 16, 250, 5000) + o := DefaultOpts + o.NumPixels = 16 + o.Intensity = 250 + o.Temperature = 5000 + d, _ := New(spitest.NewRecordRaw(&buf), &o) d.Draw(d.Bounds(), img, image.Point{}) if !bytes.Equal(expectedi250t5000, buf.Bytes()) { t.Fatalf("%#v != %#v", expectedi250t5000, buf.Bytes()) @@ -557,7 +588,10 @@ func TestHalt(t *testing.T) { }, }, } - d, _ := New(&s, 4, 250, 5000) + o := DefaultOpts + o.NumPixels = 4 + o.Temperature = 5000 + d, _ := New(&s, &o) if err := d.Halt(); err != nil { t.Fatal(err) } @@ -583,7 +617,10 @@ func BenchmarkWriteWhite(b *testing.B) { for i := range pixels { pixels[i] = 0xFF } - d, _ := New(spitest.NewRecordRaw(ioutil.Discard), len(pixels)/3, 255, 6500) + o := DefaultOpts + o.NumPixels = len(pixels) / 3 + o.Intensity = 250 + d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &o) _, _ = d.Write(pixels[:]) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -597,7 +634,10 @@ func BenchmarkWriteDim(b *testing.B) { for i := range pixels { pixels[i] = 1 } - d, _ := New(spitest.NewRecordRaw(ioutil.Discard), len(pixels)/3, 255, 6500) + o := DefaultOpts + o.NumPixels = len(pixels) / 3 + o.Intensity = 250 + d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &o) _, _ = d.Write(pixels[:]) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -608,7 +648,10 @@ func BenchmarkWriteDim(b *testing.B) { func BenchmarkWriteBlack(b *testing.B) { b.ReportAllocs() pixels := [150 * 3]byte{} - d, _ := New(spitest.NewRecordRaw(ioutil.Discard), len(pixels)/3, 255, 6500) + o := DefaultOpts + o.NumPixels = len(pixels) / 3 + o.Intensity = 250 + d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &o) _, _ = d.Write(pixels[:]) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -622,7 +665,11 @@ func BenchmarkWriteColorful(b *testing.B) { for i := range pixels { pixels[i] = uint8(i) + uint8(i>>8) } - d, _ := New(spitest.NewRecordRaw(ioutil.Discard), len(pixels)/3, 250, 5000) + o := DefaultOpts + o.NumPixels = len(pixels) / 3 + o.Intensity = 250 + o.Temperature = 5000 + d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &o) _, _ = d.Write(pixels[:]) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -637,7 +684,11 @@ func BenchmarkDrawNRGBAColorful(b *testing.B) { for i := range img.Pix { img.Pix[i] = uint8(i) + uint8(i>>8) } - d, _ := New(spitest.NewRecordRaw(ioutil.Discard), img.Bounds().Max.X, 250, 5000) + o := DefaultOpts + o.NumPixels = img.Bounds().Max.X + o.Intensity = 250 + o.Temperature = 5000 + d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &o) r := d.Bounds() p := image.Point{} d.Draw(r, img, p) @@ -654,7 +705,11 @@ func BenchmarkDrawRGBAColorful(b *testing.B) { for i := range img.Pix { img.Pix[i] = uint8(i) + uint8(i>>8) } - d, _ := New(spitest.NewRecordRaw(ioutil.Discard), img.Bounds().Max.X, 250, 5000) + o := DefaultOpts + o.NumPixels = img.Bounds().Max.X + o.Intensity = 250 + o.Temperature = 5000 + d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &o) r := d.Bounds() p := image.Point{} d.Draw(r, img, p) @@ -671,7 +726,11 @@ func BenchmarkWriteColorfulVariation(b *testing.B) { for i := range pixels { pixels[i] = uint8(i) + uint8(i>>8) } - d, _ := New(spitest.NewRecordRaw(ioutil.Discard), len(pixels)/3, 250, 5000) + o := DefaultOpts + o.NumPixels = len(pixels) / 3 + o.Intensity = 250 + o.Temperature = 5000 + d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &o) _, _ = d.Write(pixels[:]) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -683,7 +742,7 @@ func BenchmarkWriteColorfulVariation(b *testing.B) { func BenchmarkHalt(b *testing.B) { b.ReportAllocs() - d, _ := New(spitest.NewRecordRaw(ioutil.Discard), 150, 250, 5000) + d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &DefaultOpts) b.ResetTimer() for i := 0; i < b.N; i++ { d.Halt() diff --git a/devices/apa102/example_test.go b/devices/apa102/example_test.go index 02240f6..edbb5c4 100644 --- a/devices/apa102/example_test.go +++ b/devices/apa102/example_test.go @@ -27,9 +27,13 @@ func Example() { } defer p.Close() - // Opens a strip of 150 lights are 50% intensity with color temperature at - // 5000 Kelvin. - dev, err := apa102.New(p, 150, 127, 5000) + // Opens a 300 lights strip at 50% intensity with color temperature at + // 3500°Kelvin. + o := apa102.DefaultOpts + o.NumPixels = 300 + o.Intensity = 127 + o.Temperature = 3500 + dev, err := apa102.New(p, &o) if err != nil { log.Fatalf("failed to open apa102: %v", err) }