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
pull/1/head
Marc-Antoine Ruel 8 years ago
parent 46ea7926cc
commit caf4bf4327

@ -26,18 +26,38 @@ func ToRGB(p []color.NRGBA) []byte {
return b 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. // 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 // 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 // there's 32 bits sent per LED, creating a staggered effect. See
// https://cpldcpu.wordpress.com/2014/11/30/understanding-the-apa102-superled/ // 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. // As per APA102-C spec, the chip's max refresh rate is 400hz.
// https://en.wikipedia.org/wiki/Flicker_fusion_threshold is a recommended // https://en.wikipedia.org/wiki/Flicker_fusion_threshold is a recommended
// reading. // 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) c, err := p.Connect(20000000, spi.Mode3, 8)
if err != nil { if err != nil {
return nil, err 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 // 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 // internal half-delay of data signal from each individual LED. See
// https://cpldcpu.wordpress.com/2014/11/30/understanding-the-apa102-superled/ // https://cpldcpu.wordpress.com/2014/11/30/understanding-the-apa102-superled/
buf := make([]byte, 4*(numPixels+1)+numPixels/2/8+1) buf := make([]byte, 4*(o.NumPixels+1)+o.NumPixels/2/8+1)
tail := buf[4+4*numPixels:] tail := buf[4+4*o.NumPixels:]
for i := range tail { for i := range tail {
tail[i] = 0xFF tail[i] = 0xFF
} }
return &Dev{ return &Dev{
Intensity: intensity, Intensity: o.Intensity,
Temperature: temperature, Temperature: o.Temperature,
s: c, s: c,
numPixels: numPixels, numPixels: o.NumPixels,
rawBuf: buf, rawBuf: buf,
pixels: buf[4 : 4+4*numPixels], pixels: buf[4 : 4+4*o.NumPixels],
}, nil }, nil
} }
@ -66,13 +86,20 @@ func New(p spi.Port, numPixels int, intensity uint8, temperature uint16) (*Dev,
// //
// Includes intensity and temperature correction. // Includes intensity and temperature correction.
type Dev struct { type Dev struct {
Intensity uint8 // Set an intensity between 0 (off) and 255 (full brightness). // Intensity set the intensity between 0 (off) and 255 (full brightness).
Temperature uint16 // In Kelvin. //
s spi.Conn // // It can be changed, it will take effect on the next Draw() or Write() call.
l lut // Updated at each .Write() call. Intensity uint8
numPixels int // // Temperature is the white adjustment in °Kelvin.
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. // 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 { func (d *Dev) String() string {

@ -326,27 +326,31 @@ func TestRampMonotonic(t *testing.T) {
func TestDevEmpty(t *testing.T) { func TestDevEmpty(t *testing.T) {
buf := bytes.Buffer{} 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 { if n, err := d.Write([]byte{}); n != 0 || err != nil {
t.Fatalf("%d %v", n, err) t.Fatalf("%d %v", n, err)
} }
if expected := []byte{0x0, 0x0, 0x0, 0x0, 0xFF}; !bytes.Equal(expected, buf.Bytes()) { if expected := []byte{0x0, 0x0, 0x0, 0x0, 0xFF}; !bytes.Equal(expected, buf.Bytes()) {
t.Fatalf("%#v != %#v", 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) t.Fatal(s)
} }
} }
func TestConnectFail(t *testing.T) { 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") t.Fatal("Connect() call have failed")
} }
} }
func TestDevLen(t *testing.T) { func TestDevLen(t *testing.T) {
buf := bytes.Buffer{} 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 { if n, err := d.Write([]byte{0}); n != 0 || err == nil {
t.Fatalf("%d %v", n, err) t.Fatalf("%d %v", n, err)
} }
@ -369,7 +373,10 @@ func TestDev(t *testing.T) {
{0x00, 0x00, 0x01, 0x00}, {0x00, 0x00, 0x01, 0x00},
{0x00, 0x00, 0x00, 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 { if n, err := d.Write(ToRGB(colors)); n != len(colors)*3 || err != nil {
t.Fatalf("%d %v", n, err) t.Fatalf("%d %v", n, err)
} }
@ -412,7 +419,11 @@ func TestDevIntensity(t *testing.T) {
{0x00, 0x00, 0x01, 0x00}, {0x00, 0x00, 0x01, 0x00},
{0x00, 0x00, 0x00, 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 { if n, err := d.Write(ToRGB(colors)); n != len(colors)*3 || err != nil {
t.Fatalf("%d %v", n, err) t.Fatalf("%d %v", n, err)
} }
@ -438,7 +449,9 @@ func TestDevIntensity(t *testing.T) {
func TestDevLong(t *testing.T) { func TestDevLong(t *testing.T) {
buf := bytes.Buffer{} buf := bytes.Buffer{}
colors := make([]color.NRGBA, 256) 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 { if n, err := d.Write(ToRGB(colors)); n != len(colors)*3 || err != nil {
t.Fatalf("%d %v", n, err) t.Fatalf("%d %v", n, err)
} }
@ -457,7 +470,9 @@ func TestDevLong(t *testing.T) {
func TestDevWrite_Long(t *testing.T) { func TestDevWrite_Long(t *testing.T) {
buf := bytes.Buffer{} 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 { if n, err := d.Write([]byte{0, 0, 0, 1, 1, 1}); n != 0 || err == nil {
t.Fatal(n, err) t.Fatal(n, err)
} }
@ -492,7 +507,11 @@ func TestDevTemperatureWarm(t *testing.T) {
// Test all intensity code paths. // Test all intensity code paths.
pixels[i] = uint8(i << 2) 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 { if n, err := d.Write(pixels[:]); n != len(pixels) || err != nil {
t.Fatalf("%d %v", n, err) t.Fatalf("%d %v", n, err)
} }
@ -511,7 +530,11 @@ func TestDrawNRGBA(t *testing.T) {
img.Pix[4*i+3] = 0 img.Pix[4*i+3] = 0
} }
buf := bytes.Buffer{} 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{}) d.Draw(d.Bounds(), img, image.Point{})
if !bytes.Equal(expectedi250t5000, buf.Bytes()) { if !bytes.Equal(expectedi250t5000, buf.Bytes()) {
t.Fatalf("%#v != %#v", 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}) img.SetNRGBA(x, 0, color.NRGBA{uint8((3 * x) << 2), uint8((3*x + 1) << 2), uint8((3*x + 2) << 2), 0})
} }
buf := bytes.Buffer{} 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{}) d.Draw(d.Bounds(), img, image.Point{})
if !bytes.Equal(expectedi250t6500, buf.Bytes()) { if !bytes.Equal(expectedi250t6500, buf.Bytes()) {
t.Fatalf("%#v != %#v", expectedi250t5000, buf.Bytes()) t.Fatalf("%#v != %#v", expectedi250t5000, buf.Bytes())
@ -542,7 +569,11 @@ func TestDrawRGBA(t *testing.T) {
img.Pix[4*i+3] = 0xFF img.Pix[4*i+3] = 0xFF
} }
buf := bytes.Buffer{} 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{}) d.Draw(d.Bounds(), img, image.Point{})
if !bytes.Equal(expectedi250t5000, buf.Bytes()) { if !bytes.Equal(expectedi250t5000, buf.Bytes()) {
t.Fatalf("%#v != %#v", 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 { if err := d.Halt(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -583,7 +617,10 @@ func BenchmarkWriteWhite(b *testing.B) {
for i := range pixels { for i := range pixels {
pixels[i] = 0xFF 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[:]) _, _ = d.Write(pixels[:])
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -597,7 +634,10 @@ func BenchmarkWriteDim(b *testing.B) {
for i := range pixels { for i := range pixels {
pixels[i] = 1 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[:]) _, _ = d.Write(pixels[:])
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -608,7 +648,10 @@ func BenchmarkWriteDim(b *testing.B) {
func BenchmarkWriteBlack(b *testing.B) { func BenchmarkWriteBlack(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
pixels := [150 * 3]byte{} 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[:]) _, _ = d.Write(pixels[:])
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -622,7 +665,11 @@ func BenchmarkWriteColorful(b *testing.B) {
for i := range pixels { for i := range pixels {
pixels[i] = uint8(i) + uint8(i>>8) 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[:]) _, _ = d.Write(pixels[:])
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -637,7 +684,11 @@ func BenchmarkDrawNRGBAColorful(b *testing.B) {
for i := range img.Pix { for i := range img.Pix {
img.Pix[i] = uint8(i) + uint8(i>>8) 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() r := d.Bounds()
p := image.Point{} p := image.Point{}
d.Draw(r, img, p) d.Draw(r, img, p)
@ -654,7 +705,11 @@ func BenchmarkDrawRGBAColorful(b *testing.B) {
for i := range img.Pix { for i := range img.Pix {
img.Pix[i] = uint8(i) + uint8(i>>8) 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() r := d.Bounds()
p := image.Point{} p := image.Point{}
d.Draw(r, img, p) d.Draw(r, img, p)
@ -671,7 +726,11 @@ func BenchmarkWriteColorfulVariation(b *testing.B) {
for i := range pixels { for i := range pixels {
pixels[i] = uint8(i) + uint8(i>>8) 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[:]) _, _ = d.Write(pixels[:])
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -683,7 +742,7 @@ func BenchmarkWriteColorfulVariation(b *testing.B) {
func BenchmarkHalt(b *testing.B) { func BenchmarkHalt(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), 150, 250, 5000) d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &DefaultOpts)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
d.Halt() d.Halt()

@ -27,9 +27,13 @@ func Example() {
} }
defer p.Close() defer p.Close()
// Opens a strip of 150 lights are 50% intensity with color temperature at // Opens a 300 lights strip at 50% intensity with color temperature at
// 5000 Kelvin. // 3500°Kelvin.
dev, err := apa102.New(p, 150, 127, 5000) o := apa102.DefaultOpts
o.NumPixels = 300
o.Intensity = 127
o.Temperature = 3500
dev, err := apa102.New(p, &o)
if err != nil { if err != nil {
log.Fatalf("failed to open apa102: %v", err) log.Fatalf("failed to open apa102: %v", err)
} }

Loading…
Cancel
Save