Activity face + Chirpy TX (#187)

* chirpy demo face; activity face stub

* activity face WIP: can log, pause and clear

* activity face and chirpy demo: ready to flash to watch

* activity face tweaks

* hour display for hours < 10

* fix: added rogue paused seconds when stopping activity

* LE mode; lower power with 1Hz tick

* fix: midnight is 12

* Documentation in code comments

* fixes from code review by @neutralinsomniac

* chirpy_demo_face option to chirp out nanosec.ini + auto-format

* UI tweaks

* remove erroneously added file (content revoked)

* UI tweaks: return from LE mode; time display vs LAP

* add default loop handler (will enable long-mode-to-first-face)

* reset watch faces to match main branch
This commit is contained in:
gugray
2023-03-11 22:27:18 +01:00
committed by GitHub
parent 2d46a9bf9e
commit 9af51de624
12 changed files with 5960 additions and 0 deletions

View File

@@ -0,0 +1,725 @@
/*
* MIT License
*
* Copyright (c) 2023 Gabor L Ugray
*
* 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.
*/
/* ** TODO
* ===================
* -- Additional power-saving optimizations
*/
#include <stdlib.h>
#include <string.h>
#include "activity_face.h"
#include "chirpy_tx.h"
#include "watch.h"
#include "watch_utility.h"
// ===========================================================================
// This part is configurable: you can edit values here to customize you activity face
// In particular, with num_enabled_activities and enabled_activities you can choose a subset of the
// activities that you want to see in your watch.
// You can also add new items to activity_names, but don't redefine or remove existing ones.
// If a logged activity is shorter than this, then it won't be added to log when it ends.
// This way scarce log slots are not taken up by aborted events that weren't real activities.
static const uint16_t activity_min_length_sec = 60;
// Supported activities. ID of activity is index in this buffer
// W e should never change order or redefine items, only add new items when needed.
static const char activity_names[][7] = {
" bIKE ",
"uuaLK ",
" rUn ",
"DAnCE ",
" yOgA ",
"CrOSS ",
"Suuinn",
"ELLIP ",
" gYnn",
" rOuu",
"SOCCEr",
" FOOTb",
" bALL ",
" SKI ",
};
// Currently enabled activities. This makes picking on first subface easier: why show activities you personally never do.
static const uint8_t enabled_activities[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
// Number of currently enabled activities (size of enabled_activities).
static const uint8_t num_enabled_activities = sizeof(enabled_activities) / sizeof(uint8_t);
// End configurable section
// ===========================================================================
// One logged activity
typedef struct __attribute__((__packed__)) {
// Activity's start time
watch_date_time start_time;
// Total duration of activity, including time spend in paus
uint16_t total_sec;
// Number of seconds the activity was paused
uint16_t pause_sec;
// Type of activity (index in activity_names)
uint8_t activity_type;
} activity_item_t;
#define MAX_ACTIVITY_SECONDS 28800 // 8 hours = 28800 sec
// Size of (fixed) buffer to log activites. Takes up x9 bytes in SRAM if face is installed.
#define ACTIVITY_LOG_SZ 99
// Number of activities in buffer.
static uint8_t activity_log_count = 0;
// Buffer with all logged activities.
static activity_item_t activity_log_buffer[ACTIVITY_LOG_SZ];
#define CHIRPY_PREFIX_LEN 2
// First two bytes chirped out, to identify transmission as from the activity face
static const uint8_t activity_chirpy_prefix[CHIRPY_PREFIX_LEN] = {0x27, 0x00};
// The face's different UI modes (views).
typedef enum {
ACTM_CHOOSE = 0,
ACTM_LOGGING,
ACTM_PAUSED,
ACTM_DONE,
ACTM_LOGSIZE,
ACTM_CHIRP,
ACTM_CHIRPING,
ACTM_CLEAR,
ACTM_CLEAR_CONFIRM,
ACTM_CLEAR_DONE,
} activity_mode_t;
// The full state of the activity face
typedef struct {
// Current mode (which secondary face, or ongoing operation like logging)
activity_mode_t mode;
// Index of currently selected activity in enabled_activities
uint8_t type_ix;
// Used for different things depending on mode
// In ACTM_DONE: countdown for animation, before returning to start face
// In ACTM_LOGGING and ACTM_PAUSED: drives blinking colon and alternating time display
// In ACTM_LOGSIZE, ACTM_CLEAR: enables timeout return to choose screen
uint16_t counter;
// Start of currently logged activity, if any
watch_date_time start_time;
// Total seconds elapsed since logging started
uint16_t curr_total_sec;
// Total paused seconds in current log
uint16_t curr_pause_sec;
// Helps us handle 1/64 ticks during transmission; including countdown timer
chirpy_tick_state_t chirpy_tick_state;
// Used by chirpy encoder during transmission
chirpy_encoder_state_t chirpy_encoder_state;
// 0: Running normally
// 1: In LE mode
// 2: Just woke up from LE mode. Will go to 0 after ignoring ALARM_BUTTON_UP.
uint8_t le_state;
} activity_state_t;
#define ACTIVITY_BUF_SZ 14
// Temp buffer used for sprintf'ing content for the display.
char activity_buf[ACTIVITY_BUF_SZ];
// Needed by _activity_get_next_byte to keep track of where we are in transmission
uint16_t *activity_seq_pos;
static void _activity_clear_buffers() {
// Clear activity buffer; 0xcd is good for diagnostics
memset(activity_log_buffer, 0xcd, ACTIVITY_LOG_SZ * sizeof(activity_item_t));
// Clear display buffer
memset(activity_buf, 0, ACTIVITY_BUF_SZ);
}
static void _activity_display_choice(activity_state_t *state);
static void _activity_update_logging_screen(movement_settings_t *settings, activity_state_t *state);
static uint8_t _activity_get_next_byte(uint8_t *next_byte);
void activity_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(activity_state_t));
memset(*context_ptr, 0, sizeof(activity_state_t));
// This happens only at boot
_activity_clear_buffers();
}
// Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep.
}
void activity_face_activate(movement_settings_t *settings, void *context) {
(void)settings;
(void)context;
// Not using this function. Calling _activity_activate from the event handler.
// That is what we get both when the face is shown upon navigation by MODE,
// and when waking from low energy state.
}
// Called from the ACTIVATE event handler in the loop
static void _activity_activate(movement_settings_t *settings, activity_state_t *state) {
// If waking from low-energy state and currently logging: update seconds values
// Those are not up-to-date because ticks have not been coming
if (state->le_state != 0 && state->mode == ACTM_LOGGING) {
state->le_state = 2;
watch_date_time now = watch_rtc_get_date_time();
uint32_t now_timestamp = watch_utility_date_time_to_unix_time(now, 0);
uint32_t start_timestamp = watch_utility_date_time_to_unix_time(state->start_time, 0);
uint32_t total_seconds = now_timestamp - start_timestamp;
state->curr_total_sec = total_seconds;
_activity_update_logging_screen(settings, state);
}
// Regular activation: start from defaults
else {
state->le_state = 0;
state->mode = 0;
state->type_ix = 0;
_activity_display_choice(state);
}
}
static void _activity_display_choice(activity_state_t *state) {
watch_display_string("AC", 0);
// If buffer is full: We say "FULL"
if (activity_log_count >= ACTIVITY_LOG_SZ) {
watch_display_string(" FULL ", 4);
}
// Otherwise, we show currently activity
else {
uint8_t activity_ix = enabled_activities[state->type_ix];
const char *name = activity_names[activity_ix];
watch_display_string((char *)name, 4);
}
}
const uint8_t activity_anim_pixels[][2] = {
{1, 4}, // TL
{0, 5}, // BL
{0, 6}, // BOT
{1, 6}, // BR
{2, 5}, // TR
{2, 4}, // TOP
// {2, 4}, // MID
};
static void _activity_update_logging_screen(movement_settings_t *settings, activity_state_t *state) {
watch_duration_t duration;
watch_display_string("AC ", 0);
// If we're in LE state: per-minute update is special
if (state->le_state == 1) {
watch_date_time now = watch_rtc_get_date_time();
uint32_t now_timestamp = watch_utility_date_time_to_unix_time(now, 0);
uint32_t start_timestamp = watch_utility_date_time_to_unix_time(state->start_time, 0);
uint32_t total_seconds = now_timestamp - start_timestamp;
duration = watch_utility_seconds_to_duration(total_seconds);
sprintf(activity_buf, " %d%02d ", duration.hours, duration.minutes);
watch_display_string(activity_buf, 4);
watch_set_colon();
watch_set_indicator(WATCH_INDICATOR_LAP);
watch_clear_indicator(WATCH_INDICATOR_PM);
watch_clear_indicator(WATCH_INDICATOR_24H);
return;
}
// Show elapsed time, or PAUSE
if ((state->counter % 5) < 3) {
watch_set_indicator(WATCH_INDICATOR_LAP);
watch_clear_indicator(WATCH_INDICATOR_PM);
watch_clear_indicator(WATCH_INDICATOR_24H);
if (state->mode == ACTM_PAUSED) {
watch_display_string(" PAUSE", 4);
watch_clear_colon();
} else {
duration = watch_utility_seconds_to_duration(state->curr_total_sec);
// Under 10 minutes: M:SS
if (state->curr_total_sec < 600) {
sprintf(activity_buf, " %01d%02d", duration.minutes, duration.seconds);
watch_display_string(activity_buf, 4);
watch_clear_colon();
}
// Under an hour: MM:SS
else if (state->curr_total_sec < 3600) {
sprintf(activity_buf, " %02d%02d", duration.minutes, duration.seconds);
watch_display_string(activity_buf, 4);
watch_clear_colon();
}
// Over an hour: H:MM:SS
// (We never go to two-digit hours; stop at 8)
else {
sprintf(activity_buf, " %d%02d%02d", duration.hours, duration.minutes, duration.seconds);
watch_display_string(activity_buf, 4);
watch_set_colon();
}
}
}
// Briefly, show time without seconds
else {
watch_clear_indicator(WATCH_INDICATOR_LAP);
watch_date_time now = watch_rtc_get_date_time();
uint8_t hour = now.unit.hour;
if (!settings->bit.clock_mode_24h) {
watch_clear_indicator(WATCH_INDICATOR_24H);
if (hour < 12)
watch_clear_indicator(WATCH_INDICATOR_PM);
else
watch_set_indicator(WATCH_INDICATOR_PM);
hour %= 12;
if (hour == 0) hour = 12;
}
else {
watch_set_indicator(WATCH_INDICATOR_24H);
watch_clear_indicator(WATCH_INDICATOR_PM);
}
sprintf(activity_buf, "%2d%02d ", hour, now.unit.minute);
watch_set_colon();
watch_display_string(activity_buf, 4);
}
}
static void _activity_quit_chirping() {
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_set_buzzer_off();
movement_request_tick_frequency(1);
}
static void _activity_chirp_tick_transmit(void *context) {
activity_state_t *state = (activity_state_t *)context;
uint8_t tone = chirpy_get_next_tone(&state->chirpy_encoder_state);
// Transmission over?
if (tone == 255) {
_activity_quit_chirping();
state->mode = ACTM_CHIRP;
state->counter = 0;
watch_display_string("AC CHIRP ", 0);
return;
}
uint16_t period = chirpy_get_tone_period(tone);
watch_set_buzzer_period(period);
watch_set_buzzer_on();
}
static void _activity_chirp_tick_countdown(void *context) {
activity_state_t *state = (activity_state_t *)context;
// Countdown over: start actual broadcast
if (state->chirpy_tick_state.seq_pos == 8 * 3) {
state->chirpy_tick_state.tick_compare = 3;
state->chirpy_tick_state.tick_count = 2; // tick_compare - 1, so it starts immediately
state->chirpy_tick_state.seq_pos = 0;
state->chirpy_tick_state.tick_fun = _activity_chirp_tick_transmit;
return;
}
// Sound or turn off buzzer
if ((state->chirpy_tick_state.seq_pos % 8) == 0) {
watch_set_buzzer_period(NotePeriods[BUZZER_NOTE_A5]);
watch_set_buzzer_on();
if (state->chirpy_tick_state.seq_pos == 0) {
watch_display_string(" --- ", 4);
} else if (state->chirpy_tick_state.seq_pos == 8) {
watch_display_string(" --", 5);
} else if (state->chirpy_tick_state.seq_pos == 16) {
watch_display_string(" -", 5);
}
} else if ((state->chirpy_tick_state.seq_pos % 8) == 1) {
watch_set_buzzer_off();
}
++state->chirpy_tick_state.seq_pos;
}
static uint8_t _activity_get_next_byte(uint8_t *next_byte) {
uint16_t num_bytes = 2 + activity_log_count * sizeof(activity_item_t);
uint16_t pos = *activity_seq_pos;
// Init counter
if (pos == 0) {
sprintf(activity_buf, "%3d", activity_log_count);
watch_display_string(activity_buf, 5);
}
if (pos == num_bytes) {
return 0;
}
// Two-byte prefix
if (pos < 2) {
(*next_byte) = activity_chirpy_prefix[pos];
}
// Data
else {
pos -= 2;
uint16_t ix = pos / sizeof(activity_item_t);
const activity_item_t *itm = &activity_log_buffer[ix];
uint16_t ofs = pos % sizeof(activity_item_t);
// Update counter when starting new item
if (ofs == 0) {
sprintf(activity_buf, "%3d", activity_log_count - ix);
watch_display_string(activity_buf, 5);
}
// Do this the hard way, byte by byte, to avoid high/low endedness issues
// Higher order bytes first, is our serialization format
uint8_t val;
// watch_date_time start_time;
// uint16_t total_sec;
// uint16_t pause_sec;
// uint8_t activity_type;
if (ofs == 0)
val = (itm->start_time.reg & 0xff000000) >> 24;
else if (ofs == 1)
val = (itm->start_time.reg & 0x00ff0000) >> 16;
else if (ofs == 2)
val = (itm->start_time.reg & 0x0000ff00) >> 8;
else if (ofs == 3)
val = (itm->start_time.reg & 0x000000ff);
else if (ofs == 4)
val = (itm->total_sec & 0xff00) >> 8;
else if (ofs == 5)
val = (itm->total_sec & 0x00ff);
else if (ofs == 6)
val = (itm->pause_sec & 0xff00) >> 8;
else if (ofs == 7)
val = (itm->pause_sec & 0x00ff);
else
val = itm->activity_type;
(*next_byte) = val;
}
++(*activity_seq_pos);
return 1;
}
static void _activity_finish_logging(activity_state_t *state) {
// Save this activity
// If shorter than minimum for log: don't save
// Sanity check about buffer length. This should never happen, but also we never want to overrun by error
if (state->curr_total_sec >= activity_min_length_sec && activity_log_count + 1 < ACTIVITY_LOG_SZ) {
activity_item_t *itm = &activity_log_buffer[activity_log_count];
itm->start_time = state->start_time;
itm->total_sec = state->curr_total_sec;
itm->pause_sec = state->curr_pause_sec;
itm->activity_type = state->type_ix;
++activity_log_count;
}
// Go to DONE animation
// TODO: Not in LE mode
state->mode = ACTM_DONE;
watch_clear_indicator(WATCH_INDICATOR_LAP);
movement_request_tick_frequency(2);
state->counter = 6 * 1;
watch_clear_display();
watch_display_string("AC dONE ", 0);
}
static void _activity_handle_tick(movement_settings_t *settings, activity_state_t *state) {
// Display stopwatch-like duration while logging, alternating with time
if (state->mode == ACTM_LOGGING || state->mode == ACTM_PAUSED) {
++state->counter;
++state->curr_total_sec;
if (state->mode == ACTM_PAUSED)
++state->curr_pause_sec;
// If we've reached max activity length: finish logging
if (state->curr_total_sec == MAX_ACTIVITY_SECONDS) {
_activity_finish_logging(state);
}
// Still logging: refresh display
else {
_activity_update_logging_screen(settings, state);
}
}
// Display countown animation, and exit face when down
else if (state->mode == ACTM_DONE) {
if (state->counter == 0) {
movement_move_to_face(0);
movement_request_tick_frequency(1);
}
else {
uint8_t cd = state->counter % 6;
watch_clear_pixel(activity_anim_pixels[cd][0], activity_anim_pixels[cd][1]);
--state->counter;
cd = state->counter % 6;
watch_set_pixel(activity_anim_pixels[cd][0], activity_anim_pixels[cd][1]);
}
}
// Log size, chirp, clear: return to choose after some time
else if (state->mode == ACTM_LOGSIZE || state->mode == ACTM_CHIRP || state->mode == ACTM_CLEAR) {
++state->counter;
// Leave Log Size after 20 seconds
// Leave Clear after only 10: this is danger zone, we don't like hanging around here
// Leave Chirp after 2 minutes: most likely need to the time to fiddle with mic & Chirpy RX on the computer
uint16_t timeout = 20;
if (state->mode == ACTM_CLEAR) timeout = 10;
else if (state->mode == ACTM_CHIRP) timeout = 120;
if (state->counter > timeout) {
state->mode = ACTM_CHOOSE;
_activity_display_choice(state);
}
}
// Chirping
else if (state->mode == ACTM_CHIRPING) {
++state->chirpy_tick_state.tick_count;
if (state->chirpy_tick_state.tick_count == state->chirpy_tick_state.tick_compare) {
state->chirpy_tick_state.tick_count = 0;
state->chirpy_tick_state.tick_fun(state);
}
}
// Clear confirm: blink CLEAR
else if (state->mode == ACTM_CLEAR_CONFIRM) {
++state->counter;
if ((state->counter % 2) == 0)
watch_display_string("CLEAR ", 4);
else
watch_display_string(" ", 4);
if (state->counter > 12) {
state->mode = ACTM_CHOOSE;
_activity_display_choice(state);
movement_request_tick_frequency(1);
}
}
// Clear done: fill up zeroes, then return to choose screen
else if (state->mode == ACTM_CLEAR_DONE) {
++state->counter;
// Animation done
if (state->counter == 7) {
state->mode = ACTM_CHOOSE;
_activity_display_choice(state);
movement_request_tick_frequency(1);
return;
}
// Display current state of animation
sprintf(activity_buf, " ");
uint8_t nZeros = state->counter + 1;
if (nZeros > 6) nZeros = 6;
for (uint8_t i = 0; i < nZeros; ++i) {
activity_buf[i] = '0';
}
watch_display_string(activity_buf, 4);
}
}
static void _activity_alarm_long(movement_settings_t *settings, activity_state_t *state) {
// On choose face: start logging activity
if (state->mode == ACTM_CHOOSE) {
// If buffer is full: Ignore this long press
if (activity_log_count >= ACTIVITY_LOG_SZ)
return;
// OK, we go ahead and start logging
state->start_time = watch_rtc_get_date_time();
state->curr_total_sec = 0;
state->curr_pause_sec = 0;
state->counter = -1;
state->mode = ACTM_LOGGING;
watch_set_indicator(WATCH_INDICATOR_LAP);
_activity_update_logging_screen(settings, state);
}
// If logging or paused: end logging
else if (state->mode == ACTM_LOGGING || state->mode == ACTM_PAUSED) {
_activity_finish_logging(state);
}
// If chirp: kick off chirping
else if (state->mode == ACTM_CHIRP) {
// Set up our tick handling for countdown beeps
activity_seq_pos = &state->chirpy_tick_state.seq_pos;
state->chirpy_tick_state.tick_compare = 8;
state->chirpy_tick_state.tick_count = 7; // tick_compare - 1, so it starts immediately
state->chirpy_tick_state.seq_pos = 0;
state->chirpy_tick_state.tick_fun = _activity_chirp_tick_countdown;
// Set up chirpy encoder
chirpy_init_encoder(&state->chirpy_encoder_state, _activity_get_next_byte);
// Show bell; switch to 64/sec ticks
watch_set_indicator(WATCH_INDICATOR_BELL);
movement_request_tick_frequency(64);
state->mode = ACTM_CHIRPING;
}
// If clear: confirm (unless empty)
else if (state->mode == ACTM_CLEAR) {
if (activity_log_count == 0)
return;
state->mode = ACTM_CLEAR_CONFIRM;
state->counter = -1;
movement_request_tick_frequency(4);
}
// If clear confirm: do clear.
else if (state->mode == ACTM_CLEAR_CONFIRM) {
_activity_clear_buffers();
activity_log_count = 0;
state->mode = ACTM_CLEAR_DONE;
state->counter = -1;
watch_display_string("0 ", 4);
movement_request_tick_frequency(2);
}
}
static void _activity_alarm_short(movement_settings_t *settings, activity_state_t *state) {
// In the choose face, short ALARM cycles through activities
if (state->mode == ACTM_CHOOSE) {
state->type_ix = (state->type_ix + 1) % num_enabled_activities;
_activity_display_choice(state);
}
// If logging: pause
else if (state->mode == ACTM_LOGGING) {
state->mode = ACTM_PAUSED;
state->counter = 0;
_activity_update_logging_screen(settings, state);
}
// If paused: Update paused seconds count and return to logging
else if (state->mode == ACTM_PAUSED) {
state->mode = ACTM_LOGGING;
state->counter = 0;
_activity_update_logging_screen(settings, state);
}
// If chirping: stoppit
else if (state->mode == ACTM_CHIRPING) {
_activity_quit_chirping();
state->mode = ACTM_CHIRP;
state->counter = 0;
watch_display_string("AC CHIRP ", 0);
}
}
static void _activity_light_short(activity_state_t *state) {
// If choose face: move to log size
if (state->mode == ACTM_CHOOSE) {
state->mode = ACTM_LOGSIZE;
state->counter = 0;
sprintf(activity_buf, "AC L#g%3d", activity_log_count);
watch_display_string(activity_buf, 0);
}
// If log size face: move to chirp
else if (state->mode == ACTM_LOGSIZE) {
state->mode = ACTM_CHIRP;
state->counter = 0;
watch_display_string("AC CHIRP ", 0);
}
// If chirp face: move to clear
else if (state->mode == ACTM_CHIRP) {
state->mode = ACTM_CLEAR;
state->counter = 0;
watch_display_string("AC CLEAR ", 0);
}
// If clear face: return to choose face
else if (state->mode == ACTM_CLEAR || state->mode == ACTM_CLEAR_CONFIRM) {
state->mode = ACTM_CHOOSE;
_activity_display_choice(state);
movement_request_tick_frequency(1);
}
// While logging or paused, light is light
else if (state->mode == ACTM_LOGGING || state->mode == ACTM_PAUSED) {
movement_illuminate_led();
}
// Otherwise, we don't do light.
}
bool activity_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
activity_state_t *state = (activity_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
_activity_activate(settings, state);
break;
case EVENT_TICK:
_activity_handle_tick(settings, state);
break;
case EVENT_MODE_BUTTON_UP:
if (state->mode != ACTM_LOGGING && state->mode != ACTM_PAUSED && state->mode != ACTM_CHIRPING) {
movement_request_tick_frequency(1);
movement_move_to_next_face();
}
break;
case EVENT_LIGHT_BUTTON_UP:
_activity_light_short(state);
break;
case EVENT_ALARM_BUTTON_UP:
// We also receive ALARM press that woke us up from LE state
// Don't want to act on that as if it were a real button press for us
if (state->le_state != 2)
_activity_alarm_short(settings, state);
else
state->le_state = 0;
break;
case EVENT_ALARM_LONG_PRESS:
_activity_alarm_long(settings, state);
break;
case EVENT_TIMEOUT:
if (state->mode != ACTM_LOGGING && state->mode != ACTM_PAUSED &&
state->mode != ACTM_CHIRP && state->mode != ACTM_CHIRPING) {
movement_request_tick_frequency(1);
movement_move_to_face(0);
}
break;
case EVENT_LOW_ENERGY_UPDATE:
state->le_state = 1;
// If we're in paused logging mode: let's lose this activity. Pause is not meant for over an hour.
if (state->mode == ACTM_PAUSED) {
// When waking, face will revert to default screen
state->mode = ACTM_CHOOSE;
watch_display_string("AC SLEEP ", 0);
watch_clear_colon();
watch_clear_indicator(WATCH_INDICATOR_LAP);
watch_clear_indicator(WATCH_INDICATOR_PM);
}
else {
_activity_update_logging_screen(settings, state);
watch_start_tick_animation(500);
}
break;
default:
movement_default_loop_handler(event, settings);
break;
}
// Return true if the watch can enter standby mode. False needed when chirping.
if (state->mode == ACTM_CHIRPING)
return false;
else
return true;
}
void activity_face_resign(movement_settings_t *settings, void *context) {
(void)settings;
(void)context;
// Face should only ever temporarily request a higher frequency, so by the time we're resigning,
// this should not be needed. But we don't want an error to create a situation that drains the battery.
// Rather do this defensively here.
movement_request_tick_frequency(1);
}

