diff --git a/devices/apa102/apa102.go b/devices/apa102/apa102.go index 42c8bfd..b63b59f 100644 --- a/devices/apa102/apa102.go +++ b/devices/apa102/apa102.go @@ -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) diff --git a/devices/apa102/apa102_test.go b/devices/apa102/apa102_test.go index 85a65af..1f6f8be 100644 --- a/devices/apa102/apa102_test.go +++ b/devices/apa102/apa102_test.go @@ -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))