├── .gitignore ├── README.md ├── build.gradle ├── demo-app ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── xavirigau │ │ └── ledcontroller │ │ ├── BoardDefaults.java │ │ ├── DiyLedActivity.java │ │ ├── RainbowLedActivity.java │ │ └── SimpleLedActivity.java │ └── res │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ └── strings.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── rpi3-schematics.png ├── settings.gradle └── ws2801-driver ├── .android-things-driver.json ├── build.gradle ├── proguard-rules.pro └── src ├── main ├── AndroidManifest.xml └── java │ └── com │ └── xrigau │ └── driver │ └── ws2801 │ ├── ColorUnpacker.java │ └── Ws2801.java └── test └── java └── com └── xrigau └── driver └── ws2801 ├── ColorUnpackerTest.java └── Ws2801Test.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | local.properties 4 | .idea/ 5 | .DS_Store 6 | build/ 7 | captures/ 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # androidthings-ws2801-driver [ ![Download](https://api.bintray.com/packages/xrigau/maven/ws2801-driver/images/download.svg) ](https://bintray.com/xrigau/maven/ws2801-driver/_latestVersion) 2 | 3 | This repo contains the Android Things driver to control an LED strip based on the WS2801 chip (and should work on similar LED strips as long as the protocol is the same). 4 | 5 | The driver can be found in the `ws2801-driver` module and there's a demo app showing how to use the driver in the `demo-app` module. 6 | 7 | Only tested with a Raspberry Pi 3 but should work with other devices too. 8 | 9 | ## Schematics 10 | 11 | ![raspberry pi 3 schematics](rpi3-schematics.png) 12 | 13 | 14 | ## Using 15 | 16 | The driver is now available on jCenter so you can include it as a gradle dependency: 17 | 18 | ```groovy 19 | dependencies { 20 | compile 'com.xrigau:ws2801-driver:1.0.0' 21 | } 22 | ``` 23 | 24 | And you can start using it in your Android Things project: 25 | 26 | ```java 27 | Ws2801 ledstrip = Ws2801.create(SPI_DEVICE_NAME, Ws2801.Mode.RGB); 28 | ledstrip.write(new int[]{Color.parseColor("#0face0")}); 29 | 30 | // Later on 31 | ledstrip.close(); 32 | ``` 33 | More on the WS2801: https://cdn-shop.adafruit.com/datasheets/WS2801.pdf 34 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:2.2.1' 7 | classpath 'com.novoda:bintray-release:0.4.0' 8 | } 9 | } 10 | 11 | allprojects { 12 | repositories { 13 | jcenter() 14 | } 15 | } 16 | 17 | task clean(type: Delete) { 18 | delete rootProject.buildDir 19 | } 20 | -------------------------------------------------------------------------------- /demo-app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | 7 | defaultConfig { 8 | applicationId "com.xavirigau.ledcontroller" 9 | minSdkVersion 24 10 | targetSdkVersion 25 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | provided 'com.google.android.things:androidthings:0.1-devpreview' 24 | compile project(':ws2801-driver') 25 | } 26 | -------------------------------------------------------------------------------- /demo-app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.4.1_1/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /demo-app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /demo-app/src/main/java/com/xavirigau/ledcontroller/BoardDefaults.java: -------------------------------------------------------------------------------- 1 | package com.xavirigau.ledcontroller; 2 | 3 | import android.os.Build; 4 | 5 | @SuppressWarnings("WeakerAccess") 6 | public class BoardDefaults { 7 | private static final String DEVICE_EDISON = "edison"; 8 | private static final String DEVICE_RPI3 = "rpi3"; 9 | private static final String DEVICE_NXP = "imx6ul"; 10 | /** 11 | * Return the preferred I2C port for each board. 12 | */ 13 | public static String getSPIPort() { 14 | switch (Build.DEVICE) { 15 | // same for Edison Arduino breakout and Edison SOM 16 | case DEVICE_EDISON: 17 | return "SPI2"; 18 | case DEVICE_RPI3: 19 | return "SPI0.0"; 20 | case DEVICE_NXP: 21 | return "SPI3_0"; 22 | default: 23 | throw new IllegalStateException("Unknown Build.DEVICE " + Build.DEVICE); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demo-app/src/main/java/com/xavirigau/ledcontroller/DiyLedActivity.java: -------------------------------------------------------------------------------- 1 | package com.xavirigau.ledcontroller; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | 7 | import com.google.android.things.pio.PeripheralManagerService; 8 | import com.google.android.things.pio.SpiDevice; 9 | 10 | import java.io.IOException; 11 | 12 | // Works with LED strip WS2801 13 | public class DiyLedActivity extends Activity { 14 | 15 | private static final String TAG = DiyLedActivity.class.getSimpleName(); 16 | 17 | private SpiDevice mDevice; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | 23 | try { 24 | PeripheralManagerService manager = new PeripheralManagerService(); 25 | mDevice = manager.openSpiDevice(BoardDefaults.getSPIPort()); 26 | configureSpiDevice(mDevice); 27 | byte[] b = {10/*R*/, 100 /*B*/, 40/*G*/}; 28 | sendCommand(mDevice, b); 29 | } catch (IOException e) { 30 | Log.w(TAG, "Unable to access SPI device", e); 31 | } 32 | } 33 | 34 | public void configureSpiDevice(SpiDevice device) throws IOException { 35 | device.setMode(SpiDevice.MODE2); 36 | device.setFrequency(1000000); 37 | device.setBitsPerWord(8); 38 | } 39 | 40 | // Half-duplex data transfer 41 | public void sendCommand(SpiDevice device, byte[] buffer) throws IOException { 42 | device.write(buffer, buffer.length); 43 | } 44 | 45 | @Override 46 | protected void onDestroy() { 47 | if (mDevice != null) { 48 | try { 49 | mDevice.close(); 50 | mDevice = null; 51 | } catch (IOException e) { 52 | Log.w(TAG, "Unable to close SPI device", e); 53 | } 54 | } 55 | 56 | super.onDestroy(); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /demo-app/src/main/java/com/xavirigau/ledcontroller/RainbowLedActivity.java: -------------------------------------------------------------------------------- 1 | package com.xavirigau.ledcontroller; 2 | 3 | import com.xrigau.driver.ws2801.Ws2801; 4 | 5 | import android.app.Activity; 6 | import android.graphics.Color; 7 | import android.os.Bundle; 8 | import android.os.Handler; 9 | import android.os.HandlerThread; 10 | import android.util.Log; 11 | 12 | import java.io.IOException; 13 | import java.util.Arrays; 14 | 15 | public class RainbowLedActivity extends Activity { 16 | 17 | private static final String TAG = RainbowLedActivity.class.getSimpleName(); 18 | private static final int FRAME_DELAY_MS = 15; 19 | private static final int NUM_LEDS = 1; 20 | 21 | private final int[] mLedColors = new int[NUM_LEDS]; 22 | 23 | private Ws2801 mLedstrip; 24 | private HandlerThread mPioThread; 25 | private Handler mHandler; 26 | 27 | private float hue = 0.0f; 28 | private float increment = 0.002f; 29 | 30 | @Override 31 | public void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | 34 | mPioThread = new HandlerThread("pioThread"); 35 | mPioThread.start(); 36 | mHandler = new Handler(mPioThread.getLooper()); 37 | 38 | try { 39 | Log.d(TAG, "Initializing LED strip"); 40 | mLedstrip = Ws2801.create(BoardDefaults.getSPIPort(), Ws2801.Mode.RBG); 41 | mHandler.post(mAnimateRunnable); 42 | // mLedstrip.write(new int[]{Color.BLACK}); // Uncomment this line and comment the previous one to turn off 43 | } catch (IOException e) { 44 | Log.e(TAG, "Error initializing LED strip", e); 45 | } 46 | } 47 | 48 | private Runnable mAnimateRunnable = new Runnable() { 49 | @Override 50 | public void run() { 51 | try { 52 | Arrays.fill(mLedColors, hue2Rgb()); // all LEDs will have the same color 53 | mLedstrip.write(mLedColors); 54 | 55 | hue += increment; 56 | if (hue >= 1.0f || hue <= 0.0f) { 57 | hue = Math.max(0.0f, Math.min(hue, 1.0f)); 58 | increment = -increment; 59 | } 60 | } catch (IOException e) { 61 | Log.e(TAG, "Error while writing to LED strip", e); 62 | } 63 | mHandler.postDelayed(mAnimateRunnable, FRAME_DELAY_MS); 64 | } 65 | }; 66 | 67 | private int hue2Rgb() { 68 | float l = 0.5f; // luminosity? 69 | float s = 0.8f; // saturation? 70 | 71 | float q = l < 0.5f ? l * (1 + s) : l + s - l * s; 72 | float p = 2f * l - q; 73 | 74 | int r = (int) (convert(p, q, hue + (1 / 3f)) * 255); 75 | int g = (int) (convert(p, q, hue) * 255); 76 | int b = (int) (convert(p, q, hue - (1 / 3f)) * 255); 77 | 78 | return Color.rgb(r, g, b); 79 | } 80 | 81 | private float convert(float p, float q, float t) { 82 | if (t < 0) t += 1f; 83 | if (t > 1) t -= 1f; 84 | if (t < 1 / 6f) return p + (q - p) * 6f * t; 85 | if (t < 1 / 2f) return q; 86 | if (t < 2 / 3f) return p + (q - p) * (2 / 3f - t) * 6f; 87 | return p; 88 | } 89 | 90 | @Override 91 | public void onDestroy() { 92 | mHandler.removeCallbacks(mAnimateRunnable); 93 | mPioThread.quitSafely(); 94 | 95 | try { 96 | mLedColors[0] = Color.BLACK; 97 | mLedstrip.write(mLedColors); 98 | mLedstrip.close(); 99 | } catch (IOException e) { 100 | Log.e(TAG, "Exception closing LED strip", e); 101 | } 102 | super.onDestroy(); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /demo-app/src/main/java/com/xavirigau/ledcontroller/SimpleLedActivity.java: -------------------------------------------------------------------------------- 1 | package com.xavirigau.ledcontroller; 2 | 3 | import android.app.Activity; 4 | import android.graphics.Color; 5 | import android.os.Bundle; 6 | import android.util.Log; 7 | 8 | import com.xrigau.driver.ws2801.Ws2801; 9 | 10 | import java.io.IOException; 11 | 12 | public class SimpleLedActivity extends Activity { 13 | 14 | private static final String TAG = SimpleLedActivity.class.getSimpleName(); 15 | 16 | private Ws2801 mLedstrip; 17 | 18 | @Override 19 | public void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | 22 | try { 23 | mLedstrip = Ws2801.create(BoardDefaults.getSPIPort(), Ws2801.Mode.RGB); 24 | mLedstrip.write(new int[]{Color.parseColor("#0face0")}); 25 | Log.d(TAG, "Done!"); 26 | } catch (IOException e) { 27 | Log.e(TAG, "Error initializing LED strip", e); 28 | } 29 | } 30 | 31 | @Override 32 | public void onDestroy() { 33 | try { 34 | mLedstrip.close(); 35 | } catch (IOException e) { 36 | Log.e(TAG, "Exception closing LED strip", e); 37 | } 38 | super.onDestroy(); 39 | } 40 | 41 | } 42 | 43 | -------------------------------------------------------------------------------- /demo-app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xrigau/androidthings-ws2801-driver/b867e5dbb49d596e8b57c26576ab2dfc0f4923c5/demo-app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo-app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xrigau/androidthings-ws2801-driver/b867e5dbb49d596e8b57c26576ab2dfc0f4923c5/demo-app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo-app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xrigau/androidthings-ws2801-driver/b867e5dbb49d596e8b57c26576ab2dfc0f4923c5/demo-app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo-app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xrigau/androidthings-ws2801-driver/b867e5dbb49d596e8b57c26576ab2dfc0f4923c5/demo-app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xrigau/androidthings-ws2801-driver/b867e5dbb49d596e8b57c26576ab2dfc0f4923c5/demo-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo-app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | LedController 3 | 4 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xrigau/androidthings-ws2801-driver/b867e5dbb49d596e8b57c26576ab2dfc0f4923c5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Dec 21 23:48:55 CET 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.2-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /rpi3-schematics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xrigau/androidthings-ws2801-driver/b867e5dbb49d596e8b57c26576ab2dfc0f4923c5/rpi3-schematics.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':demo-app', ':ws2801-driver' 2 | -------------------------------------------------------------------------------- /ws2801-driver/.android-things-driver.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "ws2801-driver", 3 | "category": "driver", 4 | "samples": [ 5 | "https://github.com/xrigau/androidthings-ws2801-driver/tree/master/demo-app" 6 | ], 7 | "published-maven": { 8 | "maven-url": "https://bintray.com/xrigau/maven/ws2801-driver/1.0.0", 9 | "groupid": "com.xrigau", 10 | "artifactid": "ws2801-driver" 11 | }, 12 | "compatible-with": [ 13 | { 14 | "title": "WS2801-based LED strip", 15 | "photo": "https://cdn.sparkfun.com//assets/parts/4/7/1/9/10312-00.jpg", 16 | "url": "https://www.adafruit.com/product/322" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /ws2801-driver/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.novoda.bintray-release' // must be applied after your artifact generating plugin (eg. java / com.android.library) 3 | 4 | android { 5 | compileSdkVersion 27 6 | buildToolsVersion "27.0.2" 7 | 8 | defaultConfig { 9 | minSdkVersion 24 10 | targetSdkVersion 27 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | provided 'com.google.android.things:androidthings:1.0' 27 | 28 | testCompile 'junit:junit:4.12' 29 | testCompile "org.mockito:mockito-core:1.10.19" 30 | } 31 | 32 | publish { 33 | userOrg = 'xrigau' 34 | groupId = 'com.xrigau' 35 | artifactId = 'ws2801-driver' 36 | publishVersion = '1.0.0' 37 | desc = 'Android Things driver for the WS2801 RGB LED strsip.' 38 | website = 'https://github.com/xrigau/androidthings-ws2801-driver' 39 | } 40 | -------------------------------------------------------------------------------- /ws2801-driver/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.4.1_1/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /ws2801-driver/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ws2801-driver/src/main/java/com/xrigau/driver/ws2801/ColorUnpacker.java: -------------------------------------------------------------------------------- 1 | package com.xrigau.driver.ws2801; 2 | 3 | import com.xrigau.driver.ws2801.Ws2801.Mode; 4 | 5 | import android.graphics.Color; 6 | 7 | class ColorUnpacker { 8 | 9 | private final Mode ledMode; 10 | 11 | ColorUnpacker(Mode ledMode) { 12 | this.ledMode = ledMode; 13 | } 14 | 15 | /** 16 | * Returns an WS2801 packet corresponding to the current brightness and given {@link Color}. 17 | * 18 | * @param color The {@link Color} to retrieve the protocol packet for. 19 | * @return WS2801 packet corresponding to the current brightness and given {@link Color}. 20 | */ 21 | byte[] unpack(int color) { 22 | int r = Color.red(color); 23 | int g = Color.green(color); 24 | int b = Color.blue(color); 25 | return getOrderedRgbBytes(ledMode, (byte) r, (byte) g, (byte) b); 26 | } 27 | 28 | static byte[] getOrderedRgbBytes(Mode ledMode, byte r, byte g, byte b) { 29 | switch (ledMode) { 30 | case RGB: 31 | return new byte[]{r, g, b}; 32 | case RBG: 33 | return new byte[]{r, b, g}; 34 | case BGR: 35 | return new byte[]{b, g, r}; 36 | case BRG: 37 | return new byte[]{b, r, g}; 38 | case GRB: 39 | return new byte[]{g, r, b}; 40 | case GBR: 41 | return new byte[]{g, b, r}; 42 | default: 43 | throw new IllegalArgumentException(ledMode.name() + " is an unknown " + Mode.class.getSimpleName()); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /ws2801-driver/src/main/java/com/xrigau/driver/ws2801/Ws2801.java: -------------------------------------------------------------------------------- 1 | package com.xrigau.driver.ws2801; 2 | 3 | import android.graphics.Color; 4 | 5 | import com.google.android.things.pio.PeripheralManager; 6 | import com.google.android.things.pio.SpiDevice; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * Device driver for WS2801 RGB LEDs using 2-wire SPI. 12 | *

