Linking Pebble and Spark Core (Part 2)

Note: The JS code may only work on Android devices.

Part 1: Linking Pebble and Spark Core

Introduction

In the last post (linked above) I detailed the basics of connecting a Pebble watchapp’s button clicks to a Spark.function() call on a Spark Core. In this post I will go over the reverse process: sending data back to the Pebble asynchronously. Once again this process uses a combination of Spark Cloud, PebbleKit JS and AppMessage to convey the message, which this time will be alerting a Pebble watch wearer that a button connected to the Core has been pressed via a short vibration pulse.

Preparing Pebble

The initial Pebble C program code is similar in structure to the last post’s starting point, but without any of the Click functionality, as this will be a receive-only app. Thus the start of your project’s main .c file will look like this:

#include <pebble.h>

#define KEY_BUTTON_STATE 0

static Window *window;
static TextLayer *text_layer;

static void window_load(Window *window) 
{
	//Create TextLayer
	text_layer = text_layer_create(GRect(0, 0, 144, 168));
	text_layer_set_text(text_layer, "Press button on Core pin D0");
	text_layer_set_text_alignment(text_layer, GTextAlignmentCenter);
	layer_add_child(window_get_root_layer(window), text_layer_get_layer(text_layer));
}

static void window_unload(Window *window) 
{
	//Destroy TextLayer
	text_layer_destroy(text_layer);
}

static void init(void) 
{
	//Create Window
	window = window_create();
	window_set_window_handlers(window, (WindowHandlers) {
		.load = window_load,
		.unload = window_unload,
	});

	//Prepare AppMessage
	app_message_open(app_message_inbox_size_maximum(), app_message_outbox_size_maximum());

	window_stack_push(window, true);
}

static void deinit(void) 
{
	//Destroy Window
	window_destroy(window);
}

int main(void) 
{
	init();
	app_event_loop();
	deinit();
}

Note that the name of the main AppMessage key has changed to a more appropriate KEY_BUTTON_STATE, but this is arbitrary – the value is still 0.

Instead of receiving button clicks, the app will be receiving messages sent from the phone on receiving a message from the Spark Cloud. To do this, we register an AppMessageInboxReceived handler before opening the service:

app_message_register_inbox_received((AppMessageInboxReceived) in_recv_handler);

and also declare the function above init():

static void in_recv_handler(DictionaryIterator *iterator, void *context)
{

}

This handler provides a DictionaryIterator structure that contains the received dictionary. To access the data, we use the dict_read_first() function to extract the tuple. This contains the key and value pair. We will then compare the value cstring and act accordingly (“HIGH” for button pressed and pulling pin D0 HIGH on the Core):

static void in_recv_handler(DictionaryIterator *iterator, void *context)
{
	//Get first tuple (should be KEY_BUTTON_STATE)
	Tuple *t = dict_read_first(iterator);

	//If it's there
	if(t)
	{
		if(strcmp("HIGH", t->value->cstring) == 0)
		{
			vibes_short_pulse();
		}
	}
}

Compile this and upload to your Pebble to make sure it is ready to work with PebbleKit JS, which we will set up next.

Preparing PebbleKit JS

Also similar to last time, we must setup the JS code to listen for events from the Spark Cloud and send AppMessages on to the watch. However, this time we do not require jQuery but instead use an object called EventSource that will provide the messages in a callback. This is done in the “ready” event handler:

Pebble.addEventListener("ready",
	function(e) {
		//Register EventSource listener
		var core = new EventSource("https://api.spark.io/v1/events/?access_token=" + accessToken);
		core.addEventListener("button_state", 
			function(response) {
				
			}, 
			false
		);

		console.log("Pebble JS Ready!");
	}
);

Note: This requires only your Access Token, not the Device ID.

Once this callback has been created, it will be executed whenever a Core firmware uses Spark.publish() with the topic “button_state”. When this event occurs, we will send the accompanying payload, either “HIGH” or “LOW” (details later) to the Pebble for it to decide whether to vibrate or not. This process looks like this:

core.addEventListener("button_state", 
	function(response) {
		//Interpret response as JSON
		var json = JSON.parse(response.data);

		console.log("Payload is '" + json.data + "'");

		//Send the payload
		Pebble.sendAppMessage(
			{"KEY_BUTTON_STATE":json.data},
			function(e) {
				console.log("Sent '" + json.data + "' to Pebble.");
			},
			function(e) {
				console.log("Failed to send data to Pebble!");
			}
		);
	}, 
	false
);

The AppMessage dictionary takes the form of a JSON dictionary with the key-value pair consisting of the declared key (remember to alter appinfo.json or the App Keys section in Settings on CloudPebble) and the word “HIGH” or “LOW” as received from the Core. We also get to register two callbacks for if the message was successful, and if it is not. The above code segment uses this to provide some meaningful log output.

This completes the setup of the JS segment of the message’s journey. With the JS code in place, re-compile and re-upload your Pebble .pbw file to your watch.

Preparing the Core

