apa102: Add RawColors option (#260)

This adds a new option to bypass the perceptual remapping code and
directly write the RGB values to the APA102.
pull/1/head
Ben Lazarus 8 years ago committed by M-A
parent 70adef3f63
commit bbc377d76a

@ -29,9 +29,10 @@ func ToRGB(p []color.NRGBA) []byte {
// 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.
NumPixels: 150, // 150 LEDs is a common strip length.
Intensity: 255, // Full blinding power.
Temperature: 5000, // More pleasing white balance.
RawColors: false, // Do perceptual color mapping.
}
// Opts defines the options for the device.
@ -43,10 +44,14 @@ type Opts struct {
// 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.
// Temperature declares the white color to use, specified in Kelvin. Has no
// effect when RawColors is true.
//
// This driver assumes the LEDs are emitting a 6500K white color.
Temperature uint16
// Skip color mapping and directly write RGB values as received. Temperature
// has no effect when this is set to true.
RawColors bool
}
// New returns a strip that communicates over SPI to APA102 LEDs.
@ -74,6 +79,7 @@ func New(p spi.Port, o *Opts) (*Dev, error) {
return &Dev{
Intensity: o.Intensity,
Temperature: o.Temperature,
RawColors: o.RawColors,
s: c,
numPixels: o.NumPixels,
rawBuf: buf,
@ -92,10 +98,16 @@ type Dev struct {
//
// It can be changed, it will take effect on the next Draw() or Write() call.
Intensity uint8
// Temperature is the white adjustment in °Kelvin.
// Temperature is the white adjustment in °Kelvin. Has no effect when
// RawColors is true.
//
// It can be changed, it will take effect on the next Draw() or Write() call.
Temperature uint16
// Whether to write raw RGB as received or do perceptual remapping.
//
// It can be changed, it will take effect on the next Draw() or Write() call.
// When true, Temperature has no effect.
RawColors bool
s spi.Conn //
l lut // Updated at each .Write() call.
@ -106,7 +118,7 @@ type Dev struct {
}
func (d *Dev) String() string {
return fmt.Sprintf("APA102{I:%d, T:%dK, %dLEDs, %s}", d.Intensity, d.Temperature, d.numPixels, d.s)
return fmt.Sprintf("APA102{I:%d, T:%dK, R:%t, %dLEDs, %s}", d.Intensity, d.Temperature, d.RawColors, d.numPixels, d.s)
}
// ColorModel implements display.Drawer. There's no surprise, it is
@ -190,6 +202,8 @@ func (d *Dev) raster(dst []byte, src []byte, hasAlpha bool) {
length = len(dst) / pBytes
}
d.l.init(d.Intensity, d.Temperature)
// For the d.RawColors == true case, allow for fast brightness scaling
brightness := int(d.Intensity) + 1
for i := 0; i < length; i++ {
// The response as seen by the human eye is very non-linear. The APA-102
// provides an overall brightness PWM but it is relatively slower and
@ -213,17 +227,26 @@ func (d *Dev) raster(dst []byte, src []byte, hasAlpha bool) {
// Computes brightness, blue, green, red.
sOff := pBytes * i
dOff := 4 * i
r, g, b := d.l.r[src[sOff]], d.l.g[src[sOff+1]], d.l.b[src[sOff+2]]
m := r | g | b
switch {
case m <= 255:
dst[dOff], dst[dOff+1], dst[dOff+2], dst[dOff+3] = 0xE1, byte(b), byte(g), byte(r)
case m <= 511:
dst[dOff], dst[dOff+1], dst[dOff+2], dst[dOff+3] = 0xE2, byte(b/2), byte(g/2), byte(r/2)
case m <= 1023:
dst[dOff], dst[dOff+1], dst[dOff+2], dst[dOff+3] = 0xE4, byte((b+2)/4), byte((g+2)/4), byte((r+2)/4)
default:
dst[dOff], dst[dOff+1], dst[dOff+2], dst[dOff+3] = 0xFF, byte((b+15)/31), byte((g+15)/31), byte((r+15)/31)
if d.RawColors {
r, g, b := src[sOff], src[sOff+1], src[sOff+2]
// Fast brightness scaling
r = uint8((uint16(r) * uint16(brightness)) >> 8)
g = uint8((uint16(g) * uint16(brightness)) >> 8)
b = uint8((uint16(b) * uint16(brightness)) >> 8)
dst[dOff], dst[dOff+1], dst[dOff+2], dst[dOff+3] = 0xFF, byte(b), byte(g), byte(r)
} else {
r, g, b := d.l.r[src[sOff]], d.l.g[src[sOff+1]], d.l.b[src[sOff+2]]
m := r | g | b
switch {
case m <= 255:
dst[dOff], dst[dOff+1], dst[dOff+2], dst[dOff+3] = 0xE1, byte(b), byte(g), byte(r)
case m <= 511:
dst[dOff], dst[dOff+1], dst[dOff+2], dst[dOff+3] = 0xE2, byte(b/2), byte(g/2), byte(r/2)
case m <= 1023:
dst[dOff], dst[dOff+1], dst[dOff+2], dst[dOff+3] = 0xE4, byte((b+2)/4), byte((g+2)/4), byte((r+2)/4)
default:
dst[dOff], dst[dOff+1], dst[dOff+2], dst[dOff+3] = 0xFF, byte((b+15)/31), byte((g+15)/31), byte((r+15)/31)
}
}
}
}
@ -320,6 +343,7 @@ func (l *lut) init(i uint8, t uint16) {
l.intensity = i
l.temperature = t
tr, tg, tb := toRGBFast(l.temperature)
// maxR, maxG and maxB are the maximum light intensity to use per channel.
maxR := uint16((uint32(maxOut)*uint32(l.intensity)*uint32(tr) + 127*127) / 65025)
maxG := uint16((uint32(maxOut)*uint32(l.intensity)*uint32(tg) + 127*127) / 65025)
maxB := uint16((uint32(maxOut)*uint32(l.intensity)*uint32(tb) + 127*127) / 65025)

@ -337,7 +337,7 @@ func TestDevEmpty(t *testing.T) {
if expected := []byte{0x0, 0x0, 0x0, 0x0, 0xFF}; !bytes.Equal(expected, buf.Bytes()) {
t.Fatalf("\ngot: %#v\nwant: %#v\n", buf.Bytes(), expected)
}
if s := d.String(); s != "APA102{I:255, T:5000K, 0LEDs, recordraw}" {
if s := d.String(); s != "APA102{I:255, T:5000K, R:false, 0LEDs, recordraw}" {
t.Fatal(s)
}
}
@ -433,6 +433,39 @@ var writeTests = []struct {
Temperature: 6500,
},
},
{
name: "Raw",
pixels: ToRGB([]color.NRGBA{
{0xFF, 0xFF, 0xFF, 0x00},
{0xFE, 0xFE, 0xFE, 0x00},
{0xF0, 0xF0, 0xF0, 0x00},
{0x80, 0x80, 0x80, 0x00},
{0x80, 0x00, 0x00, 0x00},
{0x00, 0x80, 0x00, 0x00},
{0x00, 0x00, 0x80, 0x00},
{0x00, 0x00, 0x10, 0x00},
{0x00, 0x00, 0x01, 0x00},
{0x00, 0x00, 0x00, 0x00},
}),
want: []byte{
0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFE, 0xFE, 0xFE,
0xFF, 0xF0, 0xF0, 0xF0,
0xFF, 0x80, 0x80, 0x80,
0xFF, 0x00, 0x00, 0x80,
0xFF, 0x00, 0x80, 0x00,
0xFF, 0x80, 0x00, 0x00,
0xFF, 0x10, 0x00, 0x00,
0xFF, 0x01, 0x00, 0x00,
0xFF, 0x00, 0x00, 0x00,
0xFF,
},
opts: Opts{
Intensity: 255,
RawColors: true,
},
},
{
name: "Intensity and temperature",
pixels: func() []byte {
@ -528,6 +561,16 @@ var expectedi250t6500 = []byte{
0x3E, 0x38, 0x32, 0xFF, 0xFF,
}
// expectedi250raw is using RawColors = true.
var expectedi250raw = []byte{
0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x03, 0x00, 0xFF, 0x13, 0x0F, 0x0B, 0xFF,
0x1F, 0x1B, 0x17, 0xFF, 0x2B, 0x27, 0x23, 0xFF, 0x36, 0x32, 0x2F, 0xFF, 0x42,
0x3E, 0x3A, 0xFF, 0x4E, 0x4A, 0x46, 0xFF, 0x5A, 0x56, 0x52, 0xFF, 0x65, 0x62,
0x5E, 0xFF, 0x71, 0x6D, 0x69, 0xFF, 0x7D, 0x79, 0x75, 0xFF, 0x89, 0x85, 0x81,
0xFF, 0x95, 0x91, 0x8D, 0xFF, 0xA0, 0x9C, 0x98, 0xFF, 0xAC, 0xA8, 0xA4, 0xFF,
0xB8, 0xB4, 0xB0, 0xFF, 0xFF,
}
var drawTests = []struct {
name string
img image.Image
@ -571,6 +614,26 @@ var drawTests = []struct {
Temperature: 6500,
},
},
{
name: "Draw NRGBA Raw",
img: func() image.Image {
im := image.NewNRGBA(image.Rect(0, 0, 16, 1))
for i := 0; i < 16; i++ {
// Test all intensity code paths. Confirm that alpha is ignored.
im.Pix[4*i] = uint8((3 * i) << 2)
im.Pix[4*i+1] = uint8((3*i + 1) << 2)
im.Pix[4*i+2] = uint8((3*i + 2) << 2)
im.Pix[4*i+3] = 0
}
return im
}(),
want: expectedi250raw,
opts: Opts{
NumPixels: 16,
Intensity: 250,
RawColors: true,
},
},
{
name: "Draw RGBA",
img: func() image.Image {
@ -612,6 +675,25 @@ var drawTests = []struct {
Temperature: 5000,
},
},
{
name: "Draw RGBA Raw",
img: func() image.Image {
im := image.NewRGBA(image.Rect(0, 0, 16, 1))
for i := 0; i < 16; i++ {
im.Pix[4*i] = uint8((3 * i) << 2)
im.Pix[4*i+1] = uint8((3*i + 1) << 2)
im.Pix[4*i+2] = uint8((3*i + 2) << 2)
im.Pix[4*i+3] = 0xFF
}
return im
}(),
want: expectedi250raw,
opts: Opts{
NumPixels: 16,
Intensity: 250,
RawColors: true,
},
},
}
func TestDraws(t *testing.T) {
@ -814,6 +896,13 @@ func BenchmarkWriteColorful(b *testing.B) {
benchmarkWrite(b, o, 150, genColorfulPixel)
}
func BenchmarkWriteColorfulRaw(b *testing.B) {
o := DefaultOpts
o.Intensity = 250
o.RawColors = true
benchmarkWrite(b, o, 150, genColorfulPixel)
}
func BenchmarkWriteColorfulVariation(b *testing.B) {
// Continuously vary the lookup tables.
b.ReportAllocs()
@ -866,6 +955,13 @@ func BenchmarkDrawNRGBAColorful(b *testing.B) {
benchmarkDraw(b, o, image.NewNRGBA(image.Rect(0, 0, 150, 1)), genColorfulPixel)
}
func BenchmarkDrawNRGBAColorfulRaw(b *testing.B) {
o := DefaultOpts
o.Intensity = 250
o.RawColors = true
benchmarkDraw(b, o, image.NewNRGBA(image.Rect(0, 0, 150, 1)), genColorfulPixel)
}
func BenchmarkDrawNRGBAWhite(b *testing.B) {
o := DefaultOpts
o.Intensity = 250
@ -880,6 +976,13 @@ func BenchmarkDrawRGBAColorful(b *testing.B) {
benchmarkDraw(b, o, image.NewRGBA(image.Rect(0, 0, 256, 1)), genColorfulPixel)
}
func BenchmarkDrawRGBAColorfulRaw(b *testing.B) {
o := DefaultOpts
o.Intensity = 250
o.RawColors = true
benchmarkDraw(b, o, image.NewRGBA(image.Rect(0, 0, 256, 1)), genColorfulPixel)
}
func BenchmarkDrawSlowpath(b *testing.B) {
// Should be an image type that doesn't have a fast path
img := image.NewCMYK(image.Rect(0, 0, 150, 1))

Loading…
Cancel
Save