View File

@@ -0,0 +1,89 @@
/*
* MIT License
*
* Copyright (c) 2023 Gabor L Ugray
*
* 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 ACTIVITY_FACE_H_
#define ACTIVITY_FACE_H_
#include "movement.h"
/*
* ACTIVITY WATCH FACE
*
* The Activity face lets you record activities like you would do with a fitness watch.
* It supports different activities like running, biking, rowing etc., and for each recorded activity
* it stores when it started and how long it was.
*
* You can save up to 99 activities this way. Every once in a while you can chirp them out
* using the watch's piezo buzzer as a modem, then clear the log in the watch.
* To record and decode a chirpy transmission on your computer, you can use the web app here:
* https://jealousmarkup.xyz/off/chirpy/rx/
*
* Using the face
*
* When you activate the face, it starts with the first screen to select the activity you want to log.
* ALARM cycles through the list of activities.
* LONG ALARM starts logging.
* While logging is in progress, the face alternates between the elapsed time and the current time.
* You can press ALARM to pause (e.g., while you hop in to the baker's for a croissant during your jog).
* Pressing ALARM again resumes the activity.
* LONG ALARM stops logging and saves the activity.
*
* When you're not loggin, you can press LIGHT to access the secondary faces.
* LIGHT #1 => Shows the size of the log (how many activities have been recorded).
* LIGHT #2 => The screen to chirp out the data. Press LONG ALARM to start chirping.
* LIGHT #3 => The screen to clear the log in the watch. Press LONG ALARM twice to clear data.
*
* Quirky details
*
* The face will discard short activities (less than a minute) when you press LONG ALARM to finish logging.
* These were probably logged by mistake, and it's better to save slots and chirping battery power for
* stuff that really matters.
*
* The face will continue to record an activity past the normal one-hour mark, when the watch
* enters low energy mode. However, it will always stop at 8 hours. If an activity takes that long,
* you probably just forgot to stop logging.
*
* The log is stored in regular memory. It will be lost when you remove the battery, so make
* sure you chirp it out before taking the watch apart.
*
* See the top of activity_face.c for some customization options. What you most likely want to do
* is reduce the list of activities shown on the first screen to the ones you are regularly doing.
*
*/
void activity_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
void activity_face_activate(movement_settings_t *settings, void *context);
bool activity_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
void activity_face_resign(movement_settings_t *settings, void *context);
#define activity_face ((const watch_face_t){ \
activity_face_setup, \
activity_face_activate, \
activity_face_loop, \
activity_face_resign, \
NULL, \
})
#endif // ACTIVITY_FACE_H_

