forked from joeycastillo/second-movement
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmovement.c
More file actions
1539 lines (1266 loc) · 57.6 KB
/
Copy pathmovement.c
File metadata and controls
1539 lines (1266 loc) · 57.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* MIT License
*
* Copyright (c) 2022 Joey Castillo
* Copyright (c) 2025 Alessandro Genova
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#define MOVEMENT_LONG_PRESS_TICKS 64
#define MOVEMENT_REALLY_LONG_PRESS_TICKS 192
#define MOVEMENT_MAX_LONG_PRESS_TICKS 1280 // get a chance to check if a button held down over 10 seconds is a glitch
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <stdlib.h>
#include "app.h"
#include "watch.h"
#include "watch_utility.h"
#include "usb.h"
#include "watch_private.h"
#include "movement.h"
#include "filesystem.h"
#include "shell.h"
#include "utz.h"
#include "zones.h"
#include "tc.h"
#include "evsys.h"
#include "delay.h"
#include "thermistor_driver.h"
#include "movement_config.h"
#include "movement_custom_signal_tunes.h"
#if __EMSCRIPTEN__
#include <emscripten.h>
void _wake_up_simulator(void);
#else
#include "watch_usb_cdc.h"
#endif
volatile movement_state_t movement_state;
void * watch_face_contexts[MOVEMENT_NUM_FACES];
watch_date_time_t scheduled_tasks[MOVEMENT_NUM_FACES];
const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800};
const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800};
const uint32_t _movement_mode_button_events_mask = 0b11111 << EVENT_MODE_BUTTON_DOWN;
const uint32_t _movement_light_button_events_mask = 0b11111 << EVENT_LIGHT_BUTTON_DOWN;
const uint32_t _movement_alarm_button_events_mask = 0b11111 << EVENT_ALARM_BUTTON_DOWN;
const uint32_t _movement_button_events_mask = _movement_mode_button_events_mask | _movement_light_button_events_mask | _movement_alarm_button_events_mask;
typedef struct {
movement_event_type_t down_event;
watch_cb_t cb_longpress;
movement_timeout_index_t timeout_index;
volatile bool is_down;
volatile rtc_counter_t down_timestamp;
#if MOVEMENT_DEBOUNCE_TICKS
volatile rtc_counter_t up_timestamp;
#endif
} movement_button_t;
/* Pieces of state that can be modified by the various interrupt callbacks.
The interrupt writes state changes here, and it will be acted upon on the next app_loop invokation.
*/
typedef struct {
volatile uint32_t pending_events;
volatile bool turn_led_off;
volatile bool has_pending_sequence;
volatile bool enter_sleep_mode;
volatile bool exit_sleep_mode;
volatile bool is_sleeping;
volatile uint8_t subsecond;
volatile rtc_counter_t minute_counter;
volatile bool minute_alarm_fired;
volatile bool is_buzzing;
volatile uint8_t pending_sequence_priority;
volatile bool schedule_next_comp;
volatile bool has_pending_accelerometer;
// button tracking for long press
movement_button_t mode_button;
movement_button_t light_button;
movement_button_t alarm_button;
// button events that will not be passed to the current face loop, but will instead passed directly to the default loop handler.
volatile uint32_t passthrough_events;
} movement_volatile_state_t;
movement_volatile_state_t movement_volatile_state;
// The last sequence that we have been asked to play while the watch was in deep sleep
static int8_t *_pending_sequence;
// The note sequence of the default alarm
int8_t alarm_tune[] = {
BUZZER_NOTE_C8, 3,
BUZZER_NOTE_REST, 4,
BUZZER_NOTE_C8, 3,
BUZZER_NOTE_REST, 4,
BUZZER_NOTE_C8, 3,
BUZZER_NOTE_REST, 4,
BUZZER_NOTE_C8, 5,
BUZZER_NOTE_REST, 38,
-8, 9,
0
};
int8_t _movement_dst_offset_cache[NUM_ZONE_NAMES] = {0};
#define TIMEZONE_DOES_NOT_OBSERVE (-127)
void cb_mode_btn_interrupt(void);
void cb_light_btn_interrupt(void);
void cb_alarm_btn_interrupt(void);
void cb_alarm_btn_extwake(void);
void cb_minute_alarm_fired(void);
void cb_tick(void);
void cb_mode_btn_timeout_interrupt(void);
void cb_light_btn_timeout_interrupt(void);
void cb_alarm_btn_timeout_interrupt(void);
void cb_led_timeout_interrupt(void);
void cb_resign_timeout_interrupt(void);
void cb_sleep_timeout_interrupt(void);
void cb_buzzer_start(void);
void cb_buzzer_stop(void);
void cb_accelerometer_event(void);
void cb_accelerometer_wake(void);
#if __EMSCRIPTEN__
void yield(void) {
}
#else
void yield(void) {
tud_task();
cdc_task();
}
#endif
static udatetime_t _movement_convert_date_time_to_udate(watch_date_time_t date_time) {
return (udatetime_t) {
.date.dayofmonth = date_time.unit.day,
.date.dayofweek = dayofweek(UYEAR_FROM_YEAR(date_time.unit.year + WATCH_RTC_REFERENCE_YEAR), date_time.unit.month, date_time.unit.day),
.date.month = date_time.unit.month,
.date.year = UYEAR_FROM_YEAR(date_time.unit.year + WATCH_RTC_REFERENCE_YEAR),
.time.hour = date_time.unit.hour,
.time.minute = date_time.unit.minute,
.time.second = date_time.unit.second
};
}
static watch_buzzer_volume_t _movement_get_buzzer_volume(movement_buzzer_priority_t priority) {
switch (priority) {
case BUZZER_PRIORITY_BUTTON:
return movement_button_volume();
case BUZZER_PRIORITY_SIGNAL:
return movement_signal_volume();
case BUZZER_PRIORITY_ALARM:
return movement_alarm_volume();
default:
return WATCH_BUZZER_VOLUME_LOUD;
}
}
static void _movement_set_top_of_minute_alarm() {
uint32_t counter = watch_rtc_get_counter();
uint32_t next_minute_counter;
watch_date_time_t date_time = watch_rtc_get_date_time();
uint32_t freq = watch_rtc_get_frequency();
uint32_t half_freq = freq >> 1;
uint32_t subsecond_mask = freq - 1;
uint32_t ticks_per_minute = watch_rtc_get_ticks_per_minute();
// get the counter at the last second tick
next_minute_counter = counter & (~subsecond_mask);
// add/subtract half second shift to sync up second tick with the 1Hz interrupt
next_minute_counter += (counter & subsecond_mask) >= half_freq ? half_freq : -half_freq;
// counter at the next top of the minute
next_minute_counter += (60 - date_time.unit.second) * freq;
// Since the minute alarm is very important, double/triple check to make sure that it will fire.
// These are theoretical corner cases that probably can't even happen, but since we do a subtraction
// above I wanna be certain that we don't schedule the next alarm at a counter value just before the
// current counter, which would result in the alarm firing after more than one year.
// This should be robust to the counter overflow, and we should ever iterate once at most.
if (next_minute_counter == counter) {
next_minute_counter += ticks_per_minute;
}
while ((next_minute_counter - counter) > ticks_per_minute) {
next_minute_counter += ticks_per_minute;
}
movement_volatile_state.minute_counter = next_minute_counter;
watch_rtc_register_comp_callback_no_schedule(cb_minute_alarm_fired, next_minute_counter, MINUTE_TIMEOUT);
movement_volatile_state.schedule_next_comp = true;
}
static bool _movement_update_dst_offset_cache(void) {
uzone_t local_zone;
udatetime_t udate_time;
bool dst_changed = false;
watch_date_time_t system_date_time = watch_rtc_get_date_time();
for (uint8_t i = 0; i < NUM_ZONE_NAMES; i++) {
unpack_zone(&zone_defns[i], "", &local_zone);
watch_date_time_t date_time = watch_utility_date_time_convert_zone(system_date_time, 0, local_zone.offset.hours * 3600 + local_zone.offset.minutes * 60);
if (!!local_zone.rules_len) {
// if local zone has DST rules, we need to see if DST applies.
udate_time = _movement_convert_date_time_to_udate(date_time);
uoffset_t offset;
get_current_offset(&local_zone, &udate_time, &offset);
int8_t new_offset = (offset.hours * 60 + offset.minutes) / 15;
if (_movement_dst_offset_cache[i] != new_offset) {
_movement_dst_offset_cache[i] = new_offset;
dst_changed = true;
}
} else {
// otherwise set the cache to a constant value that indicates no DST check needs to be performed.
_movement_dst_offset_cache[i] = TIMEZONE_DOES_NOT_OBSERVE;
}
}
return dst_changed;
}
static inline void _movement_reset_inactivity_countdown(void) {
rtc_counter_t counter = watch_rtc_get_counter();
uint32_t freq = watch_rtc_get_frequency();
watch_rtc_register_comp_callback_no_schedule(
cb_resign_timeout_interrupt,
counter + movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval] * freq,
RESIGN_TIMEOUT
);
movement_volatile_state.enter_sleep_mode = false;
watch_rtc_register_comp_callback_no_schedule(
cb_sleep_timeout_interrupt,
counter + movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval] * freq,
SLEEP_TIMEOUT
);
movement_volatile_state.schedule_next_comp = true;
}
static inline void _movement_disable_inactivity_countdown(void) {
watch_rtc_disable_comp_callback_no_schedule(RESIGN_TIMEOUT);
watch_rtc_disable_comp_callback_no_schedule(SLEEP_TIMEOUT);
movement_volatile_state.schedule_next_comp = true;
}
static void _movement_renew_top_of_minute_alarm(void) {
// Renew the alarm for a minute from the previous one (ensures no drift)
movement_volatile_state.minute_counter += watch_rtc_get_ticks_per_minute();
watch_rtc_register_comp_callback_no_schedule(cb_minute_alarm_fired, movement_volatile_state.minute_counter, MINUTE_TIMEOUT);
movement_volatile_state.schedule_next_comp = true;
}
static uint32_t _movement_get_accelerometer_events() {
uint32_t accelerometer_events = 0;
uint8_t int_src = lis2dw_get_interrupt_source();
if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) {
accelerometer_events |= 1 << EVENT_DOUBLE_TAP;
printf("Double tap!\r\n");
}
if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) {
accelerometer_events |= 1 << EVENT_SINGLE_TAP;
printf("Single tap!\r\n");
}
return accelerometer_events;
}
static void _movement_handle_button_presses(uint32_t pending_events) {
bool any_up = false;
bool any_down = false;
bool any_long = false;
movement_button_t* buttons[3] = {
&movement_volatile_state.mode_button,
&movement_volatile_state.light_button,
&movement_volatile_state.alarm_button
};
uint32_t button_events_masks[3] = {
_movement_mode_button_events_mask,
_movement_light_button_events_mask,
_movement_alarm_button_events_mask,
};
for (uint8_t i = 0; i < 3; i++) {
movement_button_t* button = buttons[i];
// If a button down occurred
if (pending_events & (1 << button->down_event)) {
watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index);
any_down = true;
// this button's events will start getting passed to the face
movement_volatile_state.passthrough_events &= ~button_events_masks[i];
}
// If a long press occurred
if (pending_events & (1 << (button->down_event + 2))) {
watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_REALLY_LONG_PRESS_TICKS, button->timeout_index);
any_long = true;
}
// If a really long press occurred
if (pending_events & (1 << (button->down_event + 4))) {
watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_MAX_LONG_PRESS_TICKS, button->timeout_index);
any_long = true;
}
// If a button up or button long up occurred
if (pending_events & (
(1 << (button->down_event + 1)) |
(1 << (button->down_event + 3))
// (1 << (button->down_event + 5))
)) {
// We cancel the timeout if it hasn't fired yet
watch_rtc_disable_comp_callback_no_schedule(button->timeout_index);
any_up = true;
}
}
if (any_down) {
// force alarm off if the user pressed a button.
watch_buzzer_abort_sequence();
// Delay auto light off if the user is still interacting with the watch.
if (movement_state.light_on) {
movement_illuminate_led();
}
}
if (any_down || any_up || any_long) {
_movement_reset_inactivity_countdown();
movement_volatile_state.schedule_next_comp = true;
}
}
static void _movement_handle_top_of_minute(void) {
watch_date_time_t date_time = watch_rtc_get_date_time();
// update the DST offset cache every 30 minutes, since someplace in the world could change.
if (date_time.unit.minute % 30 == 0) {
_movement_update_dst_offset_cache();
}
for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
// For each face that offers an advisory...
if (watch_faces[i].advise != NULL) {
// ...we ask for one.
movement_watch_face_advisory_t advisory = watch_faces[i].advise(watch_face_contexts[i]);
// If it wants a background task...
if (advisory.wants_background_task) {
// we give it one. pretty straightforward!
movement_event_t background_event = { EVENT_BACKGROUND_TASK, 0 };
watch_faces[i].loop(background_event, watch_face_contexts[i]);
}
// TODO: handle other advisory types
}
}
}
static void _movement_handle_scheduled_tasks(void) {
watch_date_time_t date_time = watch_rtc_get_date_time();
uint8_t num_active_tasks = 0;
for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
if (scheduled_tasks[i].reg) {
if (scheduled_tasks[i].reg <= date_time.reg) {
scheduled_tasks[i].reg = 0;
movement_event_t background_event = { EVENT_BACKGROUND_TASK, 0 };
watch_faces[i].loop(background_event, watch_face_contexts[i]);
// check if loop scheduled a new task
if (scheduled_tasks[i].reg) {
num_active_tasks++;
}
} else {
num_active_tasks++;
}
}
}
if (num_active_tasks == 0) {
movement_state.has_scheduled_background_task = false;
} else {
_movement_reset_inactivity_countdown();
}
}
void movement_request_tick_frequency(uint8_t freq) {
// Movement requires at least a 1 Hz tick.
// If we are asked for an invalid frequency, default back to 1 Hz.
if (freq == 0 || __builtin_popcount(freq) != 1) freq = 1;
// disable all periodic callbacks
watch_rtc_disable_matching_periodic_callbacks(0xFF);
// this left-justifies the period in a 32-bit integer.
uint32_t tmp = (freq & 0xFF) << 24;
// now we can count the leading zeroes to get the value we need.
// 0x01 (1 Hz) will have 7 leading zeros for PER7. 0x80 (128 Hz) will have no leading zeroes for PER0.
uint8_t per_n = __builtin_clz(tmp);
movement_state.tick_frequency = freq;
movement_state.tick_pern = per_n;
watch_rtc_register_periodic_callback(cb_tick, freq);
}
void movement_illuminate_led(void) {
if (movement_state.settings.bit.led_duration != 0b111) {
movement_state.light_on = true;
watch_set_led_color_rgb(movement_state.settings.bit.led_red_color | movement_state.settings.bit.led_red_color << 4,
movement_state.settings.bit.led_green_color | movement_state.settings.bit.led_green_color << 4,
movement_state.settings.bit.led_blue_color | movement_state.settings.bit.led_blue_color << 4);
if (movement_state.settings.bit.led_duration == 0) {
// Do nothing it'll be turned off on button release
} else {
// Set a timeout to turn off the light
rtc_counter_t counter = watch_rtc_get_counter();
uint32_t freq = watch_rtc_get_frequency();
watch_rtc_register_comp_callback_no_schedule(
cb_led_timeout_interrupt,
counter + (movement_state.settings.bit.led_duration * 2 - 1) * freq,
LED_TIMEOUT
);
movement_volatile_state.schedule_next_comp = true;
}
}
}
void movement_force_led_on(uint8_t red, uint8_t green, uint8_t blue) {
// this is hacky, we need a way for watch faces to set an arbitrary color and prevent Movement from turning it right back off.
movement_state.light_on = true;
watch_set_led_color_rgb(red, green, blue);
// The led will stay on until movement_force_led_off is called, so disable the led timeout in case we were in the middle of it.
watch_rtc_disable_comp_callback_no_schedule(LED_TIMEOUT);
movement_volatile_state.schedule_next_comp = true;
}
void movement_force_led_off(void) {
movement_state.light_on = false;
// The led timeout probably already triggered, but still disable just in case we are switching off the light by other means
watch_rtc_disable_comp_callback_no_schedule(LED_TIMEOUT);
movement_volatile_state.schedule_next_comp = true;
watch_set_led_off();
}
bool movement_default_loop_handler(movement_event_t event) {
switch (event.event_type) {
case EVENT_MODE_BUTTON_UP:
movement_move_to_next_face();
break;
case EVENT_LIGHT_BUTTON_DOWN:
movement_illuminate_led();
break;
case EVENT_LIGHT_BUTTON_UP:
case EVENT_LIGHT_LONG_UP:
if (movement_state.settings.bit.led_duration == 0) {
movement_force_led_off();
}
break;
case EVENT_MODE_LONG_PRESS:
if (MOVEMENT_SECONDARY_FACE_INDEX && movement_state.current_face_idx == 0) {
movement_move_to_face(MOVEMENT_SECONDARY_FACE_INDEX);
} else {
movement_move_to_face(0);
}
break;
default:
break;
}
return true;
}
void movement_move_to_face(uint8_t watch_face_index) {
movement_state.watch_face_changed = true;
movement_state.next_face_idx = watch_face_index;
}
void movement_move_to_next_face(void) {
uint16_t face_max;
if (MOVEMENT_SECONDARY_FACE_INDEX) {
face_max = (movement_state.current_face_idx < (int16_t)MOVEMENT_SECONDARY_FACE_INDEX) ? MOVEMENT_SECONDARY_FACE_INDEX : MOVEMENT_NUM_FACES;
} else {
face_max = MOVEMENT_NUM_FACES;
}
movement_move_to_face((movement_state.current_face_idx + 1) % face_max);
}
void movement_schedule_background_task(watch_date_time_t date_time) {
movement_schedule_background_task_for_face(movement_state.current_face_idx, date_time);
}
void movement_cancel_background_task(void) {
movement_cancel_background_task_for_face(movement_state.current_face_idx);
}
void movement_schedule_background_task_for_face(uint8_t watch_face_index, watch_date_time_t date_time) {
watch_date_time_t now = watch_rtc_get_date_time();
if (date_time.reg > now.reg) {
movement_state.has_scheduled_background_task = true;
scheduled_tasks[watch_face_index].reg = date_time.reg;
}
}
void movement_cancel_background_task_for_face(uint8_t watch_face_index) {
scheduled_tasks[watch_face_index].reg = 0;
bool other_tasks_scheduled = false;
for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
if (scheduled_tasks[i].reg != 0) {
other_tasks_scheduled = true;
break;
}
}
movement_state.has_scheduled_background_task = other_tasks_scheduled;
}
void movement_request_sleep(void) {
movement_volatile_state.enter_sleep_mode = true;
}
void movement_request_wake() {
movement_volatile_state.exit_sleep_mode = true;
_movement_reset_inactivity_countdown();
}
void cb_buzzer_start(void) {
movement_volatile_state.is_buzzing = true;
}
void cb_buzzer_stop(void) {
movement_volatile_state.is_buzzing = false;
movement_volatile_state.pending_sequence_priority = 0;
}
void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms) {
static int8_t single_note_sequence[3];
single_note_sequence[0] = note;
// 64 ticks per second for the tc0
// Each tick is approximately 15ms
uint16_t duration = duration_ms / 15;
if (duration > 127) duration = 127;
single_note_sequence[1] = (int8_t)duration;
single_note_sequence[2] = 0;
movement_play_sequence(single_note_sequence, BUZZER_PRIORITY_BUTTON);
}
void movement_play_signal(void) {
movement_play_sequence(signal_tune, BUZZER_PRIORITY_SIGNAL);
}
void movement_play_alarm(void) {
movement_play_sequence(alarm_tune, BUZZER_PRIORITY_ALARM);
}
void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note) {
// Ugly but necessary to avoid breaking backward compatibility with some faces.
// Create an alarm tune on the fly with the specified note and repetition.
static int8_t custom_alarm_tune[19];
if (rounds == 0) rounds = 1;
if (rounds > 20) rounds = 20;
for (uint8_t i = 0; i < 9; i++) {
uint8_t note_idx = i * 2;
uint8_t duration_idx = note_idx + 1;
int8_t note = alarm_tune[note_idx];
int8_t duration = alarm_tune[duration_idx];
if (note == BUZZER_NOTE_C8) {
note = alarm_note;
} else if (note < 0) {
duration = rounds;
}
custom_alarm_tune[note_idx] = note;
custom_alarm_tune[duration_idx] = duration;
}
custom_alarm_tune[18] = 0;
movement_play_sequence(custom_alarm_tune, BUZZER_PRIORITY_ALARM);
}
void movement_play_sequence(int8_t *note_sequence, movement_buzzer_priority_t priority) {
// Priority is used to ensure that lower priority sequences don't cancel higher priority ones
// Priotity order: alarm(2) > signal(1) > note(0)
if (priority < movement_volatile_state.pending_sequence_priority) {
return;
}
movement_volatile_state.pending_sequence_priority = priority;
// The tcc is off during sleep, we can't play immediately.
// Ask to wake up the watch.
if (movement_volatile_state.is_sleeping) {
_pending_sequence = note_sequence;
movement_volatile_state.has_pending_sequence = true;
movement_volatile_state.exit_sleep_mode = true;
} else {
watch_buzzer_play_sequence_with_volume(note_sequence, NULL, _movement_get_buzzer_volume(priority));
}
}
uint8_t movement_claim_backup_register(void) {
// We use backup register 7 in watch_rtc to keep track of the reference time
if (movement_state.next_available_backup_register >= 7) return 0;
return movement_state.next_available_backup_register++;
}
int32_t movement_get_current_timezone_offset_for_zone(uint8_t zone_index) {
int8_t cached_dst_offset = _movement_dst_offset_cache[zone_index];
if (cached_dst_offset == TIMEZONE_DOES_NOT_OBSERVE) {
// if time zone doesn't observe DST, we can just return the standard time offset from the zone definition.
return (int32_t)zone_defns[zone_index].offset_inc_minutes * OFFSET_INCREMENT * 60;
} else {
// otherwise, we've precalculated the offset for this zone and can return it.
return (int32_t)cached_dst_offset * OFFSET_INCREMENT * 60;
}
}
int32_t movement_get_current_timezone_offset(void) {
return movement_get_current_timezone_offset_for_zone(movement_state.settings.bit.time_zone);
}
int32_t movement_get_timezone_index(void) {
return movement_state.settings.bit.time_zone;
}
void movement_set_timezone_index(uint8_t value) {
movement_state.settings.bit.time_zone = value;
}
watch_date_time_t movement_get_utc_date_time(void) {
return watch_rtc_get_date_time();
}
watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index) {
int32_t offset = movement_get_current_timezone_offset_for_zone(zone_index);
unix_timestamp_t timestamp = watch_rtc_get_unix_time();
return watch_utility_date_time_from_unix_time(timestamp, offset);
}
watch_date_time_t movement_get_local_date_time(void) {
static struct {
unix_timestamp_t timestamp;
rtc_date_time_t datetime;
} cached_date_time = {.datetime.reg=0, .timestamp=0};
unix_timestamp_t timestamp = watch_rtc_get_unix_time();
if (timestamp != cached_date_time.timestamp) {
cached_date_time.timestamp = timestamp;
cached_date_time.datetime = watch_utility_date_time_from_unix_time(timestamp, movement_get_current_timezone_offset());
}
return cached_date_time.datetime;
}
uint32_t movement_get_utc_timestamp(void) {
return watch_rtc_get_unix_time();
}
void movement_set_utc_date_time(watch_date_time_t date_time) {
movement_set_utc_timestamp(watch_utility_date_time_to_unix_time(date_time, 0));
}
void movement_set_local_date_time(watch_date_time_t date_time) {
int32_t current_offset = movement_get_current_timezone_offset();
movement_set_utc_timestamp(watch_utility_date_time_to_unix_time(date_time, current_offset));
}
void movement_set_utc_timestamp(uint32_t timestamp) {
watch_rtc_set_unix_time(timestamp);
// If the time was changed, the top of the minute alarm needs to be reset accordingly
_movement_set_top_of_minute_alarm();
// this may seem wasteful, but if the user's local time is in a zone that observes DST,
// they may have just crossed a DST boundary, which means the next call to this function
// could require a different offset to force local time back to UTC. Quelle horreur!
_movement_update_dst_offset_cache();
}
bool movement_button_should_sound(void) {
return movement_state.settings.bit.button_should_sound;
}
void movement_set_button_should_sound(bool value) {
movement_state.settings.bit.button_should_sound = value;
}
watch_buzzer_volume_t movement_button_volume(void) {
return movement_state.settings.bit.button_volume;
}
void movement_set_button_volume(watch_buzzer_volume_t value) {
movement_state.settings.bit.button_volume = value;
}
watch_buzzer_volume_t movement_signal_volume(void) {
return movement_state.signal_volume;
}
void movement_set_signal_volume(watch_buzzer_volume_t value) {
movement_state.signal_volume = value;
}
watch_buzzer_volume_t movement_alarm_volume(void) {
return movement_state.alarm_volume;
}
void movement_set_alarm_volume(watch_buzzer_volume_t value) {
movement_state.alarm_volume = value;
}
movement_clock_mode_t movement_clock_mode_24h(void) {
return movement_state.settings.bit.clock_mode_24h ? MOVEMENT_CLOCK_MODE_24H : MOVEMENT_CLOCK_MODE_12H;
}
void movement_set_clock_mode_24h(movement_clock_mode_t value) {
movement_state.settings.bit.clock_mode_24h = (value == MOVEMENT_CLOCK_MODE_24H);
}
bool movement_use_imperial_units(void) {
return movement_state.settings.bit.use_imperial_units;
}
void movement_set_use_imperial_units(bool value) {
movement_state.settings.bit.use_imperial_units = value;
}
uint8_t movement_get_fast_tick_timeout(void) {
return movement_state.settings.bit.to_interval;
}
void movement_set_fast_tick_timeout(uint8_t value) {
movement_state.settings.bit.to_interval = value;
}
uint8_t movement_get_low_energy_timeout(void) {
return movement_state.settings.bit.le_interval;
}
void movement_set_low_energy_timeout(uint8_t value) {
movement_state.settings.bit.le_interval = value;
}
movement_color_t movement_backlight_color(void) {
return (movement_color_t) {
.red = movement_state.settings.bit.led_red_color,
.green = movement_state.settings.bit.led_green_color,
.blue = movement_state.settings.bit.led_blue_color
};
}
void movement_set_backlight_color(movement_color_t color) {
movement_state.settings.bit.led_red_color = color.red;
movement_state.settings.bit.led_green_color = color.green;
movement_state.settings.bit.led_blue_color = color.blue;
}
uint8_t movement_get_backlight_dwell(void) {
return movement_state.settings.bit.led_duration;
}
void movement_set_backlight_dwell(uint8_t value) {
movement_state.settings.bit.led_duration = value;
}
void movement_store_settings(void) {
movement_settings_t old_settings;
filesystem_read_file("settings.u32", (char *)&old_settings, sizeof(movement_settings_t));
if (movement_state.settings.reg != old_settings.reg) {
filesystem_write_file("settings.u32", (char *)&movement_state.settings, sizeof(movement_settings_t));
}
}
bool movement_alarm_enabled(void) {
return movement_state.alarm_enabled;
}
void movement_set_alarm_enabled(bool value) {
movement_state.alarm_enabled = value;
}
bool movement_enable_tap_detection_if_available(void) {
if (movement_state.has_lis2dw) {
// configure tap duration threshold and enable Z axis
lis2dw_configure_tap_threshold(0, 0, 12, LIS2DW_REG_TAP_THS_Z_Z_AXIS_ENABLE);
lis2dw_configure_tap_duration(2, 2, 2);
// ramp data rate up to 400 Hz and high performance mode
lis2dw_set_low_noise_mode(true);
lis2dw_set_data_rate(LIS2DW_DATA_RATE_HP_400_HZ);
lis2dw_set_mode(LIS2DW_MODE_LOW_POWER);
lis2dw_enable_double_tap();
// Settling time (1 sample duration, i.e. 1/400Hz)
delay_ms(3);
// enable tap detection on INT1/A3.
lis2dw_configure_int1(LIS2DW_CTRL4_INT1_SINGLE_TAP | LIS2DW_CTRL4_INT1_DOUBLE_TAP);
return true;
}
return false;
}
bool movement_disable_tap_detection_if_available(void) {
if (movement_state.has_lis2dw) {
// Ramp data rate back down to the usual lowest rate to save power.
lis2dw_set_low_noise_mode(false);
lis2dw_set_data_rate(movement_state.accelerometer_background_rate);
lis2dw_set_mode(LIS2DW_MODE_LOW_POWER);
lis2dw_disable_double_tap();
// ...disable Z axis (not sure if this is needed, does this save power?)...
lis2dw_configure_tap_threshold(0, 0, 0, 0);
return true;
}
return false;
}
lis2dw_data_rate_t movement_get_accelerometer_background_rate(void) {
if (movement_state.has_lis2dw) return movement_state.accelerometer_background_rate;
else return LIS2DW_DATA_RATE_POWERDOWN;
}
bool movement_set_accelerometer_background_rate(lis2dw_data_rate_t new_rate) {
if (movement_state.has_lis2dw) {
if (movement_state.accelerometer_background_rate != new_rate) {
lis2dw_set_data_rate(new_rate);
movement_state.accelerometer_background_rate = new_rate;
return true;
}
}
return false;
}
uint8_t movement_get_accelerometer_motion_threshold(void) {
if (movement_state.has_lis2dw) return movement_state.accelerometer_motion_threshold;
else return 0;
}
bool movement_set_accelerometer_motion_threshold(uint8_t new_threshold) {
if (movement_state.has_lis2dw) {
if (movement_state.accelerometer_motion_threshold != new_threshold) {
lis2dw_configure_wakeup_threshold(new_threshold);
movement_state.accelerometer_motion_threshold = new_threshold;
return true;
}
}
return false;
}
float movement_get_temperature(void) {
float temperature_c = (float)0xFFFFFFFF;
#if __EMSCRIPTEN__
temperature_c = EM_ASM_DOUBLE({
return temp_c || 25.0;
});
#else
if (movement_state.has_thermistor) {
thermistor_driver_enable();
temperature_c = thermistor_driver_get_temperature();
thermistor_driver_disable();
} else if (movement_state.has_lis2dw) {
int16_t val = lis2dw_get_temperature();
val = val >> 4;
temperature_c = 25 + (float)val / 16.0;
}
#endif
return temperature_c;
}
void app_init(void) {
_watch_init();
filesystem_init();
// check if we are plugged into USB power.
HAL_GPIO_VBUS_DET_in();
HAL_GPIO_VBUS_DET_pulldown();
delay_ms(100);
if (HAL_GPIO_VBUS_DET_read()){
/// if so, enable USB functionality.
_watch_enable_usb();
}
HAL_GPIO_VBUS_DET_off();
memset((void *)&movement_state, 0, sizeof(movement_state));
movement_volatile_state.pending_events = 0;
movement_volatile_state.turn_led_off = false;
movement_volatile_state.minute_alarm_fired = false;
movement_volatile_state.minute_counter = 0;
movement_volatile_state.enter_sleep_mode = false;
movement_volatile_state.exit_sleep_mode = false;
movement_volatile_state.has_pending_sequence = false;
movement_volatile_state.has_pending_accelerometer = false;
movement_volatile_state.is_sleeping = false;
movement_volatile_state.is_buzzing = false;
movement_volatile_state.pending_sequence_priority = 0;
movement_volatile_state.mode_button.down_event = EVENT_MODE_BUTTON_DOWN;
movement_volatile_state.mode_button.is_down = false;
movement_volatile_state.mode_button.down_timestamp = 0;
movement_volatile_state.mode_button.timeout_index = MODE_BUTTON_TIMEOUT;
movement_volatile_state.mode_button.cb_longpress = cb_mode_btn_timeout_interrupt;
movement_volatile_state.light_button.down_event = EVENT_LIGHT_BUTTON_DOWN;
movement_volatile_state.light_button.is_down = false;
movement_volatile_state.light_button.down_timestamp = 0;
movement_volatile_state.light_button.timeout_index = LIGHT_BUTTON_TIMEOUT;
movement_volatile_state.light_button.cb_longpress = cb_light_btn_timeout_interrupt;
movement_volatile_state.alarm_button.down_event = EVENT_ALARM_BUTTON_DOWN;
movement_volatile_state.alarm_button.is_down = false;
movement_volatile_state.alarm_button.down_timestamp = 0;
movement_volatile_state.alarm_button.timeout_index = ALARM_BUTTON_TIMEOUT;
movement_volatile_state.alarm_button.cb_longpress = cb_alarm_btn_timeout_interrupt;
movement_state.has_thermistor = thermistor_driver_init();
bool settings_file_exists = filesystem_file_exists("settings.u32");
movement_settings_t maybe_settings;
if (settings_file_exists && maybe_settings.bit.version == 0) {
filesystem_read_file("settings.u32", (char *) &maybe_settings, sizeof(movement_settings_t));
}
if (settings_file_exists && maybe_settings.bit.version == 0) {
// If settings file exists and has a valid version, restore it!
movement_state.settings.reg = maybe_settings.reg;
} else {
// Otherwise set default values.
movement_state.settings.bit.version = 0;
movement_state.settings.bit.clock_mode_24h = MOVEMENT_DEFAULT_24H_MODE;
movement_state.settings.bit.time_zone = UTZ_UTC;
movement_state.settings.bit.led_red_color = MOVEMENT_DEFAULT_RED_COLOR;
movement_state.settings.bit.led_green_color = MOVEMENT_DEFAULT_GREEN_COLOR;
#if defined(WATCH_BLUE_TCC_CHANNEL) && !defined(WATCH_GREEN_TCC_CHANNEL)
// If there is a blue LED but no green LED, this is a blue Special Edition board.
// In the past, the "green color" showed up as the blue color on the blue board.
if (MOVEMENT_DEFAULT_RED_COLOR == 0 && MOVEMENT_DEFAULT_BLUE_COLOR == 0) {
// If the red color is 0 and the blue color is 0, we'll fall back to the old
// behavior, since otherwise there would be no default LED color.
movement_state.settings.bit.led_blue_color = MOVEMENT_DEFAULT_GREEN_COLOR;
} else {
// however if either the red or blue color is nonzero, we'll assume the user
// has used the new defaults and knows what color they want. this could be red
// if blue is 0, or a custom color if both are nonzero.