The last thing to do is to configure the Core to call Spark.publish() to notify the JS and C code we have already set up. This is done in the loop() function and takes the form of a simple if, else statement, depending on whether digitalRead(D0) determines whether the button is pressed. If you don’t have a button to hand, you can simulate one by simply touching the 3.3V pin of your core to D0 briefly once the following code is in place and uploaded:

static bool pressed = false;

void setup() {
    pinMode(D0, INPUT);
}

void loop() {
    //Publish button state
    if(digitalRead(D0) == HIGH && pressed == false)
    {
        Spark.publish("button_state", "HIGH");
        pressed = true;
        
        //Rate limit to prevent spamming the cloud
        delay(500);
    }
    else if(digitalRead(D0) == LOW && pressed == true)
    {
        Spark.publish("button_state", "LOW");
        pressed = false;
    }
}

If you do have a push button to hand, here is how to connect it up, as depicted on the Arduino site, except instead of pin 2, we are using Core pin D0. Once this is done, ensure both watchapp and Core firmware are uploaded and running before pressing the button. The watch should vibrate within a couple of seconds!

Conclusion

There we have an expansion on the original post, showing how to send asynchronous events and data from the Spare Core to the Pebble watch. A slight reduction in latency between the two can be theoretically achieved by calling app_comm_set_sniff_interval(SNIFF_INTERVAL_REDUCED), although this will consume more power over a long term period.

As always, the source code to this project can be found here on GitHub.

Enjoy!