View File

@@ -0,0 +1,335 @@
/*
* MIT License
*
* Copyright (c) 2023 Gabor L Ugray
*
* 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 <stdlib.h>
#include <string.h>
#include "chirpy_demo_face.h"
#include "chirpy_tx.h"
#include "filesystem.h"
typedef enum {
CDM_CHOOSE = 0,
CDM_CHIRPING,
} chirpy_demo_mode_t;
typedef enum {
CDP_SCALE = 0,
CDP_INFO_SHORT,
CDP_INFO_LONG,
CDP_INFO_NANOSEC,
} chirpy_demo_program_t;
typedef struct {
// Current mode
chirpy_demo_mode_t mode;
// Selected program
chirpy_demo_program_t program;
// Helps us handle 1/64 ticks during transmission; including countdown timer
chirpy_tick_state_t tick_state;
// Used by chirpy encoder during transmission
chirpy_encoder_state_t encoder_state;
} chirpy_demo_state_t;
static uint8_t long_data_str[] =
"There once was a ship that put to sea\n"
"The name of the ship was the Billy of Tea\n"
"The winds blew up, her bow dipped down\n"
"O blow, my bully boys, blow (huh)\n"
"\n"
"Soon may the Wellerman come\n"
"To bring us sugar and tea and rum\n"
"One day, when the tonguin' is done\n"
"We'll take our leave and go\n";
static uint16_t short_data_len = 20;
static uint8_t short_data[] = {
0x27,
0x00,
0x0c,
0x42,
0xa3,
0xd4,
0x06,
0x54,
0x00,
0x00,
0x02,
0x0c,
0x6b,
0x05,
0x5a,
0x09,
0xd8,
0x00,
0xf5,
0x00,
};
#define NANOSEC_INI_FILE_NAME "nanosec.ini"
static uint8_t *nanosec_buffer = 0;
static uint16_t nanosec_buffer_size = 0;
void chirpy_demo_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(chirpy_demo_state_t));
memset(*context_ptr, 0, sizeof(chirpy_demo_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 chirpy_demo_face_activate(movement_settings_t *settings, void *context) {
(void)settings;
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
memset(context, 0, sizeof(chirpy_demo_state_t));
state->mode = CDM_CHOOSE;
state->program = CDP_SCALE;
// Do we have nanosec data? Load it.
int32_t sz = filesystem_get_file_size(NANOSEC_INI_FILE_NAME);
if (sz > 0) {
// We will free this in resign.
// I don't like any kind of dynamic allocation in long-running embedded software...
// But there's no way around it here; I don't want to hard-wire (and squat) any fixed size structure
// Nanosec data may change in the future too
nanosec_buffer_size = sz + 2;
nanosec_buffer = malloc(nanosec_buffer_size);
// First two bytes of prefix, so Chirpy RX can recognize this data type
nanosec_buffer[0] = 0xc0;
nanosec_buffer[1] = 0x00;
// Read file
filesystem_read_file(NANOSEC_INI_FILE_NAME, (char*)&nanosec_buffer[2], sz);
}
}
// To create / check test file in emulator:
// echo TestData > nanosec.ini
// cat nanosec.ini
static void _cdf_update_lcd(chirpy_demo_state_t *state) {
watch_display_string("CH", 0);
if (state->program == CDP_SCALE)
watch_display_string(" SCALE", 4);
else if (state->program == CDP_INFO_SHORT)
watch_display_string("SHORT ", 4);
else if (state->program == CDP_INFO_LONG)
watch_display_string(" LOng ", 4);
else if (state->program == CDP_INFO_NANOSEC)
watch_display_string("nAnO ", 4);
else
watch_display_string("---- ", 4);
}
static void _cdf_quit_chirping(chirpy_demo_state_t *state) {
state->mode = CDM_CHOOSE;
watch_set_buzzer_off();
watch_clear_indicator(WATCH_INDICATOR_BELL);
movement_request_tick_frequency(1);
}
static void _cdf_scale_tick(void *context) {
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
chirpy_tick_state_t *tick_state = &state->tick_state;
// Scale goes in 200Hz increments from 700 Hz to 12.3 kHz -> 58 steps
if (tick_state->seq_pos == 58) {
_cdf_quit_chirping(state);
return;
}
uint32_t freq = 700 + tick_state->seq_pos * 200;
uint32_t period = 1000000 / freq;
watch_set_buzzer_period(period);
watch_set_buzzer_on();
++tick_state->seq_pos;
}
static void _cdf_data_tick(void *context) {
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
uint8_t tone = chirpy_get_next_tone(&state->encoder_state);
// Transmission over?
if (tone == 255) {
_cdf_quit_chirping(state);
return;
}
uint16_t period = chirpy_get_tone_period(tone);
watch_set_buzzer_period(period);
watch_set_buzzer_on();
}
static uint8_t *curr_data_ptr;
static uint16_t curr_data_ix;
static uint16_t curr_data_len;
static uint8_t _cdf_get_next_byte(uint8_t *next_byte) {
if (curr_data_ix == curr_data_len)
return 0;
*next_byte = curr_data_ptr[curr_data_ix];
++curr_data_ix;
return 1;
}
static void _cdf_countdown_tick(void *context) {
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
chirpy_tick_state_t *tick_state = &state->tick_state;
// Countdown over: start actual broadcast
if (tick_state->seq_pos == 8 * 3) {
tick_state->tick_compare = 3;
tick_state->tick_count = -1;
tick_state->seq_pos = 0;
// We'll be chirping out a scale
if (state->program == CDP_SCALE) {
tick_state->tick_fun = _cdf_scale_tick;
}
// We'll be chirping out data
else {
// Set up the encoder
chirpy_init_encoder(&state->encoder_state, _cdf_get_next_byte);
tick_state->tick_fun = _cdf_data_tick;
// Set up the data
curr_data_ix = 0;
if (state->program == CDP_INFO_SHORT) {
curr_data_ptr = short_data;
curr_data_len = short_data_len;
} else if (state->program == CDP_INFO_LONG) {
curr_data_ptr = long_data_str;
curr_data_len = strlen((const char *)long_data_str);
} else if (state->program == CDP_INFO_NANOSEC) {
curr_data_ptr = nanosec_buffer;
curr_data_len = nanosec_buffer_size;
}
}
return;
}
// Sound or turn off buzzer
if ((tick_state->seq_pos % 8) == 0) {
watch_set_buzzer_period(NotePeriods[BUZZER_NOTE_A5]);
watch_set_buzzer_on();
} else if ((tick_state->seq_pos % 8) == 1) {
watch_set_buzzer_off();
}
++tick_state->seq_pos;
}
static void _cdm_setup_chirp(chirpy_demo_state_t *state) {
// We want frequent callbacks from now on
movement_request_tick_frequency(64);
watch_set_indicator(WATCH_INDICATOR_BELL);
state->mode = CDM_CHIRPING;
// Set up tick state; start with countdown
state->tick_state.tick_count = -1;
state->tick_state.tick_compare = 8;
state->tick_state.seq_pos = 0;
state->tick_state.tick_fun = _cdf_countdown_tick;
}
bool chirpy_demo_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
(void)settings;
chirpy_demo_state_t *state = (chirpy_demo_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
_cdf_update_lcd(state);
break;
case EVENT_MODE_BUTTON_UP:
// Do not exit face while we're chirping
if (state->mode != CDM_CHIRPING) {
movement_move_to_next_face();
}
break;
case EVENT_LIGHT_BUTTON_UP:
// We don't do light.
break;
case EVENT_ALARM_BUTTON_UP:
// If in choose mode: select next program
if (state->mode == CDM_CHOOSE) {
if (state->program == CDP_SCALE)
state->program = CDP_INFO_SHORT;
else if (state->program == CDP_INFO_SHORT)
state->program = CDP_INFO_LONG;
else if (state->program == CDP_INFO_LONG) {
if (nanosec_buffer_size > 0)
state->program = CDP_INFO_NANOSEC;
else
state->program = CDP_SCALE;
} else if (state->program == CDP_INFO_NANOSEC)
state->program = CDP_SCALE;
_cdf_update_lcd(state);
}
// If chirping: stoppit
else if (state->mode == CDM_CHIRPING) {
_cdf_quit_chirping(state);
}
break;
case EVENT_ALARM_LONG_PRESS:
// If in choose mode: start chirping
if (state->mode == CDM_CHOOSE) {
_cdm_setup_chirp(state);
}
break;
case EVENT_TICK:
if (state->mode == CDM_CHIRPING) {
++state->tick_state.tick_count;
if (state->tick_state.tick_count == state->tick_state.tick_compare) {
state->tick_state.tick_count = 0;
state->tick_state.tick_fun(context);
}
}
break;
case EVENT_TIMEOUT:
// Do not time out while we're chirping
if (state->mode != CDM_CHIRPING) {
movement_move_to_face(0);
}
default:
break;
}
// Return true if the watch can enter standby mode. False needed when chirping.
if (state->mode == CDM_CHIRPING)
return false;
else
return true;
}
void chirpy_demo_face_resign(movement_settings_t *settings, void *context) {
(void)settings;
(void)context;
if (nanosec_buffer != 0) {
free(nanosec_buffer);
nanosec_buffer = 0;
nanosec_buffer_size = 0;
}
}

View File

@@ -0,0 +1,70 @@
/*
* 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 CHIRPY_DEMO_FACE_H_
#define CHIRPY_DEMO_FACE_H_
#include "movement.h"
/*
* CHIRPY DEMO FACE
*
* This face demonstrates the chirpy-tx library. It is intended to help you
* include chirpy-tx in your own watch faces that need to transmit data out
* of the watch.
*
* The face's first screen lets you select from a few built-in transmissions:
*
* SCALE cycles through frequencies in fixed increments. This is intended to
* collect and analyze audio samples from different watches. With this info
* it may be possible to improve chirpy-tx's parameters like the frequencies
* it uses to make the method more robust.
*
* SHORT is a small transmission that contains data taked from the activity_face.
*
* LONG is a longer transmission that contains the first two strophes of a
* famous sea shanty.
*
* Select the transmission you want with ALARM, the press LONG ALARM to chirp.
*
* To record and decode a chirpy transmission on your computer, you can use the web app here:
* https://jealousmarkup.xyz/off/chirpy/rx/
*
*/
void chirpy_demo_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
void chirpy_demo_face_activate(movement_settings_t *settings, void *context);
bool chirpy_demo_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
void chirpy_demo_face_resign(movement_settings_t *settings, void *context);
#define chirpy_demo_face ((const watch_face_t){ \
chirpy_demo_face_setup, \
chirpy_demo_face_activate, \
chirpy_demo_face_loop, \
chirpy_demo_face_resign, \
NULL, \
})
#endif // CHIRPY_DEMO_FACE_H_