Pebble voice control

2016-06-18 08.14.03I bought a Pebble Time Steel a few weeks ago when the price dropped, and have just started looking at creating my own apps and watch faces for it.

The CloudPebble site makes it very easy to develop apps for the Pebble.

The part of the app that runs on the watch is written in C, and the part that runs on the phone is in Javascript. The app is seamlessly installed to both, and the debugging features are good.

So, to make voice control of my home automation system work, I modified a simple voice transcription app and made it send the command to node-RED and then show the command and response on the watch.

I already had a node-RED html flow that executes my house control commands and returns the reply.

Whether the app is practical is debatable. From having a watch face displayed, the sequence of actions is:

  1. Press the Select button to open the app list
  2. Scroll down to the voice control app
  3. Press Select to open the app
  4. Press Select to listen
  5. Speak the command
  6. Press select to stop listening and review the voice transcription
  7. Press Select to execute it, if it was OK, or Back (to step 5) if not
  8. Look at the reply on the watch
  9. Press the Back button to go back to the watch face

The voice transcription seems pretty good, so I don’t often have to repeat steps 5 to 7.

Here is the C program that runs on the watch:

#include <pebble.h>

static Window *s_main_window;
static TextLayer *s_output_layer;
static DictationSession *s_dictation_session;
static char s_last_text[256];
static char s_command[256];
static char s_reply[64];

/******************************* Dictation API ********************************/

static void dictation_session_callback(DictationSession *session, DictationSessionStatus status, 
                                       char *transcription, void *context) {
  if(status == DictationSessionStatusSuccess) {
    strncpy(s_command, transcription, sizeof (s_command));
    DictionaryIterator* dictionaryIterator = NULL;
    app_message_outbox_begin (&dictionaryIterator);
    dict_write_cstring (dictionaryIterator, MESSAGE_KEY_COMMAND, transcription);
    dict_write_end (dictionaryIterator);
  } else {
    // Display the reason for any error
    static char s_failed_buff[128];
    snprintf(s_failed_buff, sizeof(s_failed_buff), "Transcription failed.\n\nError ID:\n%d", (int)status);
    text_layer_set_text(s_output_layer, s_failed_buff);

/************************************ Messaging *************************************/

static void inbox_received_callback(DictionaryIterator *iterator, void *context) {
  APP_LOG(APP_LOG_LEVEL_INFO, "Message received");
  // Read tuples for data
  Tuple *temp_tuple = dict_find(iterator, MESSAGE_KEY_REPLY);
  strncpy( s_reply, temp_tuple->value->cstring, sizeof (s_reply));
  APP_LOG(APP_LOG_LEVEL_INFO, "Reply: %s", s_reply);
  // Display the dictated text
  snprintf(s_last_text, sizeof(s_last_text), "Command:\n%s\nReply: %s", s_command, s_reply);
  text_layer_set_text(s_output_layer, s_last_text);

static void inbox_dropped_callback(AppMessageResult reason, void *context) {
  APP_LOG(APP_LOG_LEVEL_ERROR, "Message dropped");

static void outbox_failed_callback(DictionaryIterator *iterator, AppMessageResult reason, void *context) {
  APP_LOG(APP_LOG_LEVEL_ERROR, "Outbox send failed");

static void outbox_sent_callback(DictionaryIterator *iterator, void *context) {
  APP_LOG(APP_LOG_LEVEL_INFO, "Outbox send success");

/************************************ App *************************************/

static void select_click_handler(ClickRecognizerRef recognizer, void *context) {
  // Start voice dictation UI

static void click_config_provider(void *context) {
  window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler);

static void window_load(Window *window) {
  Layer *window_layer = window_get_root_layer(window);
  GRect bounds = layer_get_bounds(window_layer);

  s_output_layer = text_layer_create(GRect(bounds.origin.x, (bounds.size.h - 24) / 2, bounds.size.w, bounds.size.h));
  text_layer_set_text(s_output_layer, "Press Select to speak");
  text_layer_set_text_alignment(s_output_layer, GTextAlignmentCenter);
  layer_add_child(window_layer, text_layer_get_layer(s_output_layer));

static void window_unload(Window *window) {

static void init() {
  s_main_window = window_create();
  window_set_click_config_provider(s_main_window, click_config_provider);
  window_set_window_handlers(s_main_window, (WindowHandlers) {
    .load = window_load,
    .unload = window_unload,
    // Register callbacks
  // Open AppMessage
  const int inbox_size = 128;
  const int outbox_size = 128;
  app_message_open(inbox_size, outbox_size);
  window_stack_push(s_main_window, true);

  // Create new dictation session
  s_dictation_session = dictation_session_create(sizeof(s_last_text), dictation_session_callback, NULL);

static void deinit() {
  // Free the last session data


int main() {

And here is the javascript code that runs on the phone:

var xhrRequest = function (url, type, callback) {
  var xhr = new XMLHttpRequest();
  xhr.onload = function () {
  };, url);

// Listen for when an AppMessage is received
  function(e) {
    // Get the dictionary from the message
    var dict = e.payload;

    console.log('Got message: ' + JSON.stringify(dict));
    var url = '' + 
    xhrRequest(url, 'GET',
      function(response) {
        console.log('Response: ' + response); 
        // Assemble dictionary using our keys
        var dictionary = {
          'REPLY': response

        // Send to Pebble
          function(e) {
            console.log('Response sent');
          function(e) {
            console.log('Error sending response');

This entry was posted in Home automation and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s