diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6f14fa2..537ab17 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jan 25 12:34:01 PST 2017 +#Tue Aug 15 21:09:08 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/settings.gradle b/settings.gradle index d843cf1..8e1c3b3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -31,5 +31,6 @@ include ':rainbowhat' include ':sensehat' include ':voicehat' include ':zxgesturesensor' +include ':ws2812b' include ':testingutils' diff --git a/ws2812b/README.md b/ws2812b/README.md new file mode 100644 index 0000000..d036c48 --- /dev/null +++ b/ws2812b/README.md @@ -0,0 +1,142 @@ +WS2812B LED driver for Android Things +===================================== + +This driver supports WS2812B LEDs (maybe WS1812S and SK6812 run too). + +NOTE: these drivers are not production-ready. They are offered as sample +implementations of Android Things user space drivers for common peripherals +as part of the Developer Preview release. There is no guarantee +of correctness, completeness or robustness. + +How to use the driver +--------------------- + +### Gradle dependency + +To use the `WS2812B` driver, simply add the line below to your project's `build.gradle`, +where `` matches the last version of the driver available on [jcenter][jcenter]. + +``` +dependencies { + compile 'com.google.android.things.contrib:driver-ws2812b:' +} +``` + +### Sample usage + +```java +import com.google.android.things.contrib.driver.ws2812b.Ws2812b; + +// Access the LED strip: + +Ws2812b mWs2812b; + +try { + mWs2812b = new Ws2812b(spiBusName); +} catch (IOException e) { + // couldn't configure the device... +} + +// Light it up! + +int[] colors = new int[] {Color.RED, Color.GREEN, Color.BLUE}; +try { + mWs2812b.write(colors); +} catch (IOException e) { + // error setting LEDs +} + +// Close the LED strip when finished: + +try { + mWs2812b.close(); +} catch (IOException e) { + // error closing LED strip +} +``` + +How it works +------------ + +### The WS2812B data format + +The WS2812B LED controller needs 24 bits of data (8 bits per color channel) to set the color of one LED. Every further LED of a strip needs another 24 bit long block of data. The transmission of these bits is done by sending a chain of high and low voltage pulses over the data line of the LED controller. +Each separate bit is hereby defined by a high voltage pulse which is followed by a low voltage pulse. The recognition as 0- or 1-bit is defined by the timings of these pulses: + +

+ +

+ +* The 0-bit is defined by a high voltage pulse with a duration of 400 ns which is followed by a low voltage pulse of 850 ns +* The 1-bit is defined by a high voltage pulse with a duration of 850 ns which is followed by a low voltage pulse of 400 ns +* Each pulse can have a deviation of +/- 150 ns + +At the moment there is no way to send such short timed pulses having _different durations_ via the Android Things API. + +### The Serial Peripheral Interface + +The [Serial Peripheral Interface (SPI)](https://developer.android.com/things/sdk/pio/spi.html) can send bits as voltage pulses. Those pulses all have the same specified duration. + +* The pulse duration is indirectly defined by the frequency of the SPI +* A transmitted 1-bit results in a short high voltage pulse at the SPI MOSI (Master Out Slave In) pinout +* A transmitted 0-bit results in a short low voltage pulse at the MOSI pinout + +### Approximating the WS2812B data format using SPI + +In order to control WS2812B LEDs via the SPI, we must find two assemblies of bits (hereinafter bit pattern) and a frequency such that each of these bit patterns results in a sequence of voltage pulses which are recognized as 0 or 1 bit by the receiving WS2812B controller. With these two bit patterns we are able to convert every bit of an arbitrary array of color data. If then the converted data is sent to the WS2812B controller by SPI, the controller will recognize the orignal color and light up the LEDs accordingly. A possible solution for this approach are the two 3 bit sized patterns below: + +

+ +

+ +The deviation from the WS2812B specified pulse duration is -16 ns and +17 ns respectively, which is within the allowed range of +/- 150 ns. It is possible to create a more accurate bit pattern with more than 3 bits, but increasing the size of the bit pattern also reduces the number of controllable LEDs as the fixed-size SPI buffer fills up more quickly. The appropriate frequency is defined by the duration of 1 bit (417 ns): + +

+ +