13 | * For more information on SPI, see: 14 | * https://en.wikipedia.org/wiki/Serial_Peripheral_Interface_Bus 15 | * For information on the WS2801 protocol, see: 16 | * https://cdn-shop.adafruit.com/datasheets/WS2801.pdf 17 | */ 18 | public class Ws2801 implements AutoCloseable { 19 | 20 | // Device SPI Configuration constants 21 | private static final int SPI_BPW = 8; // Bits per word 22 | private static final int SPI_FREQUENCY = 1_000_000; 23 | private static final int SPI_MODE = SpiDevice.MODE0; // Mode 0 seems to work best for WS2801 24 | private static final int WS2801_PACKET_LENGTH = 3; // R, G & B in any order 25 | 26 | /** 27 | * Color ordering for the RGB LED messages; the most common modes are BGR and RGB. 28 | */ 29 | public enum Mode { 30 | RGB, 31 | RBG, 32 | GRB, 33 | GBR, 34 | BRG, 35 | BGR 36 | } 37 | 38 | public enum Direction { 39 | NORMAL, 40 | REVERSED, 41 | } 42 | 43 | private final SpiDevice device; 44 | private final ColorUnpacker colorUnpacker; 45 | // Direction of the led strip; 46 | private final Direction direction; 47 | 48 | /** 49 | * Create a new Ws2801 driver. 50 | * 51 | * @param spiBusPort Name of the SPI bus 52 | */ 53 | public static Ws2801 create(String spiBusPort) throws IOException { 54 | return create(spiBusPort, Mode.RGB); 55 | } 56 | 57 | /** 58 | * Create a new Ws2801 driver. 59 | * 60 | * @param spiBusPort Name of the SPI bus 61 | * @param ledMode The {@link Mode} indicating the red/green/blue byte ordering for the device. 62 | */ 63 | public static Ws2801 create(String spiBusPort, Mode ledMode) throws IOException { 64 | return create(spiBusPort, ledMode, Direction.NORMAL); 65 | } 66 | 67 | /** 68 | * Create a new Ws2801 driver. 69 | * 70 | * @param spiBusPort Name of the SPI bus 71 | * @param ledMode The {@link Mode} indicating the red/green/blue byte ordering for the device. 72 | * @param direction The {@link Direction} or the LED strip. 73 | */ 74 | public static Ws2801 create(String spiBusPort, Mode ledMode, Direction direction) throws IOException { 75 | PeripheralManager pioService = PeripheralManager.getInstance(); 76 | try { 77 | return new Ws2801(pioService.openSpiDevice(spiBusPort), new ColorUnpacker(ledMode), direction); 78 | } catch (IOException e) { 79 | throw new IOException("Unable to open SPI device in bus port " + spiBusPort, e); 80 | } 81 | } 82 | 83 | Ws2801(SpiDevice device, ColorUnpacker colorUnpacker, Direction direction) throws IOException { 84 | this.device = device; 85 | this.colorUnpacker = colorUnpacker; 86 | this.direction = direction; 87 | configure(device); 88 | } 89 | 90 | private static void configure(SpiDevice device) throws IOException { 91 | device.setFrequency(SPI_FREQUENCY); 92 | device.setMode(SPI_MODE); 93 | device.setBitsPerWord(SPI_BPW); 94 | } 95 | 96 | /** 97 | * Writes the current RGB LED data to the peripheral bus. 98 | * 99 | * @param colors An array of integers corresponding to a {@link Color}. 100 | * The size of the array should match the number of LEDs in the strip. 101 | * @throws IOException if writing to the SPI interface fails. 102 | */ 103 | public void write(int[] colors) throws IOException { 104 | byte[] ledData = new byte[WS2801_PACKET_LENGTH * colors.length]; 105 | 106 | for (int i = 0; i < colors.length; i++) { 107 | int outputPosition = i * WS2801_PACKET_LENGTH; 108 | int di = direction == Direction.NORMAL ? i : colors.length - i - 1; 109 | System.arraycopy(colorUnpacker.unpack(colors[di]), 0, ledData, outputPosition, WS2801_PACKET_LENGTH); 110 | } 111 | 112 | device.write(ledData, ledData.length); 113 | } 114 | 115 | /** 116 | * Releases the SPI interface. 117 | */ 118 | @Override 119 | public void close() throws IOException { 120 | device.close(); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /ws2801-driver/src/test/java/com/xrigau/driver/ws2801/ColorUnpackerTest.java: -------------------------------------------------------------------------------- 1 | package com.xrigau.driver.ws2801; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | public class ColorUnpackerTest { 8 | 9 | private static final byte R = (byte) 111; 10 | private static final byte G = (byte) 222; 11 | private static final byte B = (byte) 333; 12 | 13 | @Test 14 | public void orderedBytesWhenModeIsRBG() { 15 | Ws2801.Mode mode = Ws2801.Mode.RBG; 16 | 17 | byte[] result = ColorUnpacker.getOrderedRgbBytes(mode, R, G, B); 18 | 19 | assertBytesOrder(result, R, B, G); 20 | } 21 | 22 | @Test 23 | public void orderedBytesWhenModeIsBGR() { 24 | Ws2801.Mode mode = Ws2801.Mode.BGR; 25 | 26 | byte[] result = ColorUnpacker.getOrderedRgbBytes(mode, R, G, B); 27 | 28 | assertBytesOrder(result, B, G, R); 29 | } 30 | 31 | @Test 32 | public void orderedBytesWhenModeIsBRG() { 33 | Ws2801.Mode mode = Ws2801.Mode.BRG; 34 | 35 | byte[] result = ColorUnpacker.getOrderedRgbBytes(mode, R, G, B); 36 | 37 | assertBytesOrder(result, B, R, G); 38 | } 39 | 40 | @Test 41 | public void orderedBytesWhenModeIsGRB() { 42 | Ws2801.Mode mode = Ws2801.Mode.GRB; 43 | 44 | byte[] result = ColorUnpacker.getOrderedRgbBytes(mode, R, G, B); 45 | 46 | assertBytesOrder(result, G, R, B); 47 | } 48 | 49 | @Test 50 | public void orderedBytesWhenModeIsGBR() { 51 | Ws2801.Mode mode = Ws2801.Mode.GBR; 52 | 53 | byte[] result = ColorUnpacker.getOrderedRgbBytes(mode, R, G, B); 54 | 55 | assertBytesOrder(result, G, B, R); 56 | } 57 | 58 | private void assertBytesOrder(byte[] bytes, byte... order) { 59 | assertEquals(order[0], bytes[0]); 60 | assertEquals(order[1], bytes[1]); 61 | assertEquals(order[2], bytes[2]); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /ws2801-driver/src/test/java/com/xrigau/driver/ws2801/Ws2801Test.java: -------------------------------------------------------------------------------- 1 | package com.xrigau.driver.ws2801; 2 | 3 | import static org.mockito.Matchers.any; 4 | import static org.mockito.Matchers.anyInt; 5 | import static org.mockito.Mockito.verify; 6 | import static org.mockito.Mockito.when; 7 | 8 | import com.google.android.things.pio.SpiDevice; 9 | import com.xrigau.driver.ws2801.Ws2801.Direction; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mock; 14 | import org.mockito.runners.MockitoJUnitRunner; 15 | 16 | import android.graphics.Color; 17 | 18 | import java.io.IOException; 19 | 20 | @RunWith(MockitoJUnitRunner.class) 21 | public class Ws2801Test { 22 | 23 | private static final byte[] ANY_RGB = {0, 1, 42}; 24 | 25 | @Mock 26 | private SpiDevice device; 27 | @Mock 28 | private ColorUnpacker unpacker; 29 | 30 | private Ws2801 driver; 31 | 32 | @Before 33 | public void setUp() throws IOException { 34 | driver = new Ws2801(device, unpacker, Direction.NORMAL); 35 | when(unpacker.unpack(anyInt())).thenReturn(ANY_RGB); 36 | } 37 | 38 | @Test 39 | public void configures1MHzClockFrequencyWhenCreated() throws Exception { 40 | verify(device).setFrequency(1_000_000); 41 | } 42 | 43 | @Test 44 | public void configuresClockToTransmitOnLeadingEdgeModeWhenCreated() throws Exception { 45 | verify(device).setMode(SpiDevice.MODE0); 46 | } 47 | 48 | @Test 49 | public void configuresBusToSend8BitsPerColorComponentWhenCreated() throws Exception { 50 | verify(device).setBitsPerWord(8); 51 | } 52 | 53 | @Test 54 | public void writesToSpiDeviceWhenWriting() throws Exception { 55 | int[] anyColors = {Color.RED, Color.DKGRAY, Color.GREEN, Color.WHITE, Color.YELLOW}; 56 | driver.write(anyColors); 57 | 58 | verify(device).write(any(byte[].class), anyInt()); 59 | } 60 | 61 | @Test 62 | public void reversesColorsWhenUsingReversedDirection() throws Exception { 63 | driver = new Ws2801(device, unpacker, Direction.REVERSED); 64 | 65 | driver.write(new int[]{0x0, 0x1}); 66 | 67 | verify(unpacker).unpack(0x1); 68 | verify(unpacker).unpack(0X0); 69 | } 70 | 71 | @Test 72 | public void doesNotReverseColorsWhenUsingNormalDirection() throws Exception { 73 | driver = new Ws2801(device, unpacker, Direction.NORMAL); 74 | 75 | driver.write(new int[]{0x0, 0x1}); 76 | 77 | verify(unpacker).unpack(0x0); 78 | verify(unpacker).unpack(0x1); 79 | } 80 | 81 | @Test 82 | public void closesSpiDeviceWhenClosing() throws Exception { 83 | driver.close(); 84 | 85 | verify(device).close(); 86 | } 87 | 88 | } 89 | --------------------------------------------------------------------------------