From 6e6440c9fb8bd3970ae0f5d9cb23f470493a0a4a Mon Sep 17 00:00:00 2001 From: Keegan Date: Tue, 25 Oct 2016 03:00:43 -0700 Subject: [PATCH] [WIP] - Basic SimpleAppMessage integration for ints. --- .npmignore | 2 +- CMakeLists.txt | 33 +++++++++++ README.md | 7 ++- clay-test/.gitignore | 10 ++++ clay-test/CMakeLists.txt | 30 ++++++++++ clay-test/package.json | 34 +++++++++++ clay-test/scripts/build.sh | 9 +++ clay-test/src/clay-test.c | 69 ++++++++++++++++++++++ clay-test/src/js/app.js | 19 ++++++ clay-test/wscript | 59 +++++++++++++++++++ dev/config-2.js | 7 +++ dev/config.js | 2 +- include/clay.h | 33 +++++++++++ index.js | 115 ++++++++++++++++++------------------- package.json | 4 +- src/c/clay.c | 91 +++++++++++++++++++++++++++++ src/c/hash.c | 21 +++++++ src/c/hash.h | 5 ++ src/scripts/lib/utils.js | 8 +++ 19 files changed, 496 insertions(+), 62 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 clay-test/.gitignore create mode 100644 clay-test/CMakeLists.txt create mode 100644 clay-test/package.json create mode 100755 clay-test/scripts/build.sh create mode 100644 clay-test/src/clay-test.c create mode 100644 clay-test/src/js/app.js create mode 100644 clay-test/wscript create mode 100644 include/clay.h create mode 100644 src/c/clay.c create mode 100644 src/c/hash.c create mode 100644 src/c/hash.h diff --git a/.npmignore b/.npmignore index 943f877..7923c95 100755 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,4 @@ -# We ignore everying except for what is actually used by the pebble tool. +# We ignore everything except for what is actually used by the pebble tool. /**/* !/dist.zip !/src/**/* diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..4aa4ad3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required (VERSION 3.2) +project (test-project) + +set(CMAKE_C_FLAGS "-std=c11 -mthumb -ffunction-sections -g -fno-diagnostics-show-caret -D_REENT_SMALL=1 -Wall -Wextra -Werror -Wpointer-arith -Wno-unused-parameter -Wno-missing-field-initializers -Wno-error=unused-function -Wno-error=unused-variable -Wno-error=unused-parameter -Wno-error=unused-but-set-variable -Wno-packed-bitfield-compat -mcpu=cortex-m3 -Os -Werror=return-type") + +file(GLOB_RECURSE src "src/c/*") +file(GLOB_RECURSE include "include/*") +file(GLOB_RECURSE build "build/basalt/*.c") + +set(SOURCES + ${src} + ${build} + ${include} + ) + +set(INCLUDES + ~/Library/Application\ Support/Pebble\ SDK/SDKs/current/sdk-core/pebble/basalt/include + ./node_modules/pebble-events/dist/include + ./node_modules/@smallstoneapps/linked-list/dist/include + ./node_modules/@keegan-stoneware/simple-dict/dist/include + ./node_modules/@keegan-stoneware/simple-app-message/dist/include + ./build/include + ./build + ./include +) + +add_definitions(-DPEBBLE) +add_definitions(-DPBL_COLOR) + +include_directories(${INCLUDES}) + +add_executable(app ${SOURCES}) + diff --git a/README.md b/README.md index 06029b4..a677138 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# Clay +# Clay - Experimental + +Welcome to the experimental branch of Clay. Expect this branch to be buggy, with incorrect docs, and with APIs that often change. DO NOT use this branch for your own projets but feel free to provide feedback via the Clay channel at http://discord.gg/aRUAYFN + +--- + Clay is a JavaScript library that makes it easy to add offline configuration pages to your Pebble apps. All you need to get started is a couple lines of JavaScript and a JSON file; no servers or HTML required. Clay will by default automatically handle the 'showConfiguration' and 'webviewclosed' events traditionally implemented by developers to relay configuration settings to the watch side of the app. This step is not required when using Clay, since each config item is given the same `messageKey` as defined in `package.json` (or PebbleKit JS Message Keys on CloudPebble), and is automatically transmitted once the configuration page is submitted by the user. Developers can override this behavior by [handling the events manually](#handling-the-showconfiguration-and-webviewclosed-events-manually). diff --git a/clay-test/.gitignore b/clay-test/.gitignore new file mode 100644 index 0000000..f69d3d2 --- /dev/null +++ b/clay-test/.gitignore @@ -0,0 +1,10 @@ +# Ignore build generated files +build/ +dist/ +dist.zip + +# Ignore waf lock file +.lock-waf* + +# Ignore installed node modules +node_modules/ diff --git a/clay-test/CMakeLists.txt b/clay-test/CMakeLists.txt new file mode 100644 index 0000000..b99718b --- /dev/null +++ b/clay-test/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required (VERSION 3.2) +project (test-project) + +set(CMAKE_C_FLAGS "-std=c11 -mthumb -ffunction-sections -g -fno-diagnostics-show-caret -D_REENT_SMALL=1 -Wall -Wextra -Werror -Wpointer-arith -Wno-unused-parameter -Wno-missing-field-initializers -Wno-error=unused-function -Wno-error=unused-variable -Wno-error=unused-parameter -Wno-error=unused-but-set-variable -Wno-packed-bitfield-compat -mcpu=cortex-m3 -Os -Werror=return-type") + +file(GLOB_RECURSE src "src/*") +file(GLOB_RECURSE build "build/basalt/*.c") + +set(SOURCES + ${src} + ${build} + ) + +set(INCLUDES + ~/Library/Application\ Support/Pebble\ SDK/SDKs/current/sdk-core/pebble/basalt/include + ./node_modules/pebble-events/dist/include + ./node_modules/@keegan-stoneware/simple-dict/dist/include + ./node_modules/pebble-clay/dist/include + ./build/include + ./build + ./include +) + +add_definitions(-DPEBBLE) +add_definitions(-DPBL_COLOR) + +include_directories(${INCLUDES}) + +add_executable(app ${SOURCES}) + diff --git a/clay-test/package.json b/clay-test/package.json new file mode 100644 index 0000000..8401a6b --- /dev/null +++ b/clay-test/package.json @@ -0,0 +1,34 @@ +{ + "name": "clay-test", + "author": "MakeAwesomeHappen", + "version": "1.0.0", + "keywords": [ + "pebble-app" + ], + "scripts": { + "build": "./scripts/build.sh" + }, + "private": true, + "dependencies": { + "pebble-clay": "../" + }, + "pebble": { + "displayName": "clay-test", + "uuid": "3fff53d2-1b59-48d7-8187-0c2f3674c32d", + "sdkVersion": "3", + "enableMultiJS": true, + "targetPlatforms": [ + "aplite", + "basalt", + "chalk", + "diorite" + ], + "watchapp": { + "watchface": false + }, + "messageKeys": [], + "resources": { + "media": [] + } + } +} diff --git a/clay-test/scripts/build.sh b/clay-test/scripts/build.sh new file mode 100755 index 0000000..9fb57b4 --- /dev/null +++ b/clay-test/scripts/build.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +test_app_folder_name="clay-test" +if [ "$(basename $(pwd))" != "$test_app_folder_name" ]; then + echo "Must be executed from $test_app_folder_name folder" + exit +fi + +pebble clean && cd .. && npm run pebble-build || { cd $test_app_folder_name && exit; } && cd $test_app_folder_name && pebble build diff --git a/clay-test/src/clay-test.c b/clay-test/src/clay-test.c new file mode 100644 index 0000000..061ff0d --- /dev/null +++ b/clay-test/src/clay-test.c @@ -0,0 +1,69 @@ +#include +#include +#include + +#define CLAY_INBOX_SIZE (64) + +static Window *s_window; +static int s_background_color; + +static void draw_background(Layer *layer, GContext *ctx) { + graphics_context_set_fill_color(ctx, GColorFromHEX(s_background_color)); + graphics_fill_rect(ctx, layer_get_bounds(layer), 0, GCornerNone); +} + +static void prv_update_layer(Layer *layer, GContext *ctx) { + draw_background(layer, ctx); +} + + +static void prv_window_unload(Window *window) { + window_destroy(window); +} + +static void prv_window_load(Window *window) { + layer_set_update_proc(window_get_root_layer(s_window), prv_update_layer); +} + +static void prv_clay_updated_handler(void *context) { + int value_int; + if (clay_get_int("test_int", &value_int)) { + s_background_color = value_int; + } + + layer_mark_dirty(window_get_root_layer(s_window)); +} + +static void prv_init(void) { + + const ClayCallbacks clay_callbacks = (ClayCallbacks) { + .settings_updated = prv_clay_updated_handler, + }; + clay_register_callbacks(&clay_callbacks, NULL); + + clay_init(CLAY_INBOX_SIZE); + + s_window = window_create(); + window_set_window_handlers(s_window, (WindowHandlers) { + .load = prv_window_load, + .unload = prv_window_unload, + }); + + window_stack_push(s_window, true); +} + +int main(void) { + prv_init(); + app_event_loop(); +} + + + +//int s_background_color = 0xffffff; +//#define TEXT_SIZE 20; +//char s_text [TEXT_SIZE]; +// +//void clay_update_handler(void *ctx) { +// clay_get_int("background_color", &s_background_color); +// clay_get_string("background_color", &s_text, TEXT_SIZE - 1); +//} diff --git a/clay-test/src/js/app.js b/clay-test/src/js/app.js new file mode 100644 index 0000000..bda97d4 --- /dev/null +++ b/clay-test/src/js/app.js @@ -0,0 +1,19 @@ +'use strict'; +var Clay = require('pebble-clay'); +var clay = new Clay({ + config: [ + { + label: 'test', + type: 'color', + messageKey: 'test_int' + }, + { + type: 'submit', + defaultValue: 'submit' + } + ], + closedCallback: function(response) { + console.log('closedcallback: arguments' + JSON.stringify(arguments)); + } +}); + diff --git a/clay-test/wscript b/clay-test/wscript new file mode 100644 index 0000000..3434717 --- /dev/null +++ b/clay-test/wscript @@ -0,0 +1,59 @@ +# +# This file is the default set of rules to compile a Pebble application. +# +# Feel free to customize this to your needs. +# +import os.path +import shutil +import waflib + +top = '.' +out = 'build' + + +def options(ctx): + ctx.load('pebble_sdk') + + +def distclean(ctx): + node_modules_dir = ctx.path.find_node('node_modules') + if node_modules_dir: + shutil.rmtree(node_modules_dir.abspath()) + waflib.Scripting.distclean(ctx) + + +def configure(ctx): + """ + This method is used to configure your build. ctx.load(`pebble_sdk`) automatically configures + a build for each valid platform in `targetPlatforms`. Platform-specific configuration: add your + change after calling ctx.load('pebble_sdk') and make sure to set the correct environment first. + Universal configuration: add your change prior to calling ctx.load('pebble_sdk'). + """ + ctx.load('pebble_sdk') + + +def build(ctx): + ctx.load('pebble_sdk') + + build_worker = os.path.exists('worker_src') + binaries = [] + + cached_env = ctx.env + for platform in ctx.env.TARGET_PLATFORMS: + ctx.env = ctx.all_envs[platform] + ctx.set_group(ctx.env.PLATFORM_NAME) + app_elf = '{}/pebble-app.elf'.format(ctx.env.BUILD_DIR) + ctx.pbl_program(source=ctx.path.ant_glob('src/**/*.c'), target=app_elf) + + if build_worker: + worker_elf = '{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR) + binaries.append({'platform': platform, 'app_elf': app_elf, 'worker_elf': worker_elf}) + ctx.pbl_worker(source=ctx.path.ant_glob('worker_src/**/*.c'), target=worker_elf) + else: + binaries.append({'platform': platform, 'app_elf': app_elf}) + ctx.env = cached_env + + ctx.set_group('bundle') + ctx.pbl_bundle(binaries=binaries, + js=ctx.path.ant_glob(['src/js/**/*.js', 'src/js/**/*.json']), + js_entry_file='src/js/app.js') diff --git a/dev/config-2.js b/dev/config-2.js index e06c08a..47101b9 100644 --- a/dev/config-2.js +++ b/dev/config-2.js @@ -12,6 +12,13 @@ module.exports = [ "type": "text", "defaultValue": "This is config 2" }, + { + type: 'thing', + defaultValue: { + en_US: 'foo', + fr_FR: 'bar' + } + }, { "type": "section", "items": [ diff --git a/dev/config.js b/dev/config.js index c98ef72..f822d12 100644 --- a/dev/config.js +++ b/dev/config.js @@ -41,7 +41,7 @@ module.exports = [ "messageKey": "email", "defaultValue": "", "label": "Input Field", - "description": "This is a description for the input component. " + + "description": "{{i18n.foo}} This is a description for the input component. " + "You can add html in here too.", "attributes": { "placeholder": "Placeholder set with attributes" diff --git a/include/clay.h b/include/clay.h new file mode 100644 index 0000000..7317ee6 --- /dev/null +++ b/include/clay.h @@ -0,0 +1,33 @@ +#pragma once + +#include <@keegan-stoneware/simple-app-message/simple-app-message.h> + +typedef void (*ClayUpdatedCallback)(void *context); + +typedef struct ClayCallbacks { + ClayUpdatedCallback settings_updated; +} ClayCallbacks; + +void clay_init(uint32_t inbox_size); + +void clay_remove(const char *key); + +void clay_clear(); + +void clay_set_bool(const char *key, bool value); + +void clay_set_data(const char *key, const void *data, size_t n); + +void clay_set_int(const char *key, int value); + +void clay_set_string(const char *key, const char *value); + +bool clay_get_bool(const char *key, bool *value_out); + +bool clay_get_data(const char *key, void *value_out, size_t n); + +bool clay_get_int(const char *key, int *value_out); + +bool clay_get_string(const char *key, char *value_out, size_t n); + +bool clay_register_callbacks(const ClayCallbacks *callbacks, void *context); diff --git a/index.js b/index.js index ef633e5..3ae14ba 100755 --- a/index.js +++ b/index.js @@ -5,34 +5,30 @@ var toSource = require('tosource'); var standardComponents = require('./src/scripts/components'); var deepcopy = require('deepcopy/build/deepcopy.min'); var version = require('./package.json').version; -var messageKeys = require('message_keys'); +var simpleAppMessage = require('@keegan-stoneware/simple-app-message'); +var utils = require('./src/scripts/lib/utils'); /** - * @param {Array} config - the Clay config - * @param {function} [customFn] - Custom code to run from the config page. Will run - * with the ClayConfig instance as context - * @param {Object} [options] - Additional options to pass to Clay - * @param {boolean} [options.autoHandleEvents=true] - If false, Clay will not - * automatically handle the 'showConfiguration' and 'webviewclosed' events - * @param {*} [options.userData={}] - Arbitrary data to pass to the config page. Will - * be available as `clayConfig.meta.userData` + * @param {Object} options - Clay options + * @param {Array} [options.config] - The config + * @param {function} [options.openCallback] + * @param {function} [options.closedCallback] + * @param {Object} [options.locales] + * @param {function} [options.customFunction] - Custom code to run from the config + * page. Will run with the ClayConfig instance as context * @constructor */ -function Clay(config, customFn, options) { +function Clay(options) { var self = this; - if (!Array.isArray(config)) { - throw new Error('config must be an Array'); + if (!options) { + utils.throw('You must define options'); } - if (customFn && typeof customFn !== 'function') { - throw new Error('customFn must be a function or "null"'); - } + options = deepcopy(options); - options = options || {}; - - self.config = deepcopy(config); - self.customFn = customFn || function() {}; + self.config = options.config; + self.customFn = options.customFn || function() {}; self.components = {}; self.meta = { activeWatchInfo: null, @@ -57,30 +53,30 @@ function Clay(config, customFn, options) { } // Let Clay handle all the magic - if (options.autoHandleEvents !== false && typeof Pebble !== 'undefined') { + Pebble.addEventListener('showConfiguration', function() { + _populateMeta(); + Pebble.openURL(self.generateUrl()); + }); - Pebble.addEventListener('showConfiguration', function() { - _populateMeta(); - Pebble.openURL(self.generateUrl()); + Pebble.addEventListener('webviewclosed', function(e) { + + if (!e || !e.response) { return; } + + var settings = self.getSettings(e.response); + + if (options.closedCallback) { + options.closedCallback(settings); + } + + simpleAppMessage.send('CLAY', settings, function(response) { + if (response.error) { + utils.log('Failed to send config data!'); + utils.log(response.error); + } else { + utils.log('Sent config data to Pebble'); + } }); - - Pebble.addEventListener('webviewclosed', function(e) { - - if (!e || !e.response) { return; } - - // Send settings to Pebble watchapp - Pebble.sendAppMessage(self.getSettings(e.response), function() { - console.log('Sent config data to Pebble'); - }, function(error) { - console.log('Failed to send config data!'); - console.log(JSON.stringify(error)); - }); - }); - } else if (typeof Pebble !== 'undefined') { - Pebble.addEventListener('ready', function() { - _populateMeta(); - }); - } + }); /** * If this function returns true then the callback will be executed @@ -195,20 +191,21 @@ Clay.prototype.getSettings = function(response, convert) { try { settings = JSON.parse(response); } catch (e) { - throw new Error('The provided response was not valid JSON'); + utils.error('The provided response was not valid JSON'); } + // @todo do something with the persist flag here // flatten the settings for localStorage - var settingsStorage = {}; - Object.keys(settings).forEach(function(key) { - if (typeof settings[key] === 'object' && settings[key]) { - settingsStorage[key] = settings[key].value; - } else { - settingsStorage[key] = settings[key]; - } - }); - - localStorage.setItem('clay-settings', JSON.stringify(settingsStorage)); +// var settingsStorage = {}; +// Object.keys(settings).forEach(function(key) { +// if (typeof settings[key] === 'object' && settings[key]) { +// settingsStorage[key] = settings[key].value; +// } else { +// settingsStorage[key] = settings[key]; +// } +// }); +// +// localStorage.setItem('clay-settings', JSON.stringify(settingsStorage)); return convert === false ? settings : Clay.prepareSettingsForAppMessage(settings); }; @@ -321,13 +318,15 @@ Clay.prepareSettingsForAppMessage = function(settings) { var result = {}; Object.keys(flatSettings).forEach(function(key) { - var messageKey = messageKeys[key]; - var settingArr = Clay.prepareForAppMessage(flatSettings[key]); - settingArr = Array.isArray(settingArr) ? settingArr : [settingArr]; +// var messageKey = messageKeys[key]; +// var settingArr = Clay.prepareForAppMessage(flatSettings[key]); +// settingArr = Array.isArray(settingArr) ? settingArr : [settingArr]; +// +// settingArr.forEach(function(setting, index) { +// result[messageKey + index] = setting; +// }); - settingArr.forEach(function(setting, index) { - result[messageKey + index] = setting; - }); + result[key] = Clay.prepareForAppMessage(flatSettings[key]); }); // validate the settings diff --git a/package.json b/package.json index 8551fab..4a0d907 100644 --- a/package.json +++ b/package.json @@ -87,5 +87,7 @@ "vinyl-source-stream": "^1.1.0", "watchify": "^3.7.0" }, - "dependencies": {} + "dependencies": { + "@keegan-stoneware/simple-app-message": "^1.0.0" + } } diff --git a/src/c/clay.c b/src/c/clay.c new file mode 100644 index 0000000..8b583cc --- /dev/null +++ b/src/c/clay.c @@ -0,0 +1,91 @@ +#include "clay.h" +#include "hash.h" + +#define SIMPLE_APP_MESSAGE_NAMESPACE ("CLAY") + +static ClayCallbacks s_callbacks; +static void *s_context; + +static bool prv_store_settings(const char *key, SimpleDictDataType type, const void *data, + size_t data_size, void *context) { + + uint32_t persist_key = hash((uint8_t *)key, strlen(key)); + + switch (type) { + case SimpleDictDataType_Raw: + persist_write_data(persist_key, data, data_size); + return true; + case SimpleDictDataType_Bool: { + const bool value = *((bool *)data); + persist_write_bool(persist_key, value); + return true; + } + case SimpleDictDataType_Int: { + const int value = *((int *)data); + APP_LOG(APP_LOG_LEVEL_INFO, "CLAY: writing %d - %d to storage", (int)persist_key, value); + persist_write_int(persist_key, value); + return true; + } + case SimpleDictDataType_String: { + const char *value = data; + persist_write_string(persist_key, value); + return true; + } + case SimpleDictDataTypeCount: + break; + } + APP_LOG(APP_LOG_LEVEL_ERROR, "Unexpected type %d", type); + return false; +} + +static void prv_simple_app_message_received_callback(const SimpleDict *message, void *context) { + if (!message) { + return; + } + + APP_LOG(APP_LOG_LEVEL_INFO, "CLAY: Received SimpleAppMessage"); + simple_dict_foreach(message, prv_store_settings, NULL); + s_callbacks.settings_updated(context); +} + +bool clay_register_callbacks(const ClayCallbacks *callbacks, void *context) { + s_callbacks = *callbacks; + s_context = context; + return true; +} + +void clay_init(uint32_t inbox_size) { + const SimpleAppMessageCallbacks simple_app_message_callbacks = (SimpleAppMessageCallbacks) { + .message_received = prv_simple_app_message_received_callback, + }; + const bool register_success = simple_app_message_register_callbacks(SIMPLE_APP_MESSAGE_NAMESPACE, + &simple_app_message_callbacks, + s_context); + if (!register_success) { + APP_LOG(APP_LOG_LEVEL_ERROR, "Failed to register callbacks for namespace %s", SIMPLE_APP_MESSAGE_NAMESPACE); + return; + } + const bool request_inbox_size_success = simple_app_message_request_inbox_size(inbox_size); + if (!request_inbox_size_success) { + APP_LOG(APP_LOG_LEVEL_ERROR, "Failed to request inbox size of %d", (int)inbox_size); + return; + } + + simple_app_message_open(); +} + +bool clay_get_int(const char *key, int *value_out) { + uint32_t persist_key = hash((uint8_t *)key, strlen(key)); + + if (persist_exists(persist_key)) { + int value = persist_read_int(persist_key); + APP_LOG(APP_LOG_LEVEL_INFO, "CLAY: reading %d - %d from storage", (int)persist_key, value); + memcpy(value_out, &value, sizeof(&value)); + + return true; + } + return false; +} + + + diff --git a/src/c/hash.c b/src/c/hash.c new file mode 100644 index 0000000..2fcb3c0 --- /dev/null +++ b/src/c/hash.c @@ -0,0 +1,21 @@ +#include "hash.h" + +#include + +// Based on DJB2 Hash +uint32_t hash(const uint8_t *bytes, const uint32_t length) { + uint32_t hash = 5381; + + if (length == 0) { + return hash; + } + + uint8_t c; + const uint8_t *last_byte = bytes + length; + while (bytes != last_byte) { + c = *bytes; + hash = ((hash << 5) + hash) + c; + bytes++; + } + return hash; +} diff --git a/src/c/hash.h b/src/c/hash.h new file mode 100644 index 0000000..e422ebe --- /dev/null +++ b/src/c/hash.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +uint32_t hash(const uint8_t *bytes, const uint32_t length); diff --git a/src/scripts/lib/utils.js b/src/scripts/lib/utils.js index 58bfc46..bd270b1 100644 --- a/src/scripts/lib/utils.js +++ b/src/scripts/lib/utils.js @@ -133,3 +133,11 @@ module.exports.includesCapability = function(activeWatchInfo, capabilities) { return result.indexOf(false) === -1; }; + +module.exports.log = function(message) { + console.log('Clay: ' + JSON.stringify(message)); +}; + +module.exports.throw = function(error) { + throw new Error('Clay: '+ error) +};