Panda : Android Wear Controlling OSX Machine
What is Panda?
Panda is short for "Panel d'administration" (French : Admin Panel). This is an ongoing project of mine to export as much admin functionality as possible via a REST API on my personal server. As part of this project, I decided to write an Android Wear component that can interface with the API.
At the time of this writing, I have implemented full music control (play/pause, search, volume and control) as well as a trigger for having my calendar read out loud to me via Apple's TTS engine. However, this project serves more as a foundation to allow arbitrary commands to be executed on remote devices - with time, it can be expanded to fully control the computer for hands free operation.
Android Component
First, I had to get Android Studio and set up bluetooth debugging on my Moto 360. Once the basics were taken care of, I created a new project, making sure to create both a mobile and wear package in the project structure.
The watch app simply has a button that launches a voice recognition activity, and then parses the resulting text for known commands. If a command is matched, it send a message to the mobile component with the URL of the resource, as well as any parameters encoded in a JSON. It's important to note that all communications must go through the phone - as I found out, it's possible to include Ion (an async json java client) in the wear app, but it does little good since there is no native networking capability on the Moto 360. As such, I had to make an activity in the mobile app that handles network communications.
The mobile component of the app has several important activities. Upon boot, a broadcast reciever launches a persistent service that is paired to the watch and listend for messages with a specific identification string. If a message falling under that category is intercepted, the payload is decoded and the phone performs an API call to the server.
I chose to have the message reciever run as a service because I do not with to have the mobile app open in order to have my watch commands work, and I added the boot detector because my phone often reboots during the day. You may wish to modify this to suit your needs.
Server Component
I've already covered Panda in another post, but in short, it's a REST API using Restler as the engine, with various shell scripts doing the heavy lifting.
For the purposes of this project, I createds several new endponts:
- /script/music/play (takes a json: {"query" : "search item"})
- /script/music/pause
- /script/music/resume
- /script/music/volume_up
- /script/music/volume_down
- /script/music/volume_set (takes a json: {"value" : "integer"})
Once an API request is received, I decode any auxilary data and pass it along to a shell script.
PHP Code:
private $CMD_PREFIX = 'sudo -u ivan ';
private $MUSIC_SCRIPT = '~/bin/carbon-control/music.sh';
/**
* @url POST /script/music/play
*/
function playSong($request_data) {
$query = $request_data['query'];
$output = array();
exec($this->CMD_PREFIX . $this->MUSIC_SCRIPT . ' play ' . $query , $output);
return json_encode($output);
}
Shell script:
#!/bin/bash
ARGS=${*:1}
echo "arguments to music script: $ARGS"
ssh -t vania@carbon -T <<EOF
osascript bin/panda-scripts/music.scpt $ARGS
EOF
Now, it's important to note that having shell scripts run by the www-data user, especially scripts that SSH, is quite dangerous. What I've done is set up my sudoer's file to ONLY allow the www-data user to run this one shell script, and not ask for password when using the -u ivan
switch. Since I've set up a SSH key, this allows me to have this script log in without a password prompt.
Desktop Component
The server shell script triggers an applescript, which controls iTunes and the system volume. Here is some sample code, with the full code on my github.
on run argv
set command to item 1 of argv
if argv is missing value then
error 2 -- command line args not passed in.
end if
if command is equal to "next" then
tell application "iTunes"
next track
end tell
end if
if command is equal to "play" then
if (length of argv) is equal to 2 then
set query to item 2 of argv
else
set query to my joinAList((items 2 through (length of argv) of argv), " ")
end if
tell application "iTunes"
set thematches to search library playlist 1 for query
set song to some item of thematches
play song
end tell
end if
end run
You'll notice that I use a run
block to assign a reference to the command line arguments, and then a custom function at the bottom to split and combine lists of args into string.