In Part 1 of this series, we talked about the hardware for our Purr Programming t-shirt and how we connected all of the components. Now let’s talk about the Android app that controls the shirt’s LEDs.
Our app has the ability to:
- Give the user a series of buttons that allows him or her to flash a few different patterns on the NeoPixels, along with flashing the swirls.
- Flash the pixels and swirls in distinct patterns if someone tweets at Twitter accounts that are registered to receive notifications on the phone.
- Allow the user to use voice control on an Android Wear watch in order to manually launch the patterns when triggered by a tweet.
Because we want to change LED patterns based on the particular Twitter account targeted, we wanted to handle MetaWear commands in a service so that the app doesn’t need to be in the foreground in order to respond to new Twitter notifications. We also decided to use Kotlin instead of Java. If you’re interested in learning more about Kotlin, this is a great resource.
Setting up the Service
In the AndroidManifest.xml we need to set up a service along with the MetaWear BLE service.
<service android:name="com.mbientlab.metawear.MetaWearBleService" />
<service
android:name=".PurrProgrammingNotificationListenerService"
android:label="notifications"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
In this instance, we’re calling the service PurrProgrammingNotificationListenerService and it will override NotificationListenerService and ServiceConnection.
class PurrProgrammingNotificationListenerService : NotificationListenerService(), ServiceConnection {
}
Our service is going to register a broadcast receiver that will receive messages from our app activity and smart watch.
internal inner class NLServiceReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.getStringExtra("command").equals("clearall")) {
}
}
Instead of binding the MetaWear BLE service in Activity, we do it in onCreate for the service.
override fun onCreate() {
super.onCreate();
nlservicereciver = NLServiceReceiver();
val filter: IntentFilter = IntentFilter();
filter.addAction(SERVICE_NOTIFICATION);
registerReceiver(nlservicereciver, filter);
bindService(Intent(this, MetaWearBleService::class.java),
this, Context.BIND_AUTO_CREATE)
}
Because this is a one-time event, we have hard coded the MAC address and immediately try to connect to the board once the service is bound. With our connection handler, we use logic to automatically reconnect the board if it gets disconnected. If this was a production application, we would add some logic to only connect and stay connected when actively sending commands to the board. That said, this is not efficient, as we found during our tests that, anecdotally, the battery life on a Nexus 5 took about a 20% hit.
private val connectionStateHandler = object : MetaWearBoard.ConnectionStateHandler() {
override fun failure(status: Int, error: Throwable?) {
super.failure(status, error)
}
override fun disconnected() {
mwBoard!!.connect()
Log.i("Purr Programming Service", "disconnected")
}
override fun connected() {
Log.i("Purr Programming Service", "connected")
if (!connected) {
Log.i("Main Activity", "Initializing neoPixels")
connected = true
}
val result = mwBoard!!.readRssi()
result.onComplete(object : AsyncOperation.CompletionHandler() {
override fun success(rssi: Int) {
Log.i("RSSI is ", Integer.toString(rssi))
}
})
}
}
Flashing the Swirls
The swirling LEDs are controlled individually via FETS that are connected to GPIO pins 1 and 2. These are turned on by setting the pin and turned off by clearing it.
private fun turnOnSwirl(pin: Int) {
gpIO.setDigitalOut(pin.toByte())
}
private fun turnOffSwirl(pin: Int) {
gpIO.clearDigitalOut(pin.toByte())
}
Setting Up the NeoPixels
First, we need to initialize the strand. In the app, the user needs to manually trigger this.
npModule = mwBoard!!.getModule(NeoPixel::class.java)
npModule.initializeStrand(STRAND, NeoPixel.ColorOrdering.MW_WS2811_GRB,
NeoPixel.StrandSpeed.SLOW, GPIO_PIN, 8)
Once the strand is initialized, pixels are set with the setPixel command.
npModule.setPixel(STRAND, (pixels[index] + 1).toByte(),
colors[index]!!.get(RED)!!,
colors[index]!!.get(GREEN)!!,
colors[index]!!.get(BLUE)!!)
We then loop through the pixels that we want to set in a method.
private fun setRubyScreen(colorIndex: Int) {
try {
val color: Map<String, Byte> = computerScreenRuby[colorIndex]!!
for (pixel in 2..5) {
npModule.setPixel(STRAND, pixel.toByte(),
color[RED]!!,
color[GREEN]!!,
color[BLUE]!!)
}
} catch(e: Exception) {
Log.i("problem with ", e.toString())
}
}
Next, we call these from a timer to get the flashing effects.
private fun startRubyScreen() {
currentWorkFlow = STATE.RUBY_SCREEN
object : CountDownTimer(30000, 2000) {
override public fun onTick(millisUntilFinished: Long) {
setRubyScreen((rubyIndex++).mod(3))
setCatEye(1, rubyIndex.mod(5))
setPaw(rubyIndex.mod(2))
}
override public fun onFinish() {
flashSwirls()
}
}.start();
}
The main app is then wired in by sending a broadcast intent. The intent is picked up by the service, which, in turn, executes the requested light pattern on the shirt.
val i: Intent = Intent(SERVICE_NOTIFICATION)
i.putExtra("command", "eyes")
sendBroadcast(i)
The source code for our working application can be found over here. Stay tuned for the next post where we’ll talk about how to listen to notifications in the service.