Advertisements
14 comments
  1. Neo Burgi said:

    When i try to compile your code on cloudpebble I receive an error message. “EventSource is not defined” Any idea about what I a missing?

    • bonsitm said:

      If it’s a lint warning then ignore it. Its not officially supported, but works.

      • Neo Burgi said:

        Cloudpebble doesnt allow to compile it if it contains errors. Any other suggestions? Since I am using Windows it is not that easy to run the SDK due to missing skills.

      • bonsitm said:

        Hi Neo,

        The project will compile in CloudPebble if you change the version to a Major.Minor eg: 1.0 and not 1.0.0. This was the compiler error. The linting error surrounding EventSource does not prevent compilation, but should be supported. I will update the repo

      • Neo Burgi said:

        Thank you very much for your help but it still doesnt compile in cloudpebble. Are there any settings that I have to check (selection boxes or something like that?) I have copied the error message:

        Setting top to : /tmp/tmpY46O3E
        Setting out to : /tmp/tmpY46O3E/build
        Checking for program gcc,cc : arm-none-eabi-gcc
        Checking for program ar : arm-none-eabi-ar
        Found Pebble SDK in : /app/sdk2/Pebble
        ‘configure’ finished successfully (0.086s)
        Waf: Entering directory `/tmp/tmpY46O3E/build’
        [ 1/13] appinfo.auto.c: appinfo.json -> build/appinfo.auto.c
        [ 2/13] app_resources.pbpack.data: -> build/app_resources.pbpack.data
        [ 3/13] app_resources.pbpack.table: ../../app/sdk2/Pebble/tools/pbpack_meta_data.py -> build/app_resources.pbpack.table
        [ 5/13] app_resources.pbpack.manifest: build/app_resources.pbpack.data ../../app/sdk2/Pebble/tools/pbpack_meta_data.py -> build/app_resources.pbpack.manifest
        [ 5/13] resource_ids.auto.h: ../../app/sdk2/Pebble/tools/generate_resource_code.py build/app_resources.pbpack.data -> build/src/resource_ids.auto.h
        [ 6/13] c: build/appinfo.auto.c -> build/appinfo.auto.c.7.o
        [ 7/13] c: src/pebble-spark-link.c -> build/src/pebble-spark-link.c.7.o
        [ 8/13] app_resources.pbpack: build/app_resources.pbpack.manifest build/app_resources.pbpack.table build/app_resources.pbpack.data -> build/app_resources.pbpack
        [ 9/13] cprogram: build/src/pebble-spark-link.c.7.o build/appinfo.auto.c.7.o -> build/pebble-app.elf
        /app/sdk2/arm-cs-tools/bin/../lib/gcc/arm-none-eabi/4.7.3/../../../../arm-none-eabi/bin/ld: warning: cannot find entry symbol main; not setting start address
        [10/13] report-memory-usage: build/pebble-app.elf
        [11/13] pebble-app.raw.bin: build/pebble-app.elf -> build/pebble-app.raw.bin
        app memory usage:
        =============
        Total footprint in RAM: 134 bytes / ~24kb
        Free RAM available (heap): 24442 bytes

        [12/13] inject-metadata: build/pebble-app.raw.bin build/pebble-app.elf build/app_resources.pbpack.data -> build/pebble-app.bin
        Waf: Leaving directory `/tmp/tmpY46O3E/build’
        Build failed
        Traceback (most recent call last):
        File “/app/sdk2/Pebble/.waf-1.7.11-756b3d890d72b466b8907300932ec61d/waflib/Task.py”, line 123, in process
        ret=self.run()
        File “/app/sdk2/Pebble/.waf-1.7.11-756b3d890d72b466b8907300932ec61d/waflib/Task.py”, line 47, in run
        return m1(self)
        File “/app/sdk2/Pebble/.waf-1.7.11-756b3d890d72b466b8907300932ec61d/waflib/extras/pebble_sdk.py”, line 132, in inject_data_rule
        inject_metadata.inject_metadata(tgt_path,elf_path,res_path,timestamp,allow_js=has_jsapp,has_worker=(worker_elf_file is not None))
        File “/app/sdk2/Pebble/.waf-1.7.11-756b3d890d72b466b8907300932ec61d/waflib/extras/inject_metadata.py”, line 124, in inject_metadata
        raise Exception(“Missing app entry point! Must be `int main(void) { … }` “)
        Exception: Missing app entry point! Must be `int main(void) { … }`

        [ERROR ] out of pty devices

      • bonsitm said:

        It looks like your code does not include a standard entry point (a main function). Can you send me a link to your code?

  2. Neo Burgi said:

    It seems, that I had some bad settings and I was able to build it now but the log shows me the following exception:
    [PHONE] pebble-app.js:?: JS: starting app: 2D717E5E-3DCA-4AAA-AD1A-9B57BF901D3C Spark Core
    [PHONE] pebble-app.js:?: app is ready: 1
    [PHONE] pebble-app.js:?: Error: Spark Core: ReferenceError: Can’t find variable: EventSource at line 6 in pebble-js-app.js

    It would be perfect if you could help me again.

    • bonsitm said:

      Are you on iOS or Android? The EventSource object may only be available on the Android platform.

      • Neo Burgi said:

        I am on iOS. So you have one more time found the missing part. Do you think that there is an alternative on the iOS device for the Eventsource part?

      • bonsitm said:

        I’m afraid not, as I don’t have intimate knowledge of how the JS code is executed on iOS. I have a good hunch that Android JS is run through some internal web view since a lot of the accompanying APIs are available.

      • Neo Burgi said:

        But thx anyway. I have just tested it on an android basis and it works just perfect. Thumps up for your help and your work.

      • bonsitm said:

        Thanks! Sorry it didn’t work out on iOS.

  3. Neo Burgi said:

    This is in the pebble js file

    var accessToken = “7a10d6415b9aebd640cb10ed22cb3b403a8554c1”;

    Pebble.addEventListener(“ready”,
    function(e) {
    //Register EventSource listener
    var core = new EventSource(“https://api.spark.io/v1/events/?access_token=” + accessToken);
    core.addEventListener(“button_state”,
    function(response) {
    //Interpret response as JSON
    var json = JSON.parse(response.data);

    console.log(“Payload is ‘” + json.data + “‘”);

    //Send the payload
    Pebble.sendAppMessage(
    {“KEY_BUTTON_STATE”:json.data},
    function(e) {
    console.log(“Sent ‘” + json.data + “‘ to Pebble.”);
    },
    function(e) {
    console.log(“Failed to send data to Pebble!”);
    }
    );
    },
    false
    );

    console.log(“Pebble JS Ready!”);
    }
    );

    And this is in a pebble.c file
    #include
    #define KEY_BUTTON_STATE 0

    static Window *window;
    static TextLayer *text_layer;

    static void window_load(Window *window)
    {
    //Create TextLayer
    text_layer = text_layer_create(GRect(0, 0, 144, 168));
    text_layer_set_text(text_layer, “Press button on Core pin D0”);
    text_layer_set_text_alignment(text_layer, GTextAlignmentCenter);
    layer_add_child(window_get_root_layer(window), text_layer_get_layer(text_layer));
    }

    static void window_unload(Window *window)
    {
    //Destroy TextLayer
    text_layer_destroy(text_layer);
    }

    static void in_recv_handler(DictionaryIterator *iterator, void *context)
    {
    //Get first tuple (should be KEY_BUTTON_STATE)
    Tuple *t = dict_read_first(iterator);

    //If it’s there
    if(t)
    {
    if(strcmp(“HIGH”, t->value->cstring) == 0)
    {
    vibes_short_pulse();
    }
    }
    }

    static void init(void)
    {
    //Create Window
    window = window_create();
    window_set_window_handlers(window, (WindowHandlers) {
    .load = window_load,
    .unload = window_unload,
    });

    //Prepare AppMessage
    app_message_register_inbox_received((AppMessageInboxReceived) in_recv_handler);
    app_message_open(app_message_inbox_size_maximum(), app_message_outbox_size_maximum());

    window_stack_push(window, true);
    }

    static void deinit(void)
    {
    //Destroy Window
    window_destroy(window);
    }

    int main(void)
    {
    init();
    app_event_loop();
    deinit();
    }

  4. Neo Burgi said:

    @bonsitm Have you also made the experience that this Service stops to working after a certain amount of time? I will try to figure out where the problem is but my resources are limited. But I have already excluded the spark core since i still receive the Spark.publish Events from the spark core.

Leave a Reply

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

WordPress.com Logo

You are commenting using your WordPress.com 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 )

Google+ photo

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

Connecting to %s

%d bloggers like this: