You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
devices/apa102/apa102_test.go

1111 lines
25 KiB
Go

// 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 apa102
import (
"bytes"
"errors"
"fmt"
"image"
"image/color"
"image/draw"
"io/ioutil"
"testing"
"periph.io/x/conn/v3/conntest"
"periph.io/x/conn/v3/physic"
"periph.io/x/conn/v3/spi"
"periph.io/x/conn/v3/spi/spitest"
)
func TestRamp(t *testing.T) {
// Tests a few known values.
data := []struct {
input uint8
expected uint16
}{
{0x00, 0x0000},
{0x01, 0x0001},
{0x02, 0x0002},
{0x03, 0x0003},
{0x04, 0x0004},
{0x05, 0x0005},
{0x06, 0x0006},
{0x07, 0x0007},
{0x08, 0x0008},
{0x09, 0x0009},
{0x0A, 0x000A},
{0x0B, 0x000B},
{0x0C, 0x000C},
{0x0D, 0x000D},
{0x0E, 0x000E},
{0x0F, 0x000F},
{0x10, 0x0010},
{0x11, 0x0011},
{0x12, 0x0012},
{0x13, 0x0013},
{0x14, 0x0014},
{0x15, 0x0015},
{0x16, 0x0016},
{0x17, 0x0017},
{0x18, 0x0018},
{0x19, 0x0019},
{0x1A, 0x001A},
{0x1B, 0x001B},
{0x1C, 0x001C},
{0x1D, 0x001D},
{0x1E, 0x001E},
{0x1F, 0x001F},
{0x20, 0x0020},
{0x21, 0x0021},
{0x22, 0x0022},
{0x23, 0x0023},
{0x24, 0x0024},
{0x25, 0x0025},
{0x26, 0x0026},
{0x27, 0x0027},
{0x28, 0x0028},
{0x29, 0x0029},
{0x2A, 0x002A},
{0x2B, 0x002B},
{0x2C, 0x002C},
{0x2D, 0x002D},
{0x2E, 0x002E},
{0x2F, 0x002F},
{0x30, 0x0030},
{0x31, 0x0031},
{0x32, 0x0032},
{0x33, 0x0033},
{0x34, 0x0034},
{0x35, 0x0035},
{0x36, 0x0036},
{0x37, 0x0037},
{0x38, 0x0038},
{0x39, 0x0039},
{0x3A, 0x003A},
{0x3B, 0x003B},
{0x3C, 0x003C},
{0x3D, 0x003D},
{0x3E, 0x003E},
{0x3F, 0x003F},
{0x40, 0x0040},
{0x41, 0x0041},
{0x42, 0x0042},
{0x43, 0x0043},
{0x44, 0x0044},
{0x45, 0x0045},
{0x46, 0x0046},
{0x47, 0x0047},
{0x48, 0x0048},
{0x49, 0x0049},
{0x4A, 0x004A},
{0x4B, 0x004B},
{0x4C, 0x004C},
{0x4D, 0x004D},
{0x4E, 0x004E},
{0x4F, 0x004F},
{0x50, 0x004F},
{0x51, 0x004F},
{0x52, 0x004F},
{0x53, 0x004F},
{0x54, 0x004F},
{0x55, 0x004F},
{0x56, 0x004F},
{0x57, 0x0050},
{0x58, 0x0050},
{0x59, 0x0050},
{0x5A, 0x0051},
{0x5B, 0x0051},
{0x5C, 0x0052},
{0x5D, 0x0053},
{0x5E, 0x0054},
{0x5F, 0x0055},
{0x60, 0x0056},
{0x61, 0x0057},
{0x62, 0x0059},
{0x63, 0x005A},
{0x64, 0x005C},
{0x65, 0x005E},
{0x66, 0x0060},
{0x67, 0x0063},
{0x68, 0x0065},
{0x69, 0x0068},
{0x6A, 0x006B},
{0x6B, 0x006E},
{0x6C, 0x0072},
{0x6D, 0x0075},
{0x6E, 0x0079},
{0x6F, 0x007E},
{0x70, 0x0082},
{0x71, 0x0087},
{0x72, 0x008C},
{0x73, 0x0092},
{0x74, 0x0098},
{0x75, 0x009E},
{0x76, 0x00A4},
{0x77, 0x00AB},
{0x78, 0x00B2},
{0x79, 0x00B9},
{0x7A, 0x00C1},
{0x7B, 0x00C9},
{0x7C, 0x00D2},
{0x7D, 0x00DA},
{0x7E, 0x00E4},
{0x7F, 0x00ED},
{0x80, 0x00F8},
{0x81, 0x0102},
{0x82, 0x010D},
{0x83, 0x0119},
{0x84, 0x0124},
{0x85, 0x0131},
{0x86, 0x013E},
{0x87, 0x014B},
{0x88, 0x0159},
{0x89, 0x0167},
{0x8A, 0x0176},
{0x8B, 0x0185},
{0x8C, 0x0195},
{0x8D, 0x01A5},
{0x8E, 0x01B6},
{0x8F, 0x01C7},
{0x90, 0x01D9},
{0x91, 0x01EC},
{0x92, 0x01FF},
{0x93, 0x0212},
{0x94, 0x0226},
{0x95, 0x023B},
{0x96, 0x0251},
{0x97, 0x0267},
{0x98, 0x027D},
{0x99, 0x0294},
{0x9A, 0x02AC},
{0x9B, 0x02C5},
{0x9C, 0x02DE},
{0x9D, 0x02F8},
{0x9E, 0x0312},
{0x9F, 0x032E},
{0xA0, 0x034A},
{0xA1, 0x0366},
{0xA2, 0x0384},
{0xA3, 0x03A2},
{0xA4, 0x03C0},
{0xA5, 0x03E0},
{0xA6, 0x0400},
{0xA7, 0x0421},
{0xA8, 0x0443},
{0xA9, 0x0465},
{0xAA, 0x0489},
{0xAB, 0x04AC},
{0xAC, 0x04D1},
{0xAD, 0x04F7},
{0xAE, 0x051D},
{0xAF, 0x0545},
{0xB0, 0x056D},
{0xB1, 0x0596},
{0xB2, 0x05C0},
{0xB3, 0x05EA},
{0xB4, 0x0616},
{0xB5, 0x0642},
{0xB6, 0x066F},
{0xB7, 0x069D},
{0xB8, 0x06CC},
{0xB9, 0x06FC},
{0xBA, 0x072D},
{0xBB, 0x075F},
{0xBC, 0x0792},
{0xBD, 0x07C6},
{0xBE, 0x07FA},
{0xBF, 0x0830},
{0xC0, 0x0866},
{0xC1, 0x089E},
{0xC2, 0x08D6},
{0xC3, 0x090F},
{0xC4, 0x094A},
{0xC5, 0x0985},
{0xC6, 0x09C2},
{0xC7, 0x09FF},
{0xC8, 0x0A3E},
{0xC9, 0x0A7D},
{0xCA, 0x0ABE},
{0xCB, 0x0B00},
{0xCC, 0x0B42},
{0xCD, 0x0B86},
{0xCE, 0x0BCB},
{0xCF, 0x0C11},
{0xD0, 0x0C58},
{0xD1, 0x0CA1},
{0xD2, 0x0CEA},
{0xD3, 0x0D34},
{0xD4, 0x0D80},
{0xD5, 0x0DCD},
{0xD6, 0x0E1B},
{0xD7, 0x0E6A},
{0xD8, 0x0EBA},
{0xD9, 0x0F0B},
{0xDA, 0x0F5E},
{0xDB, 0x0FB2},
{0xDC, 0x1007},
{0xDD, 0x105D},
{0xDE, 0x10B4},
{0xDF, 0x110D},
{0xE0, 0x1167},
{0xE1, 0x11C2},
{0xE2, 0x121F},
{0xE3, 0x127C},
{0xE4, 0x12DB},
{0xE5, 0x133C},
{0xE6, 0x139D},
{0xE7, 0x1400},
{0xE8, 0x1464},
{0xE9, 0x14CA},
{0xEA, 0x1530},
{0xEB, 0x1599},
{0xEC, 0x1602},
{0xED, 0x166D},
{0xEE, 0x16D9},
{0xEF, 0x1747},
{0xF0, 0x17B6},
{0xF1, 0x1826},
{0xF2, 0x1898},
{0xF3, 0x190B},
{0xF4, 0x197F},
{0xF5, 0x19F5},
{0xF6, 0x1A6D},
{0xF7, 0x1AE5},
{0xF8, 0x1B60},
{0xF9, 0x1BDB},
{0xFA, 0x1C58},
{0xFB, 0x1CD7},
{0xFC, 0x1D57},
{0xFD, 0x1DD9},
{0xFE, 0x1E5C},
{0xFF, 0x1EE1},
}
if false {
for i := 0; i <= 255; i++ {
fmt.Printf("{0x%02X, 0x%04X},\n", i, ramp(uint8(i), maxOut))
}
}
for i, line := range data {
if i != int(line.input) {
t.Fatalf("expected ordered inputs; %d != %d", i, line.input)
}
if actual := ramp(line.input, maxOut); actual != line.expected {
t.Fatalf("%d: %v != %v", i, line.expected, actual)
}
}
if v := ramp(0x00, 0xFF); v != 0x00 {
t.Fatal(v)
}
if v := ramp(0x7F, 0xFF); v != 0x21 {
t.Fatal(v)
}
if v := ramp(0xFF, 0xFF); v != 0xFF {
t.Fatal(v)
}
}
func TestRampMonotonic(t *testing.T) {
// Ensures the ramp is 100% monotonically increasing and without bumps.
lastValue := uint16(0)
lastDelta := uint16(0)
for in := uint32(0); in <= 255; in++ {
out := ramp(uint8(in), maxOut)
if out < lastValue {
t.Fatalf("f(%d) = %d; f(%d) = %d", in-1, lastValue, in, out)
}
if out > maxOut {
t.Fatalf("f(%d) = %d", in, out)
}
if out-lastValue+1 < lastDelta {
t.Errorf("f(%d)=%d f(%d)=%d f(%d)=%d Deltas: '%d+1 < %d' but should be '>='",
in-2, ramp(uint8(in-2), maxOut), in-1, ramp(uint8(in-1), maxOut), in, ramp(uint8(in), maxOut), out-lastValue, lastDelta)
}
lastDelta = out - lastValue
lastValue = out
}
}
func TestDevEmpty(t *testing.T) {
buf := bytes.Buffer{}
o := DefaultOpts
o.NumPixels = 0
d, _ := New(spitest.NewRecordRaw(&buf), &o, spi.Mode3)
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("\ngot: %#02v\nwant: %#02v\n", buf.Bytes(), expected)
}
if s := d.String(); s != "APA102{I:255, T:5000K, GPWM:true, 0LEDs, recordraw}" {
t.Fatal(s)
}
}
func TestConnectFail(t *testing.T) {
if d, err := New(&configFail{}, &DefaultOpts, spi.Mode3); d != nil || err == nil {
t.Fatal("Connect() call have failed")
}
}
func TestDevLen(t *testing.T) {
buf := bytes.Buffer{}
o := DefaultOpts
o.NumPixels = 1
d, _ := New(spitest.NewRecordRaw(&buf), &o, spi.Mode3)
if n, err := d.Write([]byte{0}); n != 0 || err == nil {
t.Fatalf("%d %v", n, err)
}
if expected := []byte{}; !bytes.Equal(expected, buf.Bytes()) {
t.Fatalf("\ngot: %#02v\nwant: %#02v\n", buf.Bytes(), expected)
}
}
var writeTests = []struct {
name string
pixels []byte
want []byte
opts Opts
}{
{
name: "Temperature",
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, 0xFB, 0xFB, 0xFB,
0xFF, 0xC4, 0xC4, 0xC4,
0xE1, 0xF8, 0xF8, 0xF8,
0xE1, 0x00, 0x00, 0xF8,
0xE1, 0x00, 0xF8, 0x00,
0xE1, 0xF8, 0x00, 0x00,
0xE1, 0x10, 0x00, 0x00,
0xE1, 0x01, 0x00, 0x00,
0xE1, 0x00, 0x00, 0x00,
0xFF,
},
opts: Opts{
Intensity: 255,
Temperature: NeutralTemp,
},
},
{
name: "Intensity",
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, 0x7F, 0x7F, 0x7F,
0xFF, 0x7D, 0x7D, 0x7D,
0xFF, 0x67, 0x67, 0x67,
0xE2, 0x9B, 0x9B, 0x9B,
0xE2, 0x00, 0x00, 0x9B,
0xE2, 0x00, 0x9B, 0x00,
0xE2, 0x9B, 0x00, 0x00,
0xE1, 0x10, 0x00, 0x00,
0xE1, 0x01, 0x00, 0x00,
0xE1, 0x00, 0x00, 0x00,
0xFF,
},
opts: Opts{
Intensity: 127,
Temperature: NeutralTemp,
},
},
{
name: "PassThru",
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: PassThruOpts,
},
{
name: "DisableGlobalPWM - Intensity",
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, 0x40, 0x40, 0x40,
0xff, 0x40, 0x40, 0x40,
0xff, 0x3c, 0x3c, 0x3c,
0xff, 0x20, 0x20, 0x20,
0xff, 0x00, 0x00, 0x20,
0xff, 0x00, 0x20, 0x00,
0xff, 0x20, 0x00, 0x00,
0xff, 0x04, 0x00, 0x00,
0xff, 0x00, 0x00, 0x00,
0xff, 0x00, 0x00, 0x00,
0xff,
},
opts: Opts{
Intensity: 64,
Temperature: NeutralTemp,
DisableGlobalPWM: true,
},
},
{
name: "DisableGlobalPWM - Temp",
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, 0xd5, 0xe8, 0xff,
0xff, 0xd4, 0xe7, 0xfe,
0xff, 0xc8, 0xda, 0xf0,
0xff, 0x6b, 0x74, 0x80,
0xff, 0x00, 0x00, 0x80,
0xff, 0x00, 0x74, 0x00,
0xff, 0x6b, 0x00, 0x00,
0xff, 0x0d, 0x00, 0x00,
0xff, 0x01, 0x00, 0x00,
0xff, 0x00, 0x00, 0x00,
0xff,
},
opts: Opts{
Intensity: 255,
Temperature: 5000,
DisableGlobalPWM: true,
},
},
{
name: "Intensity and temperature",
pixels: func() []byte {
var p []byte
for i := 0; i < 16*3; i++ {
p = append(p, uint8(i<<2))
}
return p
}(),
want: expectedi250t5000,
opts: Opts{
Intensity: 250,
Temperature: 5000,
},
},
}
func TestWrites(t *testing.T) {
for _, tt := range writeTests {
buf := bytes.Buffer{}
tt.opts.NumPixels = len(tt.pixels) / 3
d, _ := New(spitest.NewRecordRaw(&buf), &tt.opts, spi.Mode3)
n, err := d.Write(tt.pixels)
if err != nil {
t.Fatal(err)
}
if n != len(tt.pixels) {
t.Fatalf("%s: Got %d bytes result, want %d", tt.name, n, len(tt.pixels)*3)
}
if !bytes.Equal(buf.Bytes(), tt.want) {
t.Fatalf("%s:\ngot: %#02v\nwant: %#02v\n", tt.name, buf.Bytes(), tt.want)
}
}
}
func TestDevColor(t *testing.T) {
if c := (&Dev{}).ColorModel(); c != color.NRGBAModel {
t.Fatal(c)
}
}
func TestDevLong(t *testing.T) {
buf := bytes.Buffer{}
colors := make([]color.NRGBA, 256)
o := DefaultOpts
o.NumPixels = len(colors)
d, _ := New(spitest.NewRecordRaw(&buf), &o, spi.Mode3)
if n, err := d.Write(ToRGB(colors)); n != len(colors)*3 || err != nil {
t.Fatalf("%d %v", n, err)
}
expected := make([]byte, 4*(256+1)+17)
for i := 0; i < 256; i++ {
expected[4+4*i] = 0xE1
}
trailer := expected[4*257:]
for i := range trailer {
trailer[i] = 0xFF
}
if !bytes.Equal(expected, buf.Bytes()) {
t.Fatalf("\ngot: %#02v\nwant: %#02v\n", buf.Bytes(), expected)
}
}
func TestDevWrite_Long(t *testing.T) {
buf := bytes.Buffer{}
o := DefaultOpts
o.NumPixels = 1
d, _ := New(spitest.NewRecordRaw(&buf), &o, spi.Mode3)
if n, err := d.Write([]byte{0, 0, 0, 1, 1, 1}); n != 0 || err == nil {
t.Fatal(n, err)
}
}
// expectedi250t5000 is the expected output for multiple test cases. Each test case
// use a completely different code path so make sure each code path results in
// the exact same output.
var expectedi250t5000 = []byte{
0x00, 0x00, 0x00, 0x00, 0xE1, 0x08, 0x04, 0x00, 0xE1, 0x14, 0x10, 0xC, 0xE1,
0x20, 0x1C, 0x18, 0xE1, 0x2C, 0x28, 0x24, 0xE1, 0x38, 0x34, 0x30, 0xE1, 0x41,
0x40, 0x3C, 0xE1, 0x44, 0x47, 0x48, 0xE1, 0x53, 0x4C, 0x4E, 0xE1, 0x78, 0x62,
0x56, 0xE1, 0xBD, 0x94, 0x73, 0xE2, 0x95, 0x77, 0x5A, 0xE2, 0xE5, 0xBD, 0x94,
0xE4, 0xAA, 0x92, 0x77, 0xE4, 0xF3, 0xD7, 0xB8, 0xFF, 0x2B, 0x28, 0x23, 0xFF,
0x3A, 0x36, 0x32, 0xFF, 0xFF,
}
// expectedi250t6500 is the default color temperature.
var expectedi250t6500 = []byte{
0x00, 0x00, 0x00, 0x00, 0xE1, 0x08, 0x04, 0x00, 0xE1, 0x14, 0x10, 0x0C, 0xE1,
0x20, 0x1C, 0x18, 0xE1, 0x2C, 0x28, 0x24, 0xE1, 0x38, 0x34, 0x30, 0xE1, 0x44,
0x40, 0x3C, 0xE1, 0x4E, 0x4C, 0x48, 0xE1, 0x52, 0x4F, 0x4E, 0xE1, 0x66, 0x5C,
0x56, 0xE1, 0x9A, 0x84, 0x73, 0xE1, 0xFB, 0xD4, 0xB4, 0xE2, 0xCB, 0xAE, 0x94,
0xE4, 0xA0, 0x8A, 0x77, 0xE4, 0xF0, 0xD2, 0xB8, 0xFF, 0x2D, 0x28, 0x23, 0xFF,
0x3E, 0x38, 0x32, 0xFF, 0xFF,
}
// expectedi250raw is using DisableGlobalPWM = true.
var expectedi250raw = []byte{
0x00, 0x00, 0x00, 0x00, 0xFF, 0x08, 0x04, 0x00, 0xFF, 0x14, 0x10, 0x0C, 0xFF,
0x1F, 0x1B, 0x18, 0xFF, 0x2B, 0x27, 0x23, 0xFF, 0x37, 0x33, 0x2F, 0xFF, 0x43,
0x3F, 0x3B, 0xFF, 0x4E, 0x4B, 0x47, 0xFF, 0x5A, 0x56, 0x52, 0xFF, 0x66, 0x62,
0x5E, 0xFF, 0x72, 0x6E, 0x6A, 0xFF, 0x7D, 0x7A, 0x76, 0xFF, 0x89, 0x85, 0x81,
0xFF, 0x95, 0x91, 0x8D, 0xFF, 0xA1, 0x9D, 0x99, 0xFF, 0xAD, 0xA9, 0xA5, 0xFF,
0xB8, 0xB4, 0xB0, 0xFF, 0xFF,
}
var drawTests = []struct {
name string
img image.Image
want []byte
opts Opts
}{
{
name: "Draw NRGBA",
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: expectedi250t5000,
opts: Opts{
NumPixels: 16,
Intensity: 250,
Temperature: 5000,
},
},
{
name: "Draw NRGBA Wide",
img: func() image.Image {
im := image.NewNRGBA(image.Rect(0, 0, 17, 2))
for x := 0; x < 16; x++ {
// Test all intensity code paths. Confirm that alpha is ignored.
im.SetNRGBA(x, 0, color.NRGBA{uint8((3 * x) << 2), uint8((3*x + 1) << 2), uint8((3*x + 2) << 2), 0})
}
return im
}(),
want: expectedi250t6500,
opts: Opts{
NumPixels: 16,
Intensity: 250,
Temperature: NeutralTemp,
},
},
{
name: "Draw NRGBA no global PWM",
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,
Temperature: NeutralTemp,
Intensity: 250,
DisableGlobalPWM: true,
},
},
{
name: "Draw RGBA",
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: expectedi250t5000,
opts: Opts{
NumPixels: 16,
Intensity: 250,
Temperature: 5000,
},
},
{
// Just something that doesn't have a fast path
name: "Draw NRGBA64",
img: func() image.Image {
im := image.NewNRGBA64(image.Rect(0, 0, 16, 1))
for i := 0; i < 16; i++ {
im.Set(i, 0, color.NRGBA64{
R: uint16(((3 * i) << 10)),
G: uint16(((3*i + 1) << 10)),
B: uint16(((3*i + 2) << 10)),
A: 0xFFFF,
})
}
return im
}(),
want: expectedi250t5000,
opts: Opts{
NumPixels: 16,
Intensity: 250,
Temperature: 5000,
},
},
{
name: "Draw RGBA no global PWM",
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,
Temperature: NeutralTemp,
Intensity: 250,
DisableGlobalPWM: true,
},
},
}
func TestDraws(t *testing.T) {
for _, tt := range drawTests {
buf := bytes.Buffer{}
d, _ := New(spitest.NewRecordRaw(&buf), &tt.opts, spi.Mode3)
if err := d.Draw(d.Bounds(), tt.img, image.Point{}); err != nil {
t.Fatalf("%s: %v", tt.name, err)
}
if !bytes.Equal(buf.Bytes(), tt.want) {
t.Fatalf("%s:\ngot: %#02v\nwant: %#02v\n", tt.name, buf.Bytes(), tt.want)
}
}
}
var offsetDrawWant = []byte{
0x00, 0x00, 0x00, 0x00,
0xE1, 0x89, 0x79, 0x6B,
0xE1, 0x9A, 0x88, 0x75,
0xE1, 0xAD, 0x98, 0x82,
0xE1, 0xC2, 0xAB, 0x92,
0xE1, 0xDA, 0xC0, 0xA4,
0xE1, 0xF5, 0xD9, 0xB9,
0xE2, 0x89, 0x7A, 0x69,
0xE2, 0x9A, 0x8A, 0x76,
0xE2, 0xAC, 0x9B, 0x86,
0xE2, 0xC0, 0xAE, 0x98,
0xE2, 0xD5, 0xC3, 0xAC,
0xE2, 0xED, 0xDA, 0xC2,
0xE4, 0x83, 0x7A, 0x6E,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0xFF,
}
var offsetDrawTests = []struct {
name string
img image.Image
point image.Point
offset image.Rectangle
want []byte
opts Opts
}{
{
name: "Offset Draw NRGBA",
img: func() image.Image {
im := image.NewNRGBA(image.Rect(0, 0, 16, 4))
for x := 0; x < 16; x++ {
for y := 0; y < 4; y++ {
i := (y*16 + x) * 3
im.Set(x, y, color.RGBA{R: uint8(i + 1), G: uint8(i + 2), B: uint8(i + 3), A: 0xFF})
}
}
return im
}(),
point: image.Point{X: 3, Y: 2},
offset: image.Rect(0, 0, 16, 1),
want: offsetDrawWant,
opts: Opts{
NumPixels: 15,
Intensity: 255,
Temperature: 5000,
},
},
{
name: "Both Offset Draw NRGBA",
img: func() image.Image {
im := image.NewNRGBA(image.Rect(0, 0, 16, 4))
for x := 0; x < 16; x++ {
for y := 0; y < 4; y++ {
i := (y*16 + x) * 3
im.Set(x, y, color.RGBA{R: uint8(i + 1), G: uint8(i + 2), B: uint8(i + 3), A: 0xFF})
}
}
return im
}(),
point: image.Point{X: 3, Y: 2},
offset: image.Rect(2, 0, 16, 1),
want: []byte{
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0xE1, 0x89, 0x79, 0x6B,
0xE1, 0x9A, 0x88, 0x75,
0xE1, 0xAD, 0x98, 0x82,
0xE1, 0xC2, 0xAB, 0x92,
0xE1, 0xDA, 0xC0, 0xA4,
0xE1, 0xF5, 0xD9, 0xB9,
0xE2, 0x89, 0x7A, 0x69,
0xE2, 0x9A, 0x8A, 0x76,
0xE2, 0xAC, 0x9B, 0x86,
0xE2, 0xC0, 0xAE, 0x98,
0xE2, 0xD5, 0xC3, 0xAC,
0xE2, 0xED, 0xDA, 0xC2,
0xE4, 0x83, 0x7A, 0x6E,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF,
},
opts: Opts{
NumPixels: 17,
Intensity: 255,
Temperature: 5000,
},
},
}
func TestOffsetDraws(t *testing.T) {
for _, tt := range offsetDrawTests {
buf := bytes.Buffer{}
d, _ := New(spitest.NewRecordRaw(&buf), &tt.opts, spi.Mode3)
if err := d.Draw(tt.offset, tt.img, tt.point); err != nil {
t.Fatalf("%s: %v", tt.name, err)
}
if !bytes.Equal(buf.Bytes(), tt.want) {
t.Fatalf("%s:\ngot: %#02v\nwant: %#02v\n", tt.name, buf.Bytes(), tt.want)
}
}
}
func TestHalt(t *testing.T) {
s := spitest.Playback{
Playback: conntest.Playback{
Ops: []conntest.IO{
{W: []byte{0x0, 0x0, 0x0, 0x0, 0xe1, 0x0, 0x0, 0x0, 0xe1, 0x0, 0x0, 0x0, 0xe1, 0x0, 0x0, 0x0, 0xe1, 0x0, 0x0, 0x0, 0xff}},
},
},
}
o := DefaultOpts
o.NumPixels = 4
o.Temperature = 5000
d, _ := New(&s, &o, spi.Mode3)
if err := d.Halt(); err != nil {
t.Fatal(err)
}
if err := s.Close(); err != nil {
t.Fatal(err)
}
}
func TestInit(t *testing.T) {
// Catch the "maxB == maxG" line.
l := lut{}
l.init(255, 6000, true)
if equalUint16(l.r[:], l.g[:]) || !equalUint16(l.g[:], l.b[:]) {
t.Fatal("test case is for only when maxG == maxB but maxR != maxG")
}
}
//
type genColor func(int) [3]byte
func benchmarkWrite(b *testing.B, o Opts, length int, f genColor) {
var pixels []byte
for i := 0; i < length; i++ {
c := f(i)
pixels = append(pixels, c[:]...)
}
o.NumPixels = length
b.ReportAllocs()
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &o, spi.Mode3)
_, _ = d.Write(pixels[:])
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = d.Write(pixels[:])
}
}
func BenchmarkWriteWhite(b *testing.B) {
o := DefaultOpts
o.Intensity = 250
benchmarkWrite(b, o, 150, func(i int) [3]byte { return [3]byte{0xFF, 0xFF, 0xFF} })
}
func BenchmarkWriteDim(b *testing.B) {
o := DefaultOpts
o.Intensity = 250
benchmarkWrite(b, o, 150, func(i int) [3]byte { return [3]byte{0x01, 0x01, 0x01} })
}
func BenchmarkWriteBlack(b *testing.B) {
o := DefaultOpts
o.Intensity = 250
benchmarkWrite(b, o, 150, func(i int) [3]byte { return [3]byte{0x0, 0x0, 0x0} })
}
func genColorfulPixel(x int) [3]byte {
i := x * 3
return [3]byte{uint8(i) + uint8(i>>8),
uint8(i+1) + uint8(i+1>>8),
uint8(i+2) + uint8(i+2>>8),
}
}
func BenchmarkWriteColorful(b *testing.B) {
o := DefaultOpts
o.Intensity = 250
o.Temperature = 5000
benchmarkWrite(b, o, 150, genColorfulPixel)
}
func BenchmarkWriteColorfulPassThru(b *testing.B) {
o := PassThruOpts
o.Intensity = 250
benchmarkWrite(b, o, 150, genColorfulPixel)
}
func BenchmarkWriteColorfulVariation(b *testing.B) {
// Continuously vary the lookup tables.
b.ReportAllocs()
pixels := [256 * 3]byte{}
for i := range pixels {
pixels[i] = uint8(i) + uint8(i>>8)
}
o := DefaultOpts
o.NumPixels = len(pixels) / 3
o.Intensity = 250
o.Temperature = 5000
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &o, spi.Mode3)
_, _ = d.Write(pixels[:])
b.ResetTimer()
for i := 0; i < b.N; i++ {
d.Intensity = uint8(i)
d.Temperature = uint16((3000 + i) & 0x1FFF)
_, _ = d.Write(pixels[:])
}
}
func benchmarkDraw(b *testing.B, o Opts, img draw.Image, f genColor) {
for x := 0; x < img.Bounds().Dx(); x++ {
for y := 0; y < img.Bounds().Dy(); y++ {
pix := f(x)
c := color.NRGBA{R: pix[0], G: pix[1], B: pix[2], A: 255}
img.Set(x, y, c)
}
}
o.NumPixels = img.Bounds().Max.X
b.ReportAllocs()
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &o, spi.Mode3)
r := d.Bounds()
p := image.Point{}
if err := d.Draw(r, img, p); err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := d.Draw(r, img, p); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkDrawNRGBAColorful(b *testing.B) {
o := DefaultOpts
o.Intensity = 250
o.Temperature = 5000
benchmarkDraw(b, o, image.NewNRGBA(image.Rect(0, 0, 150, 1)), genColorfulPixel)
}
func BenchmarkDrawNRGBAColorfulPassThru(b *testing.B) {
o := PassThruOpts
o.Intensity = 250
benchmarkDraw(b, o, image.NewNRGBA(image.Rect(0, 0, 150, 1)), genColorfulPixel)
}
func BenchmarkDrawNRGBAWhite(b *testing.B) {
o := DefaultOpts
o.Intensity = 250
o.Temperature = 5000
benchmarkDraw(b, o, image.NewNRGBA(image.Rect(0, 0, 150, 1)), func(i int) [3]byte { return [3]byte{0xFF, 0xFF, 0xFF} })
}
func BenchmarkDrawRGBAColorful(b *testing.B) {
o := DefaultOpts
o.Intensity = 250
o.Temperature = 5000
benchmarkDraw(b, o, image.NewRGBA(image.Rect(0, 0, 256, 1)), genColorfulPixel)
}
func BenchmarkDrawRGBAColorfulPassThru(b *testing.B) {
o := PassThruOpts
o.Intensity = 250
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.NewGray(image.Rect(0, 0, 150, 1))
for x := 0; x < img.Bounds().Dx(); x++ {
for y := 0; y < img.Bounds().Dy(); y++ {
pix := genColorfulPixel(x)
img.Set(x, y, color.Gray{pix[0]})
}
}
o := DefaultOpts
o.NumPixels = img.Bounds().Max.X
b.ReportAllocs()
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &o, spi.Mode3)
r := d.Bounds()
p := image.Point{}
if err := d.Draw(r, img, p); err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := d.Draw(r, img, p); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkHalt(b *testing.B) {
b.ReportAllocs()
d, _ := New(spitest.NewRecordRaw(ioutil.Discard), &DefaultOpts, spi.Mode3)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := d.Halt(); err != nil {
b.Fatal(err)
}
}
}
//
type configFail struct {
spitest.Record
}
func (c *configFail) Connect(f physic.Frequency, mode spi.Mode, bits int) (spi.Conn, error) {
return nil, errors.New("injected error")
}
func equalUint16(a, b []uint16) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}