├── .gitignore ├── README.md ├── appinfo.json ├── pebble-jshintrc ├── resources ├── fonts │ ├── OFL.txt │ └── VarelaRound-Regular.ttf └── images │ ├── bluetooth_off.png │ ├── bluetooth_on.png │ ├── menu_icon.png │ └── pebble9x4.png ├── src ├── config.html ├── pebble_one.c └── pebble_one.js └── wscript /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | build 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ONE](http://freudenbergs.de/pebble/BertONEbanner.jpg) 2 | 3 | The ONE Pebble Watch Face 4 | ========================= 5 | 6 | This is a highly readable analog watchface for the [Pebble Smartwatch][pebble], intended for every-day use. 7 | Its features are customizable, but I want to keep the overall look simple. 8 | 9 | You can install the app from the [Pebble Store][download]. 10 | 11 | Contributions are highly welcome - just send me a [pull request][pullreq]. 12 | 13 | -- Bert Freudenberg, January 2016 14 | 15 | [pebble]: https://getpebble.com/ 16 | [pullreq]: https://help.github.com/articles/using-pull-requests 17 | [download]: https://apps.getpebble.com/applications/52bca8bb87e209ebc9000005 18 | -------------------------------------------------------------------------------- /appinfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "f6c1ad30-2e14-427a-b959-9442f3a46949", 3 | "shortName": "ONE", 4 | "longName": "PebbleONE", 5 | "companyName": "Bert Freudenberg", 6 | "versionCode": 15, 7 | "versionLabel": "3.1", 8 | "watchapp": { 9 | "watchface": true 10 | }, 11 | "capabilities": [ 12 | "configurable" 13 | ], 14 | "appKeys": { 15 | "seconds_mode": 0, 16 | "battery_mode": 1, 17 | "date_mode": 2, 18 | "bluetooth_mode": 3, 19 | "graphics_mode": 4, 20 | "connlost_mode": 5, 21 | "date_pos": 6, 22 | "request_config": 100 23 | }, 24 | "resources": { 25 | "media": [ 26 | { "type": "bitmap", "name": "IMAGE_MENU_ICON", "file": "images/menu_icon.png", "menuIcon": true }, 27 | { "type": "bitmap", "name": "IMAGE_LOGO", "file": "images/pebble9x4.png", "memoryFormat": "1BitPalette" }, 28 | { "type": "bitmap", "name": "IMAGE_BLUETOOTH_OFF", "file": "images/bluetooth_off.png", "memoryFormat": "1BitPalette" }, 29 | { "type": "bitmap", "name": "IMAGE_BLUETOOTH_ON", "file": "images/bluetooth_on.png", "memoryFormat": "1BitPalette" }, 30 | { 31 | "type": "font", 32 | "name": "FONT_30", 33 | "file": "fonts/VarelaRound-Regular.ttf", 34 | "characterRegex": "[0-9A-Za-z éáåäö]" 35 | } 36 | ] 37 | }, 38 | "targetPlatforms": [ 39 | "aplite", 40 | "basalt", 41 | "chalk" 42 | ], 43 | "sdkVersion": "3" 44 | } 45 | -------------------------------------------------------------------------------- /pebble-jshintrc: -------------------------------------------------------------------------------- 1 | /* 2 | * jshint configuration file for Pebble development. 3 | * 4 | * Check out the full documentation at http://www.jshint.com/docs/options/ 5 | */ 6 | { 7 | // Declares the existence of a global 'Pebble' object 8 | "globals": { "Pebble" : true }, 9 | 10 | // And standard objects (XMLHttpRequest and console) 11 | "browser": true, 12 | "devel": true, 13 | 14 | // Do not mess with standard JavaScript objects (Array, Date, etc) 15 | "freeze": true, 16 | 17 | // Do not use eval! Keep this warning turned on (ie: false) 18 | "evil": false, 19 | 20 | /* 21 | * The options below are more style/developer dependent. 22 | * Customize to your liking. 23 | */ 24 | 25 | // All variables should be in camelcase 26 | "camelcase": false, 27 | 28 | // Do not allow blocks without { } 29 | "curly": true, 30 | 31 | // Prohibits the use of immediate function invocations without wrapping them in parentheses 32 | "immed": true, 33 | 34 | // Enforce indentation 35 | "indent": 4, 36 | 37 | // Do not use a variable before it's defined 38 | "latedef": "nofunc", 39 | 40 | // Spot undefined variables 41 | "undef": "true", 42 | 43 | // Spot unused variables 44 | "unused": "true" 45 | } 46 | -------------------------------------------------------------------------------- /resources/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Joe Prince, Admix Designs (http://www.admixdesigns.com/), 2 | with Reserved Font Names "Varela" and "Varela Round". 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /resources/fonts/VarelaRound-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefrau/PebbleONE/a5d7303fa341762034ce101feb6474f27e8e800b/resources/fonts/VarelaRound-Regular.ttf -------------------------------------------------------------------------------- /resources/images/bluetooth_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefrau/PebbleONE/a5d7303fa341762034ce101feb6474f27e8e800b/resources/images/bluetooth_off.png -------------------------------------------------------------------------------- /resources/images/bluetooth_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefrau/PebbleONE/a5d7303fa341762034ce101feb6474f27e8e800b/resources/images/bluetooth_on.png -------------------------------------------------------------------------------- /resources/images/menu_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefrau/PebbleONE/a5d7303fa341762034ce101feb6474f27e8e800b/resources/images/menu_icon.png -------------------------------------------------------------------------------- /resources/images/pebble9x4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefrau/PebbleONE/a5d7303fa341762034ce101feb6474f27e8e800b/resources/images/pebble9x4.png -------------------------------------------------------------------------------- /src/config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 58 | 59 | 60 |
61 | Seconds hand: 62 |
63 | 67 |
68 |
69 | Battery indicator: 70 |
71 | 75 |
76 |
77 | Bluetooth indicator: 78 |
79 | 83 |
84 |
85 | Date display and language: 86 |
87 | 91 |
92 |
93 | 100 |
101 |
102 |
103 | 106 |
107 | Color
scheme: 108 |
109 |
110 | 113 |
114 | If lost phone
connection: 115 |
116 |

117 | This is an open source app: Here is the source code. 118 | Contributions are highly welcome!
119 | -- Bert Freudenberg, January 2016 120 |

121 |
122 | 123 |
124 |
125 | 126 |
127 |
128 |
129 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /src/pebble_one.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Bert Freudenberg 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | #include 24 | #include 25 | 26 | // keys for app message and storage 27 | #define SECONDS_MODE 0 28 | #define BATTERY_MODE 1 29 | #define DATE_MODE 2 30 | #define BLUETOOTH_MODE 3 31 | #define GRAPHICS_MODE 4 32 | #define CONNLOST_MODE 5 33 | #define DATE_POS 6 34 | #define INBOX_SIZE (1 + (7+4) * 7) 35 | 36 | #define REQUEST_CONFIG 100 37 | #define OUTBOX_SIZE (1 + (7+4) * 1) 38 | 39 | #define SECONDS_MODE_NEVER 0 40 | #define SECONDS_MODE_IFNOTLOW 1 41 | #define SECONDS_MODE_ALWAYS 2 42 | #define BATTERY_MODE_NEVER 0 43 | #define BATTERY_MODE_IF_LOW 1 44 | #define BATTERY_MODE_ALWAYS 2 45 | #define DATE_POS_OFF 0 46 | #define DATE_POS_TOP 1 47 | #define DATE_POS_BOTTOM 2 48 | #define DATE_MODE_OFF 0 49 | #define DATE_MODE_FIRST 1 50 | #define DATE_MODE_EN 1 51 | #define DATE_MODE_DE 2 52 | #define DATE_MODE_ES 3 53 | #define DATE_MODE_FR 4 54 | #define DATE_MODE_IT 5 55 | #define DATE_MODE_SE 6 56 | #define DATE_MODE_LAST 6 57 | #define BLUETOOTH_MODE_NEVER 0 58 | #define BLUETOOTH_MODE_IFOFF 1 59 | #define BLUETOOTH_MODE_ALWAYS 2 60 | #define GRAPHICS_MODE_NORMAL 0 61 | #define GRAPHICS_MODE_INVERT 1 62 | #define CONNLOST_MODE_IGNORE 0 63 | #define CONNLOST_MODE_WARN 1 64 | 65 | 66 | #define SCREENSHOT 0 67 | #define DEBUG 0 68 | 69 | #define ONE TRIG_MAX_RATIO 70 | #define THREESIXTY TRIG_MAX_ANGLE 71 | 72 | #define EXTENT PBL_IF_ROUND_ELSE(180, 144) 73 | #define CENTER_X PBL_IF_ROUND_ELSE( 90, 71) 74 | #define CENTER_Y PBL_IF_ROUND_ELSE( 90, 71) 75 | #define DOTS_RADIUS PBL_IF_ROUND_ELSE( 80, 67) 76 | #define DOTS_SIZE PBL_IF_ROUND_ELSE( 5, 4) 77 | #define SEC_RADIUS PBL_IF_ROUND_ELSE( 72, 62) 78 | 79 | #define max(a,b) ((a) > (b) ? (a) : (b)) 80 | #define min(a,b) ((a) < (b) ? (a) : (b)) 81 | 82 | static int seconds_mode = SECONDS_MODE_NEVER; 83 | static int battery_mode = BATTERY_MODE_IF_LOW; 84 | static int date_pos = PBL_IF_ROUND_ELSE(DATE_POS_OFF, DATE_POS_BOTTOM); 85 | static int date_mode = DATE_MODE_EN; 86 | static int bluetooth_mode = BLUETOOTH_MODE_ALWAYS; 87 | static int graphics_mode = PBL_IF_ROUND_ELSE(GRAPHICS_MODE_INVERT, GRAPHICS_MODE_NORMAL); 88 | static int connlost_mode = CONNLOST_MODE_WARN; 89 | static bool has_config = false; 90 | 91 | static Window *window; 92 | static Layer *background_layer; 93 | static Layer *hands_layer; 94 | static Layer *date_layer; 95 | static Layer *battery_layer; 96 | 97 | static GBitmap *logo; 98 | static BitmapLayer *logo_layer; 99 | 100 | static GBitmap *bluetooth_images[4]; 101 | static BitmapLayer *bluetooth_layer; 102 | 103 | static GColor bw_palette[2]; 104 | #define BG_COLOR bw_palette[0] 105 | #define FG_COLOR bw_palette[1] 106 | 107 | static struct tm *now = NULL; 108 | static int date_wday = -1; 109 | static int date_mday = -1; 110 | static bool hide_seconds = false; 111 | static bool was_connected = false; 112 | 113 | static GFont font; 114 | #define DATE_BUFFER_BYTES 32 115 | static char date_buffer[DATE_BUFFER_BYTES]; 116 | 117 | #if DEBUG 118 | static TextLayer *debug_layer; 119 | #define DEBUG_BUFFER_BYTES 32 120 | static char debug_buffer[DEBUG_BUFFER_BYTES]; 121 | #endif 122 | 123 | const GPathInfo HOUR_POINTS = { 124 | 6, 125 | (GPoint []) { 126 | { 6,-37}, 127 | { 3,-40}, 128 | {-3,-40}, 129 | {-6,-37}, 130 | {-6, 0}, 131 | { 6, 0}, 132 | } 133 | }; 134 | static GPath *hour_path; 135 | 136 | const GPathInfo MIN_POINTS = { 137 | 6, 138 | (GPoint []) { 139 | { 5,-57}, 140 | { 3,-61}, 141 | {-3,-61}, 142 | {-5,-57}, 143 | {-5, 0}, 144 | { 5, 0}, 145 | } 146 | }; 147 | static GPath *min_path; 148 | 149 | const GPathInfo SEC_POINTS = { 150 | 4, 151 | (GPoint []) { 152 | { 2, 0}, 153 | { 2,-61}, 154 | {-2,-61}, 155 | {-2, 0}, 156 | } 157 | }; 158 | static GPath *sec_path; 159 | 160 | const GPathInfo BATTERY_POINTS = { 161 | 8, 162 | (GPoint []) { 163 | { 7, 0}, 164 | {20, 0}, 165 | {20, 6}, 166 | {21, 2}, 167 | {21, 6}, 168 | {20, 6}, 169 | {20, 8}, 170 | { 7, 8}, 171 | } 172 | }; 173 | static GPath *battery_path; 174 | 175 | const GPathInfo CHARGE_POINTS = { 176 | 4, 177 | (GPoint []) { 178 | {3, 1}, 179 | {0, 4}, 180 | {3, 4}, 181 | {0, 7}, 182 | } 183 | }; 184 | static GPath *charge_path; 185 | 186 | const char WEEKDAY_NAMES[6][7][5] = { // 3 chars, 1 for utf-8, 1 for terminating 0 187 | {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}, 188 | {"So", "Mo", "Di", "Mi", "Do", "Fr", "Sa" }, 189 | {"Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb"}, 190 | {"Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"}, 191 | {"Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab"}, 192 | {"Sön", "Mån", "Tis", "Ons", "Tor", "Fre", "Lör"}, 193 | }; 194 | 195 | static Animation *startup_animation; 196 | static int32_t hour_angle = 0; 197 | static int32_t min_angle = 0; 198 | static int32_t sec_angle = 0; 199 | static GPoint hour_pos, hour_delta; 200 | static GPoint min_pos, min_delta; 201 | static GPoint sec_pos, sec_delta; 202 | static int32_t dots_radius = 0; 203 | 204 | void background_layer_update_callback(Layer *layer, GContext* ctx) { 205 | if (!dots_radius) return; 206 | graphics_context_set_fill_color(ctx, FG_COLOR); 207 | for (int32_t angle = 0; angle < THREESIXTY; angle += THREESIXTY / 12) { 208 | GPoint pos = GPoint( 209 | CENTER_X + dots_radius * cos_lookup(angle) / ONE, 210 | CENTER_Y - dots_radius * sin_lookup(angle) / ONE); 211 | graphics_fill_circle(ctx, pos, DOTS_SIZE); 212 | } 213 | } 214 | 215 | void update_angles() { 216 | hour_angle = THREESIXTY * (now->tm_hour * 5 + now->tm_min / 12) / 60; 217 | min_angle = THREESIXTY * now->tm_min / 60; 218 | sec_angle = THREESIXTY * now->tm_sec / 60; 219 | } 220 | 221 | void hands_layer_update_callback(Layer *layer, GContext* ctx) { 222 | #if SCREENSHOT 223 | now->tm_hour = 10; 224 | now->tm_min = 9; 225 | now->tm_sec = 36; 226 | #endif 227 | update_angles(); 228 | 229 | // hours and minutes 230 | gpath_rotate_to(hour_path, hour_angle); 231 | gpath_rotate_to(min_path, min_angle); 232 | graphics_context_set_fill_color(ctx, FG_COLOR); 233 | graphics_context_set_stroke_color(ctx, BG_COLOR); 234 | gpath_draw_filled(ctx, hour_path); 235 | gpath_draw_outline(ctx, hour_path); 236 | graphics_fill_circle(ctx, hour_pos, DOTS_SIZE+3); 237 | gpath_draw_filled(ctx, min_path); 238 | gpath_draw_outline(ctx, min_path); 239 | graphics_fill_circle(ctx, min_pos, DOTS_SIZE+3); 240 | 241 | // seconds 242 | if (!hide_seconds) { 243 | GPoint sec_end = GPoint( 244 | sec_pos.x + SEC_RADIUS * sin_lookup(sec_angle) / ONE, 245 | sec_pos.y - SEC_RADIUS * cos_lookup(sec_angle) / ONE); 246 | graphics_context_set_fill_color(ctx, BG_COLOR); 247 | gpath_rotate_to(sec_path, sec_angle); 248 | gpath_draw_filled(ctx, sec_path); 249 | graphics_context_set_stroke_color(ctx, FG_COLOR); 250 | graphics_context_set_compositing_mode(ctx, GCompOpAssignInverted); 251 | graphics_draw_line(ctx, sec_pos, sec_end); 252 | } 253 | 254 | // center dot 255 | graphics_context_set_fill_color(ctx, BG_COLOR); 256 | graphics_fill_circle(ctx, min_pos, DOTS_SIZE); 257 | } 258 | 259 | void date_layer_update_callback(Layer *layer, GContext* ctx) { 260 | 261 | #if SCREENSHOT 262 | now->tm_wday = 0; 263 | now->tm_mday = 25; 264 | #endif 265 | 266 | graphics_context_set_text_color(ctx, FG_COLOR); 267 | 268 | // weekday 269 | if (date_mode < DATE_MODE_FIRST || date_mode > DATE_MODE_LAST) 270 | date_mode = DATE_MODE_FIRST; 271 | graphics_draw_text(ctx, 272 | WEEKDAY_NAMES[date_mode - DATE_MODE_FIRST][now->tm_wday], 273 | font, 274 | GRect(0, -6, EXTENT, 32), 275 | GTextOverflowModeWordWrap, 276 | GTextAlignmentLeft, 277 | NULL); 278 | 279 | // day of month 280 | strftime(date_buffer, DATE_BUFFER_BYTES, "%e", now); 281 | graphics_draw_text(ctx, 282 | date_buffer, 283 | font, 284 | GRect(0, -6, EXTENT, 32), 285 | GTextOverflowModeWordWrap, 286 | GTextAlignmentRight, 287 | NULL); 288 | 289 | date_wday = now->tm_wday; 290 | date_mday = now->tm_mday; 291 | } 292 | 293 | void battery_layer_update_callback(Layer *layer, GContext* ctx) { 294 | BatteryChargeState battery = battery_state_service_peek(); 295 | graphics_context_set_stroke_color(ctx, FG_COLOR); 296 | gpath_draw_outline(ctx, battery_path); 297 | int width = battery.charge_percent * 10 / 100; 298 | #ifdef PBL_BW 299 | graphics_context_set_fill_color(ctx, FG_COLOR); 300 | #else 301 | graphics_context_set_fill_color(ctx, battery.is_plugged ? (width < 3 ? GColorRed : GColorGreen) : FG_COLOR); 302 | #endif 303 | graphics_fill_rect(ctx, GRect(9, 2, width, 5), 0, GCornerNone); 304 | if (battery.is_plugged) 305 | gpath_draw_outline_open(ctx, charge_path); 306 | } 307 | 308 | void handle_tick(struct tm *tick_time, TimeUnits units_changed) { 309 | time_t clock = time(NULL); 310 | now = localtime(&clock); 311 | layer_mark_dirty(hands_layer); 312 | if (date_pos != DATE_POS_OFF && (now->tm_wday != date_wday || now->tm_mday != date_mday)) 313 | layer_mark_dirty(date_layer); 314 | } 315 | 316 | void handle_app_did_focus(bool in_focus) { 317 | if (in_focus && startup_animation) { 318 | animation_schedule(startup_animation); 319 | startup_animation = NULL; 320 | } 321 | } 322 | 323 | void startup_animation_init() { 324 | update_angles(); 325 | hour_delta = GPoint( 326 | DOTS_RADIUS * sin_lookup(hour_angle) / ONE, 327 | -DOTS_RADIUS * cos_lookup(hour_angle) / ONE); 328 | gpath_move_to(hour_path, GPoint(CENTER_X + hour_delta.x, CENTER_Y + hour_delta.y)); 329 | min_delta = GPoint( 330 | DOTS_RADIUS * sin_lookup(min_angle) / ONE, 331 | -DOTS_RADIUS * cos_lookup(min_angle) / ONE); 332 | gpath_move_to(min_path, GPoint(CENTER_X + min_delta.x, CENTER_Y + min_delta.y)); 333 | sec_delta = GPoint( 334 | DOTS_RADIUS * sin_lookup(sec_angle) / ONE, 335 | -DOTS_RADIUS * cos_lookup(sec_angle) / ONE); 336 | gpath_move_to(sec_path, GPoint(CENTER_X + sec_delta.x, CENTER_Y + sec_delta.y)); 337 | } 338 | 339 | void startup_animation_update(Animation *animation, const AnimationProgress progress) { 340 | dots_radius = DOTS_RADIUS * progress / ANIMATION_NORMALIZED_MAX; 341 | AnimationProgress reverse = ANIMATION_NORMALIZED_MAX - progress; 342 | hour_pos = GPoint( 343 | CENTER_X + hour_delta.x * reverse / ANIMATION_NORMALIZED_MAX, 344 | CENTER_Y + hour_delta.y * reverse / ANIMATION_NORMALIZED_MAX); 345 | gpath_move_to(hour_path, hour_pos); 346 | min_pos = GPoint( 347 | CENTER_X + min_delta.x * reverse / ANIMATION_NORMALIZED_MAX, 348 | CENTER_Y + min_delta.y * reverse / ANIMATION_NORMALIZED_MAX); 349 | gpath_move_to(min_path, min_pos); 350 | sec_pos = GPoint( 351 | CENTER_X + sec_delta.x * reverse / ANIMATION_NORMALIZED_MAX, 352 | CENTER_Y + sec_delta.y * reverse / ANIMATION_NORMALIZED_MAX); 353 | gpath_move_to(sec_path, sec_pos); 354 | layer_mark_dirty(background_layer); 355 | layer_mark_dirty(hands_layer); 356 | } 357 | 358 | void startup_animation_teardown(Animation *animation) { 359 | startup_animation_update(animation, ANIMATION_NORMALIZED_MAX); 360 | animation_destroy(animation); 361 | } 362 | 363 | static AnimationImplementation startup_animation_implementation = { 364 | .update = startup_animation_update, 365 | .teardown = startup_animation_teardown 366 | }; 367 | 368 | void lost_connection_warning(void *); 369 | 370 | void handle_bluetooth(bool connected) { 371 | bitmap_layer_set_bitmap(bluetooth_layer, bluetooth_images[connected ? 1 : 0]); 372 | layer_set_hidden(bitmap_layer_get_layer(bluetooth_layer), 373 | bluetooth_mode == BLUETOOTH_MODE_NEVER || 374 | (bluetooth_mode == BLUETOOTH_MODE_IFOFF && connected)); 375 | if (was_connected && !connected && connlost_mode == CONNLOST_MODE_WARN) 376 | lost_connection_warning((void*) 0); 377 | was_connected = connected; 378 | } 379 | 380 | void lost_connection_warning(void *data) { 381 | int count = (int) data; 382 | bool on_off = count & 1; 383 | // blink icon 384 | bitmap_layer_set_bitmap(bluetooth_layer, bluetooth_images[on_off ? 1 : 0]); 385 | layer_set_hidden(bitmap_layer_get_layer(bluetooth_layer), false); 386 | // buzz 3 times 387 | if (count < 6 && !on_off) 388 | vibes_short_pulse(); 389 | if (count < 50) // blink for 15 seconds 390 | app_timer_register(300, lost_connection_warning, (void*) (count+1)); 391 | else // restore bluetooth icon 392 | handle_bluetooth(bluetooth_connection_service_peek()); 393 | } 394 | 395 | void handle_layout() { 396 | BatteryChargeState charge_state = battery_state_service_peek(); 397 | bool battery_is_low = charge_state.charge_percent <= 20; 398 | bool showSeconds = seconds_mode == SECONDS_MODE_ALWAYS 399 | || (seconds_mode == SECONDS_MODE_IFNOTLOW && (!battery_is_low || charge_state.is_plugged)); 400 | bool showBattery = battery_mode == BATTERY_MODE_ALWAYS 401 | || (battery_mode == BATTERY_MODE_IF_LOW && battery_is_low) 402 | || charge_state.is_plugged; 403 | int face_top = PBL_IF_ROUND_ELSE(0, date_pos == DATE_POS_BOTTOM ? 0 : date_pos == DATE_POS_OFF ? 12 : 24); 404 | int date_top = date_pos == DATE_POS_TOP ? 0 : EXTENT; 405 | int battery_top = date_pos == DATE_POS_TOP ? 168-10-3 : 3; 406 | if (hide_seconds != !showSeconds) { 407 | hide_seconds = !showSeconds; 408 | tick_timer_service_unsubscribe(); 409 | tick_timer_service_subscribe(hide_seconds ? MINUTE_UNIT : SECOND_UNIT, &handle_tick); 410 | } 411 | layer_set_hidden(date_layer, date_pos == DATE_POS_OFF); 412 | layer_set_hidden(battery_layer, !showBattery); 413 | layer_set_frame(background_layer, GRect(0, face_top, EXTENT, EXTENT)); 414 | layer_set_frame(date_layer, GRect(0, date_top, EXTENT, 24)); 415 | layer_set_frame(battery_layer, GRect(EXTENT-22-3, battery_top, 22, 10)); 416 | FG_COLOR = graphics_mode == GRAPHICS_MODE_INVERT ? GColorBlack : GColorWhite; 417 | BG_COLOR = graphics_mode == GRAPHICS_MODE_INVERT ? GColorWhite : GColorBlack; 418 | window_set_background_color(window, BG_COLOR); 419 | } 420 | 421 | void handle_battery(BatteryChargeState charge_state) { 422 | handle_layout(); 423 | layer_mark_dirty(battery_layer); 424 | } 425 | 426 | void handle_appmessage_receive(DictionaryIterator *received, void *context) { 427 | Tuple *tuple = dict_read_first(received); 428 | while (tuple) { 429 | switch (tuple->key) { 430 | case SECONDS_MODE: 431 | seconds_mode = tuple->value->int32; 432 | break; 433 | case BATTERY_MODE: 434 | battery_mode = tuple->value->int32; 435 | break; 436 | case DATE_POS: 437 | date_pos = tuple->value->int32; 438 | break; 439 | case DATE_MODE: 440 | date_mode = tuple->value->int32; 441 | break; 442 | case BLUETOOTH_MODE: 443 | bluetooth_mode = tuple->value->int32; 444 | break; 445 | case GRAPHICS_MODE: 446 | graphics_mode = tuple->value->int32; 447 | break; 448 | case CONNLOST_MODE: 449 | connlost_mode = tuple->value->int32; 450 | break; 451 | } 452 | tuple = dict_read_next(received); 453 | } 454 | APP_LOG(APP_LOG_LEVEL_DEBUG, "Received config"); 455 | has_config = true; 456 | handle_bluetooth(bluetooth_connection_service_peek()); 457 | handle_layout(); 458 | layer_mark_dirty(battery_layer); 459 | layer_mark_dirty(hands_layer); 460 | layer_mark_dirty(date_layer); 461 | layer_mark_dirty(background_layer); 462 | } 463 | 464 | void request_config(void) { 465 | APP_LOG(APP_LOG_LEVEL_DEBUG, "Requesting config"); 466 | Tuplet request_tuple = TupletInteger(REQUEST_CONFIG, 1); 467 | DictionaryIterator *iter; 468 | app_message_outbox_begin(&iter); 469 | if (!iter) return; 470 | dict_write_tuplet(iter, &request_tuple); 471 | dict_write_end(iter); 472 | app_message_outbox_send(); 473 | } 474 | 475 | 476 | void handle_init() { 477 | time_t clock = time(NULL); 478 | now = localtime(&clock); 479 | window = window_create(); 480 | window_stack_push(window, true /* Animated */); 481 | window_set_background_color(window, BG_COLOR); 482 | 483 | date_layer = layer_create(GRect(0, EXTENT, EXTENT, 24)); 484 | layer_set_update_proc(date_layer, &date_layer_update_callback); 485 | layer_add_child(window_get_root_layer(window), date_layer); 486 | 487 | background_layer = layer_create(GRect(0, 0, EXTENT, EXTENT)); 488 | layer_set_update_proc(background_layer, &background_layer_update_callback); 489 | layer_add_child(window_get_root_layer(window), background_layer); 490 | 491 | logo = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_LOGO); 492 | gbitmap_set_palette(logo, bw_palette, false); 493 | GRect frame = gbitmap_get_bounds(logo); 494 | grect_align(&frame, &GRect(0, 0, EXTENT, CENTER_Y), GAlignCenter, false); 495 | logo_layer = bitmap_layer_create(frame); 496 | bitmap_layer_set_bitmap (logo_layer, logo); 497 | layer_add_child(background_layer, bitmap_layer_get_layer(logo_layer)); 498 | 499 | hands_layer = layer_create(layer_get_frame(background_layer)); 500 | layer_set_update_proc(hands_layer, &hands_layer_update_callback); 501 | layer_add_child(background_layer, hands_layer); 502 | 503 | battery_layer = layer_create(GRect(EXTENT-22-3, 3, 22, 10)); 504 | layer_set_update_proc(battery_layer, &battery_layer_update_callback); 505 | layer_add_child(window_get_root_layer(window), battery_layer); 506 | 507 | for (int i = 0; i < 2; i++) { 508 | bluetooth_images[i] = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_BLUETOOTH_OFF + i); 509 | gbitmap_set_palette(bluetooth_images[i], bw_palette, false); 510 | } 511 | bluetooth_layer = bitmap_layer_create(GRect(CENTER_X - 6, CENTER_Y - DOTS_RADIUS - 4, 13, 13)); 512 | layer_add_child(background_layer, bitmap_layer_get_layer(bluetooth_layer)); 513 | 514 | #if DEBUG 515 | debug_layer = text_layer_create(GRect(0, 0, 32, 16)); 516 | strcpy(debug_buffer, ""); 517 | text_layer_set_text(debug_layer, debug_buffer); 518 | text_layer_set_text_color(debug_layer, FG_COLOR); 519 | text_layer_set_background_color(debug_layer, BG_COLOR); 520 | layer_add_child(window_get_root_layer(window), text_layer_get_layer(debug_layer)); 521 | #endif 522 | 523 | hour_path = gpath_create(&HOUR_POINTS); 524 | gpath_move_to(hour_path, GPoint(CENTER_X, CENTER_Y)); 525 | min_path = gpath_create(&MIN_POINTS); 526 | gpath_move_to(min_path, GPoint(CENTER_X, CENTER_Y)); 527 | sec_path = gpath_create(&SEC_POINTS); 528 | gpath_move_to(sec_path, GPoint(CENTER_X, CENTER_Y)); 529 | battery_path = gpath_create(&BATTERY_POINTS); 530 | charge_path = gpath_create(&CHARGE_POINTS); 531 | 532 | startup_animation = animation_create(); 533 | animation_set_curve(startup_animation, AnimationCurveEaseOut); 534 | animation_set_implementation(startup_animation, &startup_animation_implementation); 535 | startup_animation_init(); 536 | app_focus_service_subscribe_handlers((AppFocusHandlers){ 537 | .did_focus = handle_app_did_focus, 538 | }); 539 | 540 | font = fonts_load_custom_font(resource_get_handle(RESOURCE_ID_FONT_30)); 541 | 542 | has_config = true; 543 | if (persist_exists(SECONDS_MODE)) seconds_mode = persist_read_int(SECONDS_MODE); else has_config = false; 544 | if (persist_exists(BATTERY_MODE)) battery_mode = persist_read_int(BATTERY_MODE); else has_config = false; 545 | if (persist_exists(DATE_POS)) date_pos = persist_read_int(DATE_POS); // added in 2.7 546 | if (persist_exists(DATE_MODE)) date_mode = persist_read_int(DATE_MODE); else has_config = false; 547 | if (persist_exists(BLUETOOTH_MODE)) bluetooth_mode = persist_read_int(BLUETOOTH_MODE); else has_config = false; 548 | if (persist_exists(GRAPHICS_MODE)) graphics_mode = persist_read_int(GRAPHICS_MODE); else has_config = false; 549 | if (persist_exists(CONNLOST_MODE)) connlost_mode = persist_read_int(CONNLOST_MODE); else has_config = false; 550 | if (has_config) APP_LOG(APP_LOG_LEVEL_DEBUG, "Loaded config"); 551 | tick_timer_service_subscribe(hide_seconds ? MINUTE_UNIT : SECOND_UNIT, &handle_tick); 552 | handle_layout(); 553 | battery_state_service_subscribe(&handle_battery); 554 | bluetooth_connection_service_subscribe(&handle_bluetooth); 555 | handle_bluetooth(bluetooth_connection_service_peek()); 556 | app_message_register_inbox_received(&handle_appmessage_receive); 557 | app_message_open(INBOX_SIZE, OUTBOX_SIZE); 558 | if (!has_config) request_config(); 559 | } 560 | 561 | void handle_deinit() { 562 | app_message_deregister_callbacks(); 563 | battery_state_service_unsubscribe(); 564 | tick_timer_service_unsubscribe(); 565 | if (has_config) { 566 | persist_write_int(SECONDS_MODE, seconds_mode); 567 | persist_write_int(BATTERY_MODE, battery_mode); 568 | persist_write_int(DATE_POS, date_pos); 569 | persist_write_int(DATE_MODE, date_mode); 570 | persist_write_int(BLUETOOTH_MODE, bluetooth_mode); 571 | persist_write_int(GRAPHICS_MODE, graphics_mode); 572 | persist_write_int(CONNLOST_MODE, connlost_mode); 573 | APP_LOG(APP_LOG_LEVEL_DEBUG, "Wrote config"); 574 | } else { 575 | APP_LOG(APP_LOG_LEVEL_DEBUG, "Did not write config"); 576 | } 577 | fonts_unload_custom_font(font); 578 | app_focus_service_unsubscribe(); 579 | animation_unschedule_all(); 580 | gpath_destroy(charge_path); 581 | gpath_destroy(battery_path); 582 | gpath_destroy(sec_path); 583 | gpath_destroy(min_path); 584 | gpath_destroy(hour_path); 585 | #if DEBUG 586 | text_layer_destroy(debug_layer); 587 | #endif 588 | layer_destroy(hands_layer); 589 | bitmap_layer_destroy(logo_layer); 590 | gbitmap_destroy(logo); 591 | layer_destroy(battery_layer); 592 | bitmap_layer_destroy(bluetooth_layer); 593 | for (int i = 0; i < 2; i++) 594 | gbitmap_destroy(bluetooth_images[i]); 595 | layer_destroy(background_layer); 596 | layer_destroy(date_layer); 597 | window_destroy(window); 598 | } 599 | 600 | int main(void) { 601 | handle_init(); 602 | app_event_loop(); 603 | handle_deinit(); 604 | } 605 | -------------------------------------------------------------------------------- /src/pebble_one.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Bert Freudenberg 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | // the config dict is sent as app message to the watch 24 | var SECONDS_MODE_NEVER = 0, 25 | SECONDS_MODE_IFNOTLOW = 1, 26 | SECONDS_MODE_ALWAYS = 2, 27 | BATTERY_MODE_NEVER = 0, 28 | BATTERY_MODE_IF_LOW = 1, 29 | BATTERY_MODE_ALWAYS = 2, 30 | DATE_POS_OFF = 0, 31 | DATE_POS_TOP = 1, 32 | DATE_POS_BOTTOM = 2, 33 | DATE_MODE_OFF = 0, // not used anymore 34 | DATE_MODE_EN = 1, 35 | DATE_MODE_DE = 2, 36 | DATE_MODE_ES = 3, 37 | DATE_MODE_FR = 4, 38 | DATE_MODE_IT = 5, 39 | DATE_MODE_SE = 6, 40 | BLUETOOTH_MODE_NEVER = 0, 41 | BLUETOOTH_MODE_IFOFF = 1, 42 | BLUETOOTH_MODE_ALWAYS = 2, 43 | GRAPHICS_MODE_NORMAL = 0, 44 | GRAPHICS_MODE_INVERT = 1, 45 | CONNLOST_MODE_IGNORE = 0, 46 | CONNLOST_MODE_WARN = 1, 47 | LOCALE_MODE_EN = 0, 48 | LOCALE_MODE_DE = 1, 49 | LOCALE_MODE_COUNT = 2; 50 | 51 | var config = { 52 | seconds_mode: SECONDS_MODE_NEVER, 53 | battery_mode: BATTERY_MODE_IF_LOW, 54 | date_pos: DATE_POS_BOTTOM, 55 | date_mode: DATE_MODE_EN, 56 | bluetooth_mode: BLUETOOTH_MODE_ALWAYS, 57 | graphics_mode: GRAPHICS_MODE_INVERT, 58 | connlost_mode: CONNLOST_MODE_WARN, 59 | }; 60 | 61 | var send_in_progress = false; 62 | 63 | // config.html will be included by build process, see build/src/js/pebble-js-app.js 64 | var config_html; 65 | 66 | function send_config_to_pebble() { 67 | if (send_in_progress) { 68 | return console.log("cannot send config, already in progress"); 69 | } 70 | send_in_progress = true; 71 | console.log("sending config " + JSON.stringify(config)); 72 | window.localStorage.setItem('pebbleNeedsConfig', 'true'); 73 | Pebble.sendAppMessage(config, 74 | function ack(e) { 75 | console.log("Successfully delivered message " + JSON.stringify(e.data)); 76 | send_in_progress = false; 77 | window.localStorage.removeItem('pebbleNeedsConfig'); 78 | }, 79 | function nack(e) { 80 | console.log("Unable to deliver message " + JSON.stringify(e)); 81 | send_in_progress = false; 82 | }); 83 | } 84 | 85 | // read config from persistent storage 86 | Pebble.addEventListener('ready', 87 | function () { 88 | var json = window.localStorage.getItem('config'); 89 | if (typeof json === 'string') { 90 | config = JSON.parse(json); 91 | if (config.date_mode == DATE_MODE_OFF) { // version 2.7 introduced date_pos 92 | config.date_mode = DATE_MODE_EN; 93 | config.date_pos = DATE_POS_OFF; 94 | } else if (config.date_pos === null) { 95 | config.date_pos = DATE_POS_BOTTOM; 96 | } 97 | console.log("loaded config " + JSON.stringify(config)); 98 | } 99 | if (window.localStorage.getItem('pebbleNeedsConfig')) { 100 | send_config_to_pebble(); 101 | } 102 | }); 103 | 104 | // got message from pebble 105 | Pebble.addEventListener("appmessage", 106 | function(e) { 107 | console.log("got message " + JSON.stringify(e.payload)); 108 | if (e.payload.request_config) { 109 | send_config_to_pebble(); 110 | } 111 | }); 112 | 113 | // open config window 114 | Pebble.addEventListener('showConfiguration', 115 | function () { 116 | console.log("show config window " + JSON.stringify(config)); 117 | var html = config_html.replace('"REPLACE_ME_AT_RUNTIME"', JSON.stringify(config), 'g'); 118 | Pebble.openURL('data:text/html,' + encodeURIComponent(html + '