├── .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 | 
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 |
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 + '