diff --git a/movement/make/Makefile b/movement/make/Makefile index 8b056a0..f4ffa54 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -116,6 +116,7 @@ SRCS += \ ../watch_faces/complication/geomancy_face.c \ ../watch_faces/clock/simple_clock_bin_led_face.c \ ../watch_faces/complication/flashlight_face.c \ + ../watch_faces/clock/wyoscan_face.c \ # New watch faces go above this line. # Leave this line at the bottom of the file; it has all the targets for making your project. diff --git a/movement/movement_faces.h b/movement/movement_faces.h index bf63732..9b83f14 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -93,6 +93,7 @@ #include "dual_timer_face.h" #include "simple_clock_bin_led_face.h" #include "flashlight_face.h" +#include "wyoscan_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ diff --git a/movement/watch_faces/clock/wyoscan_face.c b/movement/watch_faces/clock/wyoscan_face.c new file mode 100644 index 0000000..fd87b91 --- /dev/null +++ b/movement/watch_faces/clock/wyoscan_face.c @@ -0,0 +1,210 @@ +/* + * MIT License + * + * Copyright (c) 2023 <#author_name#> + * + * 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. + */ + +#include +#include +#include "wyoscan_face.h" +#include "watch_private_display.h" + +/* +Slowly render the current time from left to right, +scanning across its liquid crystal face, completing 1 cycle every 2 seconds. + +Created to mimic the wyoscan watch that was produced by Halmos and designed by Dexter Sinister +It looks like this https://www.o-r-g.com/apps/wyoscan + +You’ll notice that reading this watch requires more attention than usual, +as the seven segments of each digit are lit one by one across its display. +This speed may be adjusted until it reaches the limits of your perception. +You and your watch are now in tune. + +This is a relatively generic way of animating a time display. +If you want to modify the animation, you can change the segment_map +the A-F are corresponding to the segments on the watch face + A +F B + G +E C + D +the X's are the frames that will be skipped in the animation +This particular segment_map allocates 8 frames to display each number +this is to achieve the 2 second cycle time. +8 frames per number * 6 numbers + the trailing 16 frames = 64 frames +at 32 frames per second, this is a 2 second cycle time. + +I tried to make the animation of each number display similar to if you were +to draw the number on the watch face with a pen, pausing with 'X' +when your pen might turn a corner or when you might cross over +a line you've already drawn. It is vaguely top to bottom and counter, +clockwise when possible. +*/ +static char *segment_map[] = { + "AXFEDCBX", // 0 + "BXXXCXXX", // 1 + "ABGEXXXD", // 2 + "ABGXXXCD", // 3 + "FXGBXXXC", // 4 + "AXFXGXCD", // 5 + "AXFEDCXG", // 6 + "AXXBXXCX", // 7 + "AFGCDEXB", // 8 + "AFGBXXCD" // 9 +}; + +/* +This is the mapping of input to the watch_set_pixel() function +for each position in hhmmss it defines the 2 dimention input at each of A-F*/ +static const int32_t clock_mapping[6][7][2] = { + // hour 1 + {{1,18}, {2,19}, {0,19}, {1,18}, {0,18}, {2,18}, {1,19}}, + // hour 2 + {{2,20}, {2,21}, {1,21}, {0,21}, {0,20}, {1,17}, {1,20}}, + // minute 1 + {{0,22}, {2,23}, {0,23}, {0,22}, {1,22}, {2,22}, {1,23}}, + // minute 2 + {{2,1}, {2,10}, {0,1}, {0,0}, {1,0}, {2,0}, {1,1}}, + // second 1 + {{2,2}, {2,3}, {0,4}, {0,3}, {0,2}, {1,2}, {1,3}}, + // second 2 + {{2,4}, {2,5}, {1,6}, {0,6}, {0,5}, {1,4}, {1,5}}, +}; + +void wyoscan_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(wyoscan_state_t)); + memset(*context_ptr, 0, sizeof(wyoscan_state_t)); + // Do any one-time tasks in here; the inside of this conditional happens only at boot. + } + // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. +} + +void wyoscan_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + wyoscan_state_t *state = (wyoscan_state_t *)context; + movement_request_tick_frequency(32); + state->total_frames = 64; +} + +bool wyoscan_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + wyoscan_state_t *state = (wyoscan_state_t *)context; + + watch_date_time date_time; + switch (event.event_type) { + case EVENT_ACTIVATE: + break; + case EVENT_TICK: + if (!state->animate) { + date_time = watch_rtc_get_date_time(); + state->start = 0; + state->end = 0; + state->animation = 0; + state->animate = true; + state->time_digits[0] = date_time.unit.hour / 10; + state->time_digits[1] = date_time.unit.hour % 10; + state->time_digits[2] = date_time.unit.minute / 10; + state->time_digits[3] = date_time.unit.minute % 10; + state->time_digits[4] = date_time.unit.second / 10; + state->time_digits[5] = date_time.unit.second % 10; + } + if ( state->animate ) { + // if we have reached the max number of illuminated segments, we clear the oldest one + if ((state->end + 1) % MAX_ILLUMINATED_SEGMENTS == state->start) { + // clear the oldest pixel if it's not 'X' + if (state->illuminated_segments[state->start][0] != 99 && state->illuminated_segments[state->start][1] != 99) { + watch_clear_pixel(state->illuminated_segments[state->start][0], state->illuminated_segments[state->start][1]); + } + // increment the start index to point to the next oldest pixel + state->start = (state->start + 1) % MAX_ILLUMINATED_SEGMENTS; + } + if (state->animation < state->total_frames - MAX_ILLUMINATED_SEGMENTS) { + if (state->animation % 32 == 0) { + if (state->colon) { + watch_set_colon(); + } else { + watch_clear_colon(); + } + state->colon = !state->colon; + } + + // calculate the start position for the current frame + state->position = (state->animation / 8) % 6; + // calculate the current segment for the current digit + state->segment = state->animation % strlen(segment_map[state->time_digits[state->position]]); + // get the segments for the current digit + state->segments = segment_map[state->time_digits[state->position]]; + + if (state->segments[state->segment] == 'X') { + // if 'X', skip this frame + state->illuminated_segments[state->end][0] = 99; + state->illuminated_segments[state->end][1] = 99; + state->end = (state->end + 1) % MAX_ILLUMINATED_SEGMENTS; + state->animation = (state->animation + 1); + break; + } + + // calculate the animation frame + state->x = clock_mapping[state->position][state->segments[state->segment]-'A'][0]; + state->y = clock_mapping[state->position][state->segments[state->segment]-'A'][1]; + + // set the new pixel + watch_set_pixel(state->x, state->y); + + // store this pixel in the buffer + state->illuminated_segments[state->end][0] = state->x; + state->illuminated_segments[state->end][1] = state->y; + // increment the end index to the next position + state->end = (state->end + 1) % MAX_ILLUMINATED_SEGMENTS; + } + else if (state->animation >= state->total_frames - MAX_ILLUMINATED_SEGMENTS && state->animation < state->total_frames) { + state->end = (state->end + 1) % MAX_ILLUMINATED_SEGMENTS; + } + else { + // reset the animation state + state->animate = false; + } + state->animation = (state->animation + 1); + } + break; + case EVENT_LOW_ENERGY_UPDATE: + break; + case EVENT_ALARM_LONG_PRESS: + break; + case EVENT_BACKGROUND_TASK: + break; + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void wyoscan_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; + + // handle any cleanup before your watch face goes off-screen. +} + diff --git a/movement/watch_faces/clock/wyoscan_face.h b/movement/watch_faces/clock/wyoscan_face.h new file mode 100644 index 0000000..68db9ee --- /dev/null +++ b/movement/watch_faces/clock/wyoscan_face.h @@ -0,0 +1,74 @@ +/* + * MIT License + * + * Copyright (c) 2023 <#author_name#> + * + * 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. + */ + +#ifndef WYOSCAN_FACE_H_ +#define WYOSCAN_FACE_H_ + +#include "movement.h" + +/* + * A DESCRIPTION OF YOUR WATCH FACE + * + * and a description of how use it + * + */ + +#define MAX_ILLUMINATED_SEGMENTS 16 + +typedef struct { + uint32_t previous_date_time; + uint8_t last_battery_check; + uint8_t watch_face_index; + bool signal_enabled; + bool battery_low; + bool alarm_enabled; + uint8_t animation; + bool animate; + uint32_t start; + uint32_t end; + uint32_t total_frames; + bool colon; + uint8_t position, segment; + char *segments; + uint8_t x, y; + uint32_t time_digits[6]; + uint32_t illuminated_segments[MAX_ILLUMINATED_SEGMENTS][2]; +} wyoscan_state_t; + +void wyoscan_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void wyoscan_face_activate(movement_settings_t *settings, void *context); +bool wyoscan_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void wyoscan_face_resign(movement_settings_t *settings, void *context); +bool wyoscan_face_wants_background_task(movement_settings_t *settings, void *context); + +#define wyoscan_face ((const watch_face_t){ \ + wyoscan_face_setup, \ + wyoscan_face_activate, \ + wyoscan_face_loop, \ + wyoscan_face_resign, \ + NULL, \ +}) + +#endif // WYOSCAN_FACE_H_ +