+ +### Handling pauses between SPI words + +SPI will automatically insert low voltage pauses between transmitted words. This short break marks the end of a transmitted word and has the same duration as a single bit. If left unhandled, those pauses would interfere with our approximated data format and lead to incorrect colors. By considering a word size of 8 bits (maximum size), the pause can be understood as an automatically inserted 0-bit between the 8th and the 9th bit. Fortunately, all possible bit sequences yield a bit pattern where every 9th bit is a 0-bit. Hence an easy solution is to discard the last bit like shown in the table below: + +| Source bit sequence | Resulting bit pattern | Without trailing 0-bit | +| ------------------- |:----------------------:|:------------------------:| +| 000 | 100 100 10**0** | 100 100 10 | +| 001 | 100 100 11**0** | 100 100 11 | +| 010 | 100 110 10**0** | 100 110 10 | +| 011 | 100 110 11**0** | 100 110 11 | +| 100 | 110 100 10**0** | 110 100 10 | +| 110 | 110 110 10**0** | 110 110 10 | +| 111 | 110 110 11**0** | 110 110 11 | + +With this in mind we can represent three source bits using one destination byte. So any possible 24 bit color needs to be converted to a 8 byte sized sequence of bit patterns (24 / 3 = 8). + +To prevent excessive memory usage this driver stores and maps only 12 bit numbers to their corresponding 4 byte sized bit patterns ([TwelveBitIntToBitPatternMapper.java](/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/TwelveBitIntToBitPatternMapper.java)). The full 8 byte sized bit pattern for 24 bits of color data is then constructed by two 4 byte sized bit patterns ([ColorToBitPatternConverter.java](/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/ColorToBitPatternConverter.java)). Last but not least, most WS2812B controllers expect GRB as order of the incoming color. So a reordering from RGB to the expected order must be done before the bit pattern conversion ([ColorChannelSequence.java](/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/ColorChannelSequence.java)). + +References +---------- +* https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ +* https://cpldcpu.com/2014/01/14/light_ws2812-library-v2-0-part-i-understanding-the-ws2812/ +* https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf + +License +------- + +Copyright 2016 Google Inc. + +Licensed to the Apache Software Foundation (ASF) under one or more contributor +license agreements. See the NOTICE file distributed with this work for +additional information regarding copyright ownership. The ASF licenses this +file to you under the Apache License, Version 2.0 (the "License"); you may not +use this file except in compliance with the License. You may obtain a copy of +the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations under +the License. + +[jcenter]: https://bintray.com/google/androidthings/contrib-driver-ws2812b/_latestVersion diff --git a/ws2812b/build.gradle b/ws2812b/build.gradle new file mode 100644 index 0000000..fcf746e --- /dev/null +++ b/ws2812b/build.gradle @@ -0,0 +1,39 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 24 + buildToolsVersion '24.0.3' + + defaultConfig { + minSdkVersion 24 + targetSdkVersion 24 + versionCode 1 + versionName "1.0" + } +} + +dependencies { + provided 'com.google.android.things:androidthings:0.5-devpreview' + compile 'com.android.support:support-annotations:24.2.0' + + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.powermock:powermock-module-junit4:1.6.6' + testCompile 'org.powermock:powermock-api-mockito:1.6.6' +} diff --git a/ws2812b/gradle.properties b/ws2812b/gradle.properties new file mode 100644 index 0000000..34f9583 --- /dev/null +++ b/ws2812b/gradle.properties @@ -0,0 +1,2 @@ +TYPE="RGB LED strip" +ARTIFACT_VERSION=0.1 diff --git a/ws2812b/publish-settings.gradle b/ws2812b/publish-settings.gradle new file mode 100644 index 0000000..f0318e9 --- /dev/null +++ b/ws2812b/publish-settings.gradle @@ -0,0 +1 @@ +rootProject.buildFileName = '../publish.gradle' diff --git a/ws2812b/src/main/AndroidManifest.xml b/ws2812b/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9416d58 --- /dev/null +++ b/ws2812b/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/ColorChannelSequence.java b/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/ColorChannelSequence.java new file mode 100644 index 0000000..81fd0a5 --- /dev/null +++ b/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/ColorChannelSequence.java @@ -0,0 +1,85 @@ +package com.google.android.things.contrib.driver.ws2812b; + + +import android.support.annotation.ColorInt; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; + +import java.lang.annotation.Retention; +import java.util.Arrays; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +@SuppressWarnings("WeakerAccess") +public class ColorChannelSequence { + public static final int RGB = 1; + public static final int RBG = 2; + public static final int GRB = 3; + public static final int GBR = 4; + public static final int BRG = 5; + public static final int BGR = 6; + @Sequence + private static final int[] ALL_SEQUENCES = {RGB, RBG, GRB, GBR, BRG, BGR}; + + @Retention(SOURCE) + @IntDef({RGB, RBG, GRB, GBR, BRG, BGR}) + @SuppressWarnings("WeakerAccess") + public @interface Sequence {} + + interface Sequencer + { + int rearrangeColorChannels(@ColorInt int color); + } + + @NonNull + static Sequencer createSequencer(@Sequence int colorChannelSequence) + { + switch (colorChannelSequence) { + case BGR: + return new Sequencer() { + @Override + public int rearrangeColorChannels(int color) { + return ((color & 0xff0000) >> 16) | (color & 0xff00) | ((color & 0xff) << 16); + } + }; + case BRG: + return new Sequencer() { + @Override + public int rearrangeColorChannels(int color) { + return ((color & 0xff0000) >> 8) | ((color & 0xff00) >> 8) | ((color & 0xff) << 16); + } + }; + case GBR: + return new Sequencer() { + @Override + public int rearrangeColorChannels(int color) { + return ((color & 0xff0000) >> 16) | ((color & 0xff00) << 8) | ((color & 0xff) << 8); + } + }; + case GRB: + return new Sequencer() { + @Override + public int rearrangeColorChannels(int color) { + return ((color & 0xff0000) >> 8) | ((color & 0xff00) << 8) | (color & 0xff); + } + }; + case RBG: + return new Sequencer() { + @Override + public int rearrangeColorChannels(int color) { + return (color & 0xff0000) | ((color & 0xff00) >> 8) | ((color & 0xff) << 8); + } + }; + case RGB: + return new Sequencer() { + @Override + public int rearrangeColorChannels(int color) { + return color; + } + }; + default: + throw new IllegalArgumentException("Invalid color channel sequence: " + colorChannelSequence + ". Supported color channel sequences are: " + Arrays.toString(ColorChannelSequence.ALL_SEQUENCES)); + } + } + +} diff --git a/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/ColorToBitPatternConverter.java b/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/ColorToBitPatternConverter.java new file mode 100644 index 0000000..8e9b1ff --- /dev/null +++ b/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/ColorToBitPatternConverter.java @@ -0,0 +1,55 @@ +package com.google.android.things.contrib.driver.ws2812b; + + +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.Size; +import android.support.annotation.VisibleForTesting; + +class ColorToBitPatternConverter { + private static final int MAX_NUMBER_OF_SUPPORTED_LEDS = 512; + private static final int FIRST_TWELVE_BIT_BIT_MASK = 0x00FFF000; + private static final int SECOND_TWELVE_BIT_BIT_MASK = 0x00000FFF; + + private final TwelveBitIntToBitPatternMapper mTwelveBitIntToBitPatternMapper; + private ColorChannelSequence.Sequencer colorChannelSequencer; + + ColorToBitPatternConverter(@ColorChannelSequence.Sequence int colorChannelSequence) { + this(colorChannelSequence, new TwelveBitIntToBitPatternMapper()); + } + + @VisibleForTesting + ColorToBitPatternConverter(@ColorChannelSequence.Sequence int colorChannelSequence, @NonNull TwelveBitIntToBitPatternMapper twelveBitIntToBitPatternMapper) { + colorChannelSequencer = ColorChannelSequence.createSequencer(colorChannelSequence); + mTwelveBitIntToBitPatternMapper = twelveBitIntToBitPatternMapper; + } + + /** + * Converts the passed color array to a correlating byte array of bit patterns. These resulting + * bit patterns are readable by a WS2812B LED strip if they are sent by a SPI device with the + * right frequency. + * + * @param colors An array of color integers {@link ColorInt} + * @return Returns a byte array of correlating bit patterns + */ + byte[] convertToBitPattern(@ColorInt @NonNull @Size(max = MAX_NUMBER_OF_SUPPORTED_LEDS) int[] colors) { + if (colors.length > MAX_NUMBER_OF_SUPPORTED_LEDS) { + throw new IllegalArgumentException("Only " + MAX_NUMBER_OF_SUPPORTED_LEDS + " LEDs are supported. A Greater Number (" + colors.length + ") will result in SPI errors!"); + } + + byte[] bitPatterns = new byte[colors.length * 8]; + + int i = 0; + for (int color : colors) { + color = colorChannelSequencer.rearrangeColorChannels(color); + int firstValue = (color & FIRST_TWELVE_BIT_BIT_MASK) >> 12; + int secondValue = color & SECOND_TWELVE_BIT_BIT_MASK; + + System.arraycopy(mTwelveBitIntToBitPatternMapper.getBitPattern(firstValue), 0, bitPatterns, i, 4); + i += 4; + System.arraycopy(mTwelveBitIntToBitPatternMapper.getBitPattern(secondValue), 0, bitPatterns, i, 4); + i += 4; + } + return bitPatterns; + } +} diff --git a/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/TwelveBitIntToBitPatternMapper.java b/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/TwelveBitIntToBitPatternMapper.java new file mode 100644 index 0000000..703c12e --- /dev/null +++ b/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/TwelveBitIntToBitPatternMapper.java @@ -0,0 +1,135 @@ +package com.google.android.things.contrib.driver.ws2812b; + + +import android.support.annotation.IntRange; +import android.support.annotation.NonNull; +import android.support.annotation.Size; +import android.support.annotation.VisibleForTesting; +import android.util.SparseArray; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + * Creates a storage which maps any possible 12 bit sized integer to bit patterns. If a sequence of + * these bit patterns is sent to a WS2812b LED strip using SPI, the outcoming high and low voltage + * pulses are recognized as the original 12 bit integers.
+ * Converting algorithm:
+ * - 1 src bit is converted to a 3 bit long bit pattern
+ * - The 9th bit in a sequence of bit pattern is a pause bit and must be removed. The pause bit is + * automatically transmitted between every byte
+ * - This results in the fact that 3 source bits are converted to 1 destination byte
+ * + * => 3 src bits = 8 bit (1 dst byte)
+ * => 12 src bits = 32 bit (4 dst bytes)
+ */ +class TwelveBitIntToBitPatternMapper { + private static final int BIGGEST_12_BIT_NUMBER = 0B1111_1111_1111; + private static final List BIT_PATTERN_FOR_ZERO_BIT = Arrays.asList(true, false, false); + private static final List BIT_PATTERN_FOR_ONE_BIT = Arrays.asList(true, true, false); + private static final int ONE_BYTE_BIT_MASKS[] = new int[]{ 0B10000000, + 0B01000000, + 0B00100000, + 0B00010000, + 0B00001000, + 0B00000100, + 0B00000010, + 0B00000001}; + @NonNull + private final SparseArray mSparseArray; + + TwelveBitIntToBitPatternMapper() { + this(new SparseArray(BIGGEST_12_BIT_NUMBER)); + } + + @VisibleForTesting + TwelveBitIntToBitPatternMapper(@NonNull final SparseArray sparseArray) { + mSparseArray = sparseArray; + fillBitPatternStorage(); + } + + /** + * Returns for each possible 12 bit integer a corresponding sequence of bit pattern as byte array. + * Throws an {@link IllegalArgumentException} if the integer is using more than 12 bit. + * + * @param twelveBitValue Any 12 bit integer (from 0 to 4095) + * @return The corresponding bit pattern as 4 byte sized array + */ + @NonNull + @Size(value = 4) + byte[] getBitPattern(@IntRange(from = 0, to = BIGGEST_12_BIT_NUMBER) int twelveBitValue) { + byte[] bitPatternByteArray = mSparseArray.get(twelveBitValue); + if (bitPatternByteArray == null) + { + throw new IllegalArgumentException("Only values from 0 to " + BIGGEST_12_BIT_NUMBER + " are allowed. The passed input value was: " + twelveBitValue); + } + return bitPatternByteArray; + } + + private void fillBitPatternStorage() { + for (int i = 0; i <= BIGGEST_12_BIT_NUMBER; i++) { + mSparseArray.append(i, calculateBitPatternByteArray(i)); + } + } + + @Size(value = 4) + private byte[] calculateBitPatternByteArray(@IntRange(from = 0, to = BIGGEST_12_BIT_NUMBER) int twelveBitNumber) { + List bitPatterns = new ArrayList<>(); + int highest12BitBitMask = 1 << 11; + for (int i = 0; i < 12; i++) { + if ((twelveBitNumber & highest12BitBitMask) == highest12BitBitMask){ + bitPatterns.addAll(BIT_PATTERN_FOR_ONE_BIT); + } + else{ + bitPatterns.addAll(BIT_PATTERN_FOR_ZERO_BIT); + } + twelveBitNumber = twelveBitNumber << 1; + } + bitPatterns = removePauseBits(bitPatterns); + return convertBitPatternsToByteArray(bitPatterns); + } + + private List removePauseBits(List bitPatterns) { + Iterator iterator = bitPatterns.iterator(); + int i = 0; + while (iterator.hasNext()) { + iterator.next(); + if (i == 8) + { + iterator.remove(); + i = 0; + continue; + } + i++; + } + return bitPatterns; + } + + @Size(value = 4) + private byte[] convertBitPatternsToByteArray(List bitPatterns) { + + if (bitPatterns.size() != 32) + { + throw new IllegalArgumentException("Undefined bit pattern size: Expected size is 32. Passed size: " + bitPatterns.size()); + } + byte[] bitPatternsAsByteArray = new byte[4]; + bitPatternsAsByteArray [0] = convertBitPatternsToByte(bitPatterns.subList(0, 8)); + bitPatternsAsByteArray [1] = convertBitPatternsToByte(bitPatterns.subList(8, 16)); + bitPatternsAsByteArray [2] = convertBitPatternsToByte(bitPatterns.subList(16, 24)); + bitPatternsAsByteArray [3] = convertBitPatternsToByte(bitPatterns.subList(24, 32)); + return bitPatternsAsByteArray; + } + + private byte convertBitPatternsToByte(List bitPatterns) { + int bitPatternByte = 0; + for (int i = 0; i < 8; i++) { + if (bitPatterns.get(i)) + { + bitPatternByte |= ONE_BYTE_BIT_MASKS[i]; + } + } + return (byte) bitPatternByte; + } +} diff --git a/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/Ws2812b.java b/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/Ws2812b.java new file mode 100644 index 0000000..cdcada0 --- /dev/null +++ b/ws2812b/src/main/java/com/google/android/things/contrib/driver/ws2812b/Ws2812b.java @@ -0,0 +1,127 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.things.contrib.driver.ws2812b; + +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; + +import com.google.android.things.pio.PeripheralManagerService; +import com.google.android.things.pio.SpiDevice; + +import java.io.IOException; + +/** + * Device driver for WS2812B LEDs using SPI. + * + * For more information on SPI, see: + * https://en.wikipedia.org/wiki/Serial_Peripheral_Interface_Bus + * For information on the WS2812B protocol, see: + * https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ + * https://cpldcpu.com/2014/01/14/light_ws2812-library-v2-0-part-i-understanding-the-ws2812/ + */ + +@SuppressWarnings({"unused", "WeakerAccess"}) +public class Ws2812b implements AutoCloseable { + private static final String TAG = "Ws2812b"; + + @NonNull + private final ColorToBitPatternConverter mColorToBitPatternConverter; + private SpiDevice mDevice = null; + + /** + * Create a new WS2812B driver. + * + * @param spiBusPort Name of the SPI bus + */ + public Ws2812b(String spiBusPort) throws IOException { + this(spiBusPort, new ColorToBitPatternConverter(ColorChannelSequence.GRB)); + } + + /** + * Create a new WS2812B driver. + * + * @param spiBusPort Name of the SPI bus + * @param colorChannelSequence The {@link ColorChannelSequence.Sequence} indicates the red/green/blue byte order for the LED strip. + * @throws IOException if the initialization of the SpiDevice fails + * + */ + public Ws2812b(String spiBusPort, @ColorChannelSequence.Sequence int colorChannelSequence) throws IOException { + this (spiBusPort, new ColorToBitPatternConverter(colorChannelSequence)); + } + + private Ws2812b(String spiBusPort, @NonNull ColorToBitPatternConverter colorToBitPatternConverter) throws IOException { + mColorToBitPatternConverter = colorToBitPatternConverter; + mDevice = new PeripheralManagerService().openSpiDevice(spiBusPort); + try { + initSpiDevice(mDevice); + } catch (IOException|RuntimeException e) { + try { + close(); + } catch (IOException|RuntimeException ignored) { + } + throw e; + } + } + + @VisibleForTesting + /*package*/ Ws2812b(SpiDevice device, @NonNull ColorToBitPatternConverter colorToBitPatternConverter) throws IOException { + mColorToBitPatternConverter = colorToBitPatternConverter; + mDevice = device; + initSpiDevice(mDevice); + } + + private void initSpiDevice(SpiDevice device) throws IOException { + + double durationOfOneBitInNs = 417.0; + double durationOfOneBitInS = durationOfOneBitInNs * Math.pow(10, -9); + int frequencyInHz = (int) Math.round(1.0 / durationOfOneBitInS); + + device.setFrequency(frequencyInHz); + device.setBitsPerWord(8); + device.setDelay(0); + } + + /** + * Transforms the passed color array and writes it to the SPI connected WS2812b LED strip. + * @param colors An array of 24 bit RGB color integers {@link ColorInt} + * @throws IOException if writing to the SPI device fails + */ + public void write(@NonNull @ColorInt int[] colors) throws IOException { + if (mDevice == null) { + throw new IllegalStateException("SPI device not opened"); + } + + byte[] convertedColors = mColorToBitPatternConverter.convertToBitPattern(colors); + mDevice.write(convertedColors, convertedColors.length); + } + + /** + * Releases the SPI interface and related resources. + * @throws IOException if the SpiDevice is already closed + */ + @Override + public void close() throws IOException { + if (mDevice != null) { + try { + mDevice.close(); + } finally { + mDevice = null; + } + } + } +} diff --git a/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/ColorChannelSequenceTest.java b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/ColorChannelSequenceTest.java new file mode 100644 index 0000000..2d07c75 --- /dev/null +++ b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/ColorChannelSequenceTest.java @@ -0,0 +1,100 @@ +package com.google.android.things.contrib.driver.ws2812b; + + +import android.graphics.Color; + +import com.google.android.things.contrib.driver.ws2812b.util.ColorMock; +import com.google.android.things.contrib.driver.ws2812b.util.ColorUtil; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + + +@RunWith(PowerMockRunner.class) +@PrepareForTest(android.graphics.Color.class) +public class ColorChannelSequenceTest { + + private int red; + private int green; + private int blue; + + @Before + public void setUp() { + ColorMock.mockStatic(); + + red = ColorUtil.generateRandomColorValue(); + green = ColorUtil.generateRandomColorValue(); + blue = ColorUtil.generateRandomColorValue(); + } + + @Test + public void reorderToRgb(){ + ColorChannelSequence.Sequencer sequencer = ColorChannelSequence.createSequencer(ColorChannelSequence.RGB); + int rgbColor = Color.rgb(red, green, blue); + int rearrangedColor = sequencer.rearrangeColorChannels(rgbColor); + + Assert.assertEquals(ColorUtil.get1stColor(rearrangedColor), red); + Assert.assertEquals(ColorUtil.get2ndColor(rearrangedColor), green); + Assert.assertEquals(ColorUtil.get3rdColor(rearrangedColor), blue); + } + + @Test + public void reorderToRbg(){ + ColorChannelSequence.Sequencer sequencer = ColorChannelSequence.createSequencer(ColorChannelSequence.RBG); + int rgbColor = Color.rgb(red, green, blue); + int rearrangedColor = sequencer.rearrangeColorChannels(rgbColor); + + Assert.assertEquals(ColorUtil.get1stColor(rearrangedColor), red); + Assert.assertEquals(ColorUtil.get2ndColor(rearrangedColor), blue); + Assert.assertEquals(ColorUtil.get3rdColor(rearrangedColor), green); + } + + @Test + public void reorderToGrb(){ + ColorChannelSequence.Sequencer sequencer = ColorChannelSequence.createSequencer(ColorChannelSequence.GRB); + int rgbColor = Color.rgb(red, green, blue); + int rearrangedColor = sequencer.rearrangeColorChannels(rgbColor); + + Assert.assertEquals(ColorUtil.get1stColor(rearrangedColor), green); + Assert.assertEquals(ColorUtil.get2ndColor(rearrangedColor), red); + Assert.assertEquals(ColorUtil.get3rdColor(rearrangedColor), blue); + } + + @Test + public void reorderToGbr(){ + ColorChannelSequence.Sequencer sequencer = ColorChannelSequence.createSequencer(ColorChannelSequence.GBR); + int rgbColor = Color.rgb(red, green, blue); + int rearrangedColor = sequencer.rearrangeColorChannels(rgbColor); + + Assert.assertEquals(ColorUtil.get1stColor(rearrangedColor), green); + Assert.assertEquals(ColorUtil.get2ndColor(rearrangedColor), blue); + Assert.assertEquals(ColorUtil.get3rdColor(rearrangedColor), red); + } + + @Test + public void reorderToBrg(){ + ColorChannelSequence.Sequencer sequencer = ColorChannelSequence.createSequencer(ColorChannelSequence.BRG); + int rgbColor = Color.rgb(red, green, blue); + int rearrangedColor = sequencer.rearrangeColorChannels(rgbColor); + + Assert.assertEquals(ColorUtil.get1stColor(rearrangedColor), blue); + Assert.assertEquals(ColorUtil.get2ndColor(rearrangedColor), red); + Assert.assertEquals(ColorUtil.get3rdColor(rearrangedColor), green); + } + + @Test + public void reorderToBgr(){ + ColorChannelSequence.Sequencer sequencer = ColorChannelSequence.createSequencer(ColorChannelSequence.BGR); + int rgbColor = Color.rgb(red, green, blue); + int rearrangedColor = sequencer.rearrangeColorChannels(rgbColor); + + Assert.assertEquals(ColorUtil.get1stColor(rearrangedColor), blue); + Assert.assertEquals(ColorUtil.get2ndColor(rearrangedColor), green); + Assert.assertEquals(ColorUtil.get3rdColor(rearrangedColor), red); + } + +} diff --git a/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/ColorToBitPatternConverterTest.java b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/ColorToBitPatternConverterTest.java new file mode 100644 index 0000000..bb9ce08 --- /dev/null +++ b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/ColorToBitPatternConverterTest.java @@ -0,0 +1,108 @@ +package com.google.android.things.contrib.driver.ws2812b; + + +import android.graphics.Color; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.util.SparseArray; + +import com.google.android.things.contrib.driver.ws2812b.util.ColorUtil; +import com.google.android.things.contrib.driver.ws2812b.util.ReverseBitPatternConverter; +import com.google.android.things.contrib.driver.ws2812b.util.ColorMock; +import com.google.android.things.contrib.driver.ws2812b.util.SparseArrayMockCreator; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(android.graphics.Color.class) +public class ColorToBitPatternConverterTest { + + private final ReverseBitPatternConverter reverseConverter = new ReverseBitPatternConverter(); + + @Test + public void convertToBitPattern_Rgb() { + ColorMock.mockStatic(); + @ColorInt int randomColor = generateRandomColor(); + ColorToBitPatternConverter converter = createColorToBitPatternConverter(ColorChannelSequence.RGB); + byte[] bitPatterns = converter.convertToBitPattern(new int[]{randomColor}); + int reconstructedColor = reverseConverter.convertBitPatternTo24BitInt(bitPatterns); + Assert.assertEquals(reconstructedColor, randomColor); + } + + @Test + public void convertToBitPattern_Rbg() { + ColorMock.mockStatic(); + @ColorInt int randomColor = generateRandomColor(); + ColorToBitPatternConverter converter = createColorToBitPatternConverter(ColorChannelSequence.RBG); + byte[] bitPatterns = converter.convertToBitPattern(new int[]{randomColor}); + int reconstructedColor = reverseConverter.convertBitPatternTo24BitInt(bitPatterns); + Assert.assertEquals(ColorUtil.get1stColor(reconstructedColor), Color.red(randomColor)); + Assert.assertEquals(ColorUtil.get2ndColor(reconstructedColor), Color.blue(randomColor)); + Assert.assertEquals(ColorUtil.get3rdColor(reconstructedColor), Color.green(randomColor)); + } + + + @Test + public void convertToBitPattern_Grb() { + ColorMock.mockStatic(); + @ColorInt int randomColor = generateRandomColor(); + ColorToBitPatternConverter converter = createColorToBitPatternConverter(ColorChannelSequence.GRB); + byte[] bitPatterns = converter.convertToBitPattern(new int[]{randomColor}); + int reconstructedColor = reverseConverter.convertBitPatternTo24BitInt(bitPatterns); + Assert.assertEquals(ColorUtil.get1stColor(reconstructedColor), Color.green(randomColor)); + Assert.assertEquals(ColorUtil.get2ndColor(reconstructedColor), Color.red(randomColor)); + Assert.assertEquals(ColorUtil.get3rdColor(reconstructedColor), Color.blue(randomColor)); + } + + @Test + public void convertToBitPattern_Gbr() { + ColorMock.mockStatic(); + @ColorInt int randomColor = generateRandomColor(); + ColorToBitPatternConverter converter = createColorToBitPatternConverter(ColorChannelSequence.GBR); + byte[] bitPatterns = converter.convertToBitPattern(new int[]{randomColor}); + int reconstructedColor = reverseConverter.convertBitPatternTo24BitInt(bitPatterns); + Assert.assertEquals(ColorUtil.get1stColor(reconstructedColor), Color.green(randomColor)); + Assert.assertEquals(ColorUtil.get2ndColor(reconstructedColor), Color.blue(randomColor)); + Assert.assertEquals(ColorUtil.get3rdColor(reconstructedColor), Color.red(randomColor)); + } + + @Test + public void convertToBitPattern_Brg() { + ColorMock.mockStatic(); + @ColorInt int randomColor = generateRandomColor(); + ColorToBitPatternConverter converter = createColorToBitPatternConverter(ColorChannelSequence.BRG); + byte[] bitPatterns = converter.convertToBitPattern(new int[]{randomColor}); + int reconstructedColor = reverseConverter.convertBitPatternTo24BitInt(bitPatterns); + Assert.assertEquals(ColorUtil.get1stColor(reconstructedColor), Color.blue(randomColor)); + Assert.assertEquals(ColorUtil.get2ndColor(reconstructedColor), Color.red(randomColor)); + Assert.assertEquals(ColorUtil.get3rdColor(reconstructedColor), Color.green(randomColor)); + } + + @Test + public void convertToBitPattern_Bgr() { + ColorMock.mockStatic(); + @ColorInt int randomColor = generateRandomColor(); + ColorToBitPatternConverter converter = createColorToBitPatternConverter(ColorChannelSequence.BGR); + byte[] bitPatterns = converter.convertToBitPattern(new int[]{randomColor}); + int reconstructedColor = reverseConverter.convertBitPatternTo24BitInt(bitPatterns); + Assert.assertEquals(ColorUtil.get1stColor(reconstructedColor), Color.blue(randomColor)); + Assert.assertEquals(ColorUtil.get2ndColor(reconstructedColor), Color.green(randomColor)); + Assert.assertEquals(ColorUtil.get3rdColor(reconstructedColor), Color.red(randomColor)); + } + + @ColorInt + private int generateRandomColor() { + return Color.rgb((int) (Math.random() * 255), (int) (Math.random() * 255), (int) (Math.random() * 255)); + } + + @NonNull + private ColorToBitPatternConverter createColorToBitPatternConverter(@ColorChannelSequence.Sequence int sequence) { + SparseArray mockedSparseArray = SparseArrayMockCreator.createMockedSparseArray(); + TwelveBitIntToBitPatternMapper bitPatternMapper = new TwelveBitIntToBitPatternMapper(mockedSparseArray); + return new ColorToBitPatternConverter(sequence, bitPatternMapper); + } +} diff --git a/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/TwelveBitIntToBitPatternMapperTest.java b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/TwelveBitIntToBitPatternMapperTest.java new file mode 100644 index 0000000..da84028 --- /dev/null +++ b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/TwelveBitIntToBitPatternMapperTest.java @@ -0,0 +1,37 @@ +package com.google.android.things.contrib.driver.ws2812b; + + +import com.google.android.things.contrib.driver.ws2812b.util.ReverseBitPatternConverter; +import com.google.android.things.contrib.driver.ws2812b.util.SparseArrayMockCreator; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.io.IOException; + + + +@RunWith(PowerMockRunner.class) +public class TwelveBitIntToBitPatternMapperTest { + + private TwelveBitIntToBitPatternMapper twelveBitIntToBitPatternMapper = new TwelveBitIntToBitPatternMapper(SparseArrayMockCreator.createMockedSparseArray()); + + @Test + public void getBitPattern() throws IOException { + ReverseBitPatternConverter reverseConverter = new ReverseBitPatternConverter(); + double limit = Math.pow(2, 12); + for (int i = 0; i < limit; i++) { + byte[] bitPattern = twelveBitIntToBitPatternMapper.getBitPattern(i); + int originalValue = reverseConverter.convertBitPatternTo12BitInt(bitPattern); + Assert.assertEquals(originalValue, i); + } + } + + @Test (expected = IllegalArgumentException.class) + public void getBitPatternIllegalArgumentException() throws IOException { + int limit = (int) Math.pow(2, 12); + twelveBitIntToBitPatternMapper.getBitPattern(limit); + } +} diff --git a/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/Ws2812bTest.java b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/Ws2812bTest.java new file mode 100644 index 0000000..cb763e2 --- /dev/null +++ b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/Ws2812bTest.java @@ -0,0 +1,73 @@ +package com.google.android.things.contrib.driver.ws2812b; + + +import android.support.annotation.NonNull; + +import com.google.android.things.contrib.driver.ws2812b.util.ColorMock; +import com.google.android.things.contrib.driver.ws2812b.util.ColorUtil; +import com.google.android.things.contrib.driver.ws2812b.util.SimpleColorToBitPatternConverter; +import com.google.android.things.contrib.driver.ws2812b.util.SparseArrayMockCreator; +import com.google.android.things.pio.SpiDevice; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.io.IOException; + + +@RunWith(PowerMockRunner.class) +@PrepareForTest(android.graphics.Color.class) +public class Ws2812bTest { + @Mock + private SpiDevice mSpiDevice; + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Rule + public ExpectedException mExpectedException = ExpectedException.none(); + + @Test + public void close() throws IOException { + Ws2812b ws2812b = createWs2812BTestDevice(); + ws2812b.close(); + Mockito.verify(mSpiDevice).close(); + } + + @Test + public void close_safeToCallTwice() throws IOException { + Ws2812b ws2812b = createWs2812BTestDevice(); + ws2812b.close(); + ws2812b.close(); + // Check if the inner SPI device was only closed once + Mockito.verify(mSpiDevice, Mockito.times(1)).close(); + } + + @Test + public void write_randomColors() throws IOException { + ColorMock.mockStatic(); + + int[] randomColors = ColorUtil.generateRandomColors(100); + byte[] bytes = new SimpleColorToBitPatternConverter().convertColorsToBitPattern(randomColors); + + Ws2812b ws2812b = createWs2812BTestDevice(); + ws2812b.write(randomColors); + + Mockito.verify(mSpiDevice).write(bytes, bytes.length); + } + + @NonNull + private Ws2812b createWs2812BTestDevice() throws IOException { + TwelveBitIntToBitPatternMapper patternMapper = new TwelveBitIntToBitPatternMapper(SparseArrayMockCreator.createMockedSparseArray()); + ColorToBitPatternConverter converter = new ColorToBitPatternConverter(ColorChannelSequence.RGB, patternMapper); + return new Ws2812b(mSpiDevice, converter); + } +} diff --git a/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/ColorMock.java b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/ColorMock.java new file mode 100644 index 0000000..caff6df --- /dev/null +++ b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/ColorMock.java @@ -0,0 +1,93 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.things.contrib.driver.ws2812b.util; + +import android.graphics.Color; + +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; + +import static org.mockito.Matchers.anyInt; + +public class ColorMock { + public static void mockStatic() { + PowerMockito.mockStatic(Color.class); + + mockRed(); + mockGreen(); + mockBlue(); + mockRgb(); + mockArgb(); + } + + private static void mockRgb() { + Mockito.when(Color.rgb(anyInt(), anyInt(), anyInt())).thenAnswer(new Answer() { + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + int r = invocation.getArgumentAt(0, Integer.class); + int g = invocation.getArgumentAt(1, Integer.class); + int b = invocation.getArgumentAt(2, Integer.class); + return (r << 16) | (g << 8) | b; + } + }); + } + + private static void mockArgb() { + Mockito.when(Color.argb(anyInt(), anyInt(), anyInt(), anyInt())).thenAnswer(new Answer() { + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + int a = invocation.getArgumentAt(0, Integer.class); + int r = invocation.getArgumentAt(1, Integer.class); + int g = invocation.getArgumentAt(2, Integer.class); + int b = invocation.getArgumentAt(3, Integer.class); + return (a << 24) | (r << 16) | (g << 8) | b; + } + }); + } + + private static void mockBlue() { + Mockito.when(Color.blue(anyInt())).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + int c = invocation.getArgumentAt(0, Integer.class); + return c & 0xff; + } + }); + } + + private static void mockGreen() { + Mockito.when(Color.green(anyInt())).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + int c = invocation.getArgumentAt(0, Integer.class); + return (c >> 8) & 0xff; + } + }); + } + + private static void mockRed() { + Mockito.when(Color.red(anyInt())).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + int c = invocation.getArgumentAt(0, Integer.class); + return (c >> 16) & 0xff; + } + }); + } +} diff --git a/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/ColorUtil.java b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/ColorUtil.java new file mode 100644 index 0000000..3d601e6 --- /dev/null +++ b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/ColorUtil.java @@ -0,0 +1,38 @@ +package com.google.android.things.contrib.driver.ws2812b.util; + + +import android.graphics.Color; +import android.support.annotation.ColorInt; +import android.support.annotation.IntRange; + +public class ColorUtil { + + @SuppressWarnings("SameParameterValue") + public static int []generateRandomColors(int numberOfRandomColors) { + int[] randomColors = new int[numberOfRandomColors]; + for (int i = 0; i < randomColors.length; i++) { + randomColors[i] = generateRandomColorValue(); + } + return randomColors; + } + + @IntRange(from = 0, to = 255) + public static int generateRandomColorValue() { + return (int) Math.round(Math.random() * 255); + } + + public static int get1stColor(@ColorInt int color) + { + return Color.red(color); + } + + public static int get2ndColor(@ColorInt int color) + { + return Color.green(color); + } + + public static int get3rdColor(@ColorInt int color) + { + return Color.blue(color); + } +} diff --git a/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/ReverseBitPatternConverter.java b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/ReverseBitPatternConverter.java new file mode 100644 index 0000000..4bc2e92 --- /dev/null +++ b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/ReverseBitPatternConverter.java @@ -0,0 +1,96 @@ +package com.google.android.things.contrib.driver.ws2812b.util; + + +import android.support.annotation.IntRange; +import android.support.annotation.NonNull; +import android.support.annotation.Size; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.ListIterator; + +public class ReverseBitPatternConverter { + + @IntRange(from = 0, to = (1 << 24) -1) + public int convertBitPatternTo24BitInt(@Size(value = 8) byte[] bitPatterns) { + byte[] firstBitPatterns = new byte[4]; + byte[] secondBitPatterns = new byte[4]; + + System.arraycopy(bitPatterns, 0, firstBitPatterns, 0, 4); + System.arraycopy(bitPatterns, 4, secondBitPatterns, 0, 4); + + int first12BitInt = convertBitPatternTo12BitInt(firstBitPatterns); + int second12BitInt = convertBitPatternTo12BitInt(secondBitPatterns); + + return (first12BitInt << 12) | second12BitInt; + } + + @IntRange(from = 0, to = (1 << 12) -1) + public int convertBitPatternTo12BitInt(@Size(value = 4) byte[] bitPatterns) { + List booleanBitPatterns = convertToBooleanBitPatternWithMissingPauseBit(bitPatterns); + List originalBooleanBits = new ArrayList<>(); + ListIterator booleanBitPatternIterator = booleanBitPatterns.listIterator(); + + checkArraySizeOrThrow(booleanBitPatterns, 12 * 3); + + for (int i = 0; i < 12; i++) { + boolean bit0 = booleanBitPatternIterator.next(); + boolean bit1 = booleanBitPatternIterator.next(); + boolean bit2 = booleanBitPatternIterator.next(); + + originalBooleanBits.add(isOneBitPattern(bit0, bit1, bit2)); + } + return convertTo12BitInt(originalBooleanBits); + } + + @NonNull + @Size(value = 9) + private List convertToBooleanBitPatternWithMissingPauseBit(@Size(value = 4) byte[] bitPatterns) { + List booleanBitPatterns = new ArrayList<>(); + for (byte bitPattern : bitPatterns) { + booleanBitPatterns.addAll(convertBitPatternTo12BitInt(bitPattern)); + // Add missing pause bit + booleanBitPatterns.add(false); + } + return booleanBitPatterns; + } + + private int convertTo12BitInt(@Size(value = 12) List originalBooleanBits) { + int converted = 0; + int highestBit = 1 << 11; + for (int i = 0; i < 12; i++) { + if (originalBooleanBits.get(i)) { + converted |= highestBit; + } + highestBit = highestBit >> 1; + } + return converted; + } + + private Boolean isOneBitPattern(boolean bit0, boolean bit1, boolean bit2) { + if (!bit0) + { + throw new AssertionError("First bit in pattern must always be true"); + } + return bit1 & !bit2; + } + + @Size(value = 8) + private List convertBitPatternTo12BitInt(int bitPattern) + { + List booleanBitPattern = new ArrayList<>(); + int oneByteBitMask = 0b1000_0000; + for (int i = 0; i < 8; i++) { + booleanBitPattern.add((oneByteBitMask & bitPattern) == oneByteBitMask); + bitPattern = bitPattern << 1; + } + return booleanBitPattern; + } + + private static void checkArraySizeOrThrow(Collection collection, int size) { + if (collection.size() != size) { + throw new IllegalArgumentException("Array must have size " + size); + } + } +} diff --git a/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/SimpleColorToBitPatternConverter.java b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/SimpleColorToBitPatternConverter.java new file mode 100644 index 0000000..cb078be --- /dev/null +++ b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/SimpleColorToBitPatternConverter.java @@ -0,0 +1,88 @@ +package com.google.android.things.contrib.driver.ws2812b.util; + + +import android.support.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +public class SimpleColorToBitPatternConverter { + private static final List ONE_BIT_PATTERN = Arrays.asList(true, true, false); + private static final List ZERO_BIT_PATTERN = Arrays.asList(true, false, false); + + public byte[] convertColorsToBitPattern(int [] colors) + { + List booleanBitPatterns = new ArrayList<>(); + for (int color : colors) { + booleanBitPatterns.addAll(constructBooleanBitPatterns(color)); + } + booleanBitPatterns = removePauseBits(booleanBitPatterns); + return convertBitPatternToByteArray(booleanBitPatterns); + } + + @NonNull + private List constructBooleanBitPatterns(int color) { + ArrayList bitPatterns = new ArrayList<>(); + + int highestBit = 1<<23; + + for (int i = 0; i < 24; i++) { + List bitPattern = (color & highestBit) == highestBit ? ONE_BIT_PATTERN : ZERO_BIT_PATTERN; + bitPatterns.addAll(bitPattern); + color = color << 1; + } + return bitPatterns; + } + + private List removePauseBits(List bitPatterns) { + Iterator iterator = bitPatterns.iterator(); + int i = 0; + while (iterator.hasNext()) { + iterator.next(); + if (i == 8){ + iterator.remove(); + i = 0; + continue; + } + i++; + } + return bitPatterns; + } + + private byte[] convertBitPatternToByteArray(List bitPatterns) { + List> eightBitPatterns = splitInEightBitParts(bitPatterns); + byte [] bytes = new byte[eightBitPatterns.size()]; + int i = 0; + + for (List eightBitPattern : eightBitPatterns) { + int currentBitMask = 0b10000000; + byte currentByte = 0; + for (Boolean booleanBit : eightBitPattern) { + if (booleanBit) { + currentByte |= currentBitMask; + } + currentBitMask >>= 1; + } + bytes[i++] = currentByte; + } + return bytes; + } + + @NonNull + private List> splitInEightBitParts(List bitPatterns) { + List> eightBitPatterns = new ArrayList<>(); + int index = 0; + int numberOfBits = bitPatterns.size(); + while (index < numberOfBits && index + 8 <= numberOfBits) { + eightBitPatterns.add(bitPatterns.subList(index, index + 8)); + index += 8; + } + if (index != numberOfBits) { + throw new IllegalStateException("Wrong number of bit pattern size: " + numberOfBits); + } + return eightBitPatterns; + } + +} diff --git a/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/SparseArrayMockCreator.java b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/SparseArrayMockCreator.java new file mode 100644 index 0000000..adffd0c --- /dev/null +++ b/ws2812b/src/test/java/com/google/android/things/contrib/driver/ws2812b/util/SparseArrayMockCreator.java @@ -0,0 +1,36 @@ +package com.google.android.things.contrib.driver.ws2812b.util; + + +import android.util.SparseArray; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.List; + +public class SparseArrayMockCreator { + + public static SparseArray createMockedSparseArray() { + + @SuppressWarnings("unchecked") + SparseArray sparseArray = Mockito.mock(SparseArray.class); + final ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(Integer.class); + final ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(byte[].class); + + Mockito.doNothing().when(sparseArray).append(keyCaptor.capture(), valueCaptor.capture()); + + Mockito.when(sparseArray.get(Mockito.anyInt())).thenAnswer(new Answer() { + @Override + public byte[] answer(InvocationOnMock invocation) throws Throwable { + Integer key = invocation.getArgumentAt(0, Integer.class); + List allKeys = keyCaptor.getAllValues(); + int lastIndexOfKey = allKeys.lastIndexOf(key); + return lastIndexOfKey != -1 ? valueCaptor.getAllValues().get(lastIndexOfKey) : null; + } + }); + + return sparseArray; + } +} diff --git a/ws2812b/ws2812b-bit-pattern-svg-objects.svg b/ws2812b/ws2812b-bit-pattern-svg-objects.svg new file mode 100644 index 0000000..b5e3164 --- /dev/null +++ b/ws2812b/ws2812b-bit-pattern-svg-objects.svg @@ -0,0 +1,543 @@ + + + +image/svg+xml1-pattern = 110 +417 ns +  +  +417 ns +417 ns +834 ns +0 bit +1 bit +1 bit +0-pattern = 100 +417 ns +417 ns +417 ns +834 ns +0 bit +0 bit +1 bit +  + \ No newline at end of file diff --git a/ws2812b/ws2812b-bit-pattern.svg b/ws2812b/ws2812b-bit-pattern.svg new file mode 100644 index 0000000..0bbad4a --- /dev/null +++ b/ws2812b/ws2812b-bit-pattern.svg @@ -0,0 +1,663 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/ws2812b/ws2812b-timings-svg-objects.svg b/ws2812b/ws2812b-timings-svg-objects.svg new file mode 100644 index 0000000..3d2691b --- /dev/null +++ b/ws2812b/ws2812b-timings-svg-objects.svg @@ -0,0 +1,445 @@ + + + +image/svg+xml0-bit +1-bit +400 ns +850 ns +400 ns +  +  +850 ns +high voltage +high voltage +low voltage +low voltage + \ No newline at end of file diff --git a/ws2812b/ws2812b-timings.svg b/ws2812b/ws2812b-timings.svg new file mode 100644 index 0000000..09f1521 --- /dev/null +++ b/ws2812b/ws2812b-timings.svg @@ -0,0 +1,559 @@ + + + +image/svg+xml \ No newline at end of file