'movement' -> 'legacy' to signal things we still need to bring in

This commit is contained in:
Joey Castillo
2024-11-27 10:56:50 -05:00
parent bc08c5a05e
commit 9719567047
221 changed files with 6 additions and 6 deletions

View File

@@ -0,0 +1,477 @@
/*
* MIT License
*
* Copyright (c) 2022 Joey Castillo
*
* 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 "accelerometer_data_acquisition_face.h"
#include "watch_utility.h"
#include "lis2dw.h"
#include "spiflash.h"
#define ACCELEROMETER_RANGE LIS2DW_RANGE_4_G
#define ACCELEROMETER_LPMODE LIS2DW_LP_MODE_2
#define ACCELEROMETER_FILTER LIS2DW_BANDWIDTH_FILTER_DIV2
#define ACCELEROMETER_LOW_NOISE true
#define SECONDS_TO_RECORD 15
static const char activity_types[][3] = {
"TE", // Testing
"ID", // Idle
"OF", // Off-wrist
"SL", // Sleeping
"WH", // Washing Hands
"WA", // Walking
"WB", // Walking with Beverage
"JO", // Jogging
"RU", // Running
"BI", // Biking
"HI", // Hiking
"EL", // Elliptical
"SU", // Stairs Up
"SD", // Stairs Down
"WL", // Weight Lifting
};
static void update(accelerometer_data_acquisition_state_t *state);
static void update_settings(accelerometer_data_acquisition_state_t *state);
static void advance_current_setting(accelerometer_data_acquisition_state_t *state);
static void start_reading(accelerometer_data_acquisition_state_t *state);
static void continue_reading(accelerometer_data_acquisition_state_t *state);
static void finish_reading(accelerometer_data_acquisition_state_t *state);
static bool wait_for_flash_ready(void);
static int16_t get_next_available_page(void);
static void write_buffer_to_page(uint8_t *buf, uint16_t page);
static void write_page(accelerometer_data_acquisition_state_t *state);
static void log_data_point(accelerometer_data_acquisition_state_t *state, lis2dw_reading_t reading, uint8_t centiseconds);
void accelerometer_data_acquisition_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
accelerometer_data_acquisition_state_t *state = (accelerometer_data_acquisition_state_t *)*context_ptr;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(accelerometer_data_acquisition_state_t));
memset(*context_ptr, 0, sizeof(accelerometer_data_acquisition_state_t));
state = (accelerometer_data_acquisition_state_t *)*context_ptr;
state->beep_with_countdown = true;
state->countdown_length = 3;
}
spi_flash_init();
wait_for_flash_ready();
uint8_t buf[256] = {0xFF};
spi_flash_read_data(0, buf, 256);
if (buf[0] & 0xF0) {
// mark first four pages as used
buf[0] = 0x0F;
wait_for_flash_ready();
HAL_GPIO_A3_clr();
spi_flash_command(CMD_ENABLE_WRITE);
wait_for_flash_ready();
spi_flash_write_data(0, buf, 256);
}
}
void accelerometer_data_acquisition_face_activate(void *context) {
accelerometer_data_acquisition_state_t *state = (accelerometer_data_acquisition_state_t *)context;
state->next_available_page = get_next_available_page();
}
bool accelerometer_data_acquisition_face_loop(movement_event_t event, void *context) {
accelerometer_data_acquisition_state_t *state = (accelerometer_data_acquisition_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
case EVENT_TICK:
switch (state->mode) {
case ACCELEROMETER_DATA_ACQUISITION_MODE_IDLE:
update(state);
if (state->repeat_ticks > 0) {
state->repeat_ticks--;
if (state->repeat_ticks == 0) {
state->countdown_ticks = state->countdown_length;
state->mode = ACCELEROMETER_DATA_ACQUISITION_MODE_COUNTDOWN;
}
}
break;
case ACCELEROMETER_DATA_ACQUISITION_MODE_COUNTDOWN:
if (state->next_available_page < 0) {
state->countdown_ticks = 0;
state->repeat_ticks = 0;
state->mode = ACCELEROMETER_DATA_ACQUISITION_MODE_IDLE;
}
if (state->countdown_ticks > 0) {
state->countdown_ticks--;
printf("countdown: %d\n", state->countdown_ticks);
if (state->countdown_ticks == 0) {
// at zero, begin reading
state->mode = ACCELEROMETER_DATA_ACQUISITION_MODE_SENSING;
state->reading_ticks = SECONDS_TO_RECORD + 1;
// also beep if the user asked for it
if (state->beep_with_countdown) watch_buzzer_play_note(BUZZER_NOTE_C6, 75);
start_reading(state);
} else if (state->countdown_ticks < 3) {
// beep for last two ticks before reading
if (state->beep_with_countdown) watch_buzzer_play_note(BUZZER_NOTE_C5, 75);
}
}
update(state);
break;
case ACCELEROMETER_DATA_ACQUISITION_MODE_SENSING:
if (state->reading_ticks > 0) {
state->reading_ticks--;
if (state->reading_ticks > 0) {
continue_reading(state);
} else {
finish_reading(state);
state->mode = ACCELEROMETER_DATA_ACQUISITION_MODE_IDLE;
watch_buzzer_play_note(BUZZER_NOTE_C4, 125);
watch_buzzer_play_note(BUZZER_NOTE_REST, 50);
watch_buzzer_play_note(BUZZER_NOTE_C4, 125);
}
}
update(state);
break;
case ACCELEROMETER_DATA_ACQUISITION_MODE_SETTINGS:
update_settings(state);
break;
}
break;
case EVENT_LIGHT_BUTTON_UP:
switch (state->mode) {
case ACCELEROMETER_DATA_ACQUISITION_MODE_IDLE:
state->activity_type_index = (state->activity_type_index + 1) % (sizeof(activity_types) / sizeof(activity_types[0]));
update(state);
break;
case ACCELEROMETER_DATA_ACQUISITION_MODE_SETTINGS:
state->settings_page++;
if (state->settings_page > ACCELEROMETER_DATA_ACQUISITION_SETTINGS_PAGE_REPEAT) {
state->settings_page = 0;
state->mode = ACCELEROMETER_DATA_ACQUISITION_MODE_IDLE;
update(state);
} else {
update_settings(state);
}
break;
default:
break;
}
break;
case EVENT_LIGHT_LONG_PRESS:
movement_illuminate_led();
break;
case EVENT_ALARM_BUTTON_UP:
printf("Alarm up! Mode is %d\n", state->mode);
switch (state->mode) {
case ACCELEROMETER_DATA_ACQUISITION_MODE_IDLE:
state->countdown_ticks = state->countdown_length;
printf("Setting countdown ticks to %d\n", state->countdown_ticks);
state->mode = ACCELEROMETER_DATA_ACQUISITION_MODE_COUNTDOWN;
printf("and mode to %d\n", state->mode);
update(state);
break;
case ACCELEROMETER_DATA_ACQUISITION_MODE_COUNTDOWN:
// cancel countdown
state->countdown_ticks = 0;
state->mode = ACCELEROMETER_DATA_ACQUISITION_MODE_IDLE;
update(state);
break;
case ACCELEROMETER_DATA_ACQUISITION_MODE_SETTINGS:
advance_current_setting(state);
update_settings(state);
break;
default:
break;
}
break;
case EVENT_ALARM_LONG_PRESS:
printf("Alarm long\n");
if (state->mode == ACCELEROMETER_DATA_ACQUISITION_MODE_IDLE) {
state->repeat_ticks = 0;
state->mode = ACCELEROMETER_DATA_ACQUISITION_MODE_SETTINGS;
update_settings(state);
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
// don't light up every time light is hit
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void accelerometer_data_acquisition_face_resign(void *context) {
accelerometer_data_acquisition_state_t *state = (accelerometer_data_acquisition_state_t *)context;
if (state->reading_ticks) {
state->reading_ticks = 0;
finish_reading(state);
}
state->mode = ACCELEROMETER_DATA_ACQUISITION_MODE_IDLE;
state->settings_page = 0;
state->countdown_ticks = 0;
state->repeat_ticks = 0;
state->reading_ticks = 0;
}
static void update(accelerometer_data_acquisition_state_t *state) {
char buf[14];
uint8_t ticks = 0;
switch (state->mode) {
case ACCELEROMETER_DATA_ACQUISITION_MODE_IDLE:
ticks = state->countdown_length;
break;
case ACCELEROMETER_DATA_ACQUISITION_MODE_COUNTDOWN:
ticks = state->countdown_ticks;
break;
case ACCELEROMETER_DATA_ACQUISITION_MODE_SENSING:
ticks = state->reading_ticks;
break;
default:
ticks = 0;
break;
}
sprintf(buf, "%s%2dre%2d#o",
activity_types[state->activity_type_index],
ticks,
(8192 - state->next_available_page) / 82);
watch_display_string(buf, 0);
watch_set_colon();
// special case: display full if full, <1% if nearly full
if (state->next_available_page < 0) watch_display_string(" FUL", 6);
else if (state->next_available_page > 8110) watch_display_string("<1", 6);
// Bell if beep enabled
if (state->beep_with_countdown) watch_set_indicator(WATCH_INDICATOR_BELL);
else watch_clear_indicator(WATCH_INDICATOR_BELL);
// Signal if sensing
if (state->reading_ticks) watch_set_indicator(WATCH_INDICATOR_SIGNAL);
else watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
// LAP if repeating
if (state->repeat_ticks) watch_set_indicator(WATCH_INDICATOR_LAP);
else watch_clear_indicator(WATCH_INDICATOR_LAP);
}
static void update_settings(accelerometer_data_acquisition_state_t *state) {
char buf[13];
watch_clear_colon();
if (state->beep_with_countdown) watch_set_indicator(WATCH_INDICATOR_BELL);
else watch_clear_indicator(WATCH_INDICATOR_BELL);
switch (state->settings_page) {
case ACCELEROMETER_DATA_ACQUISITION_SETTINGS_PAGE_SOUND:
sprintf(buf, "SO Beep %c", state->beep_with_countdown ? 'Y' : 'N');
watch_display_string(buf, 0);
break;
case ACCELEROMETER_DATA_ACQUISITION_SETTINGS_PAGE_DELAY:
sprintf(buf, "DL %2d SeC", state->countdown_length);
watch_display_string(buf, 0);
break;
case ACCELEROMETER_DATA_ACQUISITION_SETTINGS_PAGE_REPEAT:
if (state->repeat_interval == 0) {
watch_display_string("rE none ", 0);
} else {
sprintf(buf, "rE %2dn&in", state->repeat_interval / 60);
watch_display_string(buf, 0);
}
break;
}
}
static void advance_current_setting(accelerometer_data_acquisition_state_t *state) {
switch (state->settings_page) {
case ACCELEROMETER_DATA_ACQUISITION_SETTINGS_PAGE_SOUND:
state->beep_with_countdown = !state->beep_with_countdown;
break;
case ACCELEROMETER_DATA_ACQUISITION_SETTINGS_PAGE_DELAY:
// this is so lazy, i'm sorry
if (state->countdown_length == 1) state->countdown_length = 3;
else if (state->countdown_length == 3) state->countdown_length = 10;
else if (state->countdown_length == 10) state->countdown_length = 30;
else state->countdown_length = 1;
break;
case ACCELEROMETER_DATA_ACQUISITION_SETTINGS_PAGE_REPEAT:
if (state->repeat_interval == 0) state->repeat_interval = 60;
else if (state->repeat_interval == 60) state->repeat_interval = 600;
else if (state->repeat_interval == 600) state->repeat_interval = 1800;
else if (state->repeat_interval == 1800) state->repeat_interval = 3600;
else state->repeat_interval = 0;
break;
}
}
static int16_t get_next_available_page(void) {
uint8_t buf[256] = {0};
uint16_t page = 0;
for(int16_t i = 0; i < 4; i++) {
wait_for_flash_ready();
spi_flash_read_data(i * 256, buf, 256);
for(int16_t j = 0; j < 256; j++) {
if(buf[j] == 0) {
page += 8;
} else {
page += __builtin_clz(((uint32_t)buf[j]) << 24);
break;
}
}
}
if (page >= 8192) return -1;
return page;
}
static void write_buffer_to_page(uint8_t *buf, uint16_t page) {
uint32_t address = 256 * page;
wait_for_flash_ready();
HAL_GPIO_A3_clr();
spi_flash_command(CMD_ENABLE_WRITE);
wait_for_flash_ready();
HAL_GPIO_A3_clr();
spi_flash_write_data(address, buf, 256);
wait_for_flash_ready();
uint8_t buf2[256];
HAL_GPIO_A3_clr();
spi_flash_read_data(address, buf2, 256);
wait_for_flash_ready();
uint8_t used_pages[256] = {0xFF};
uint16_t address_to_mark_used = page / 8;
uint8_t header_page = address_to_mark_used / 256;
uint8_t used_byte = 0x7F >> (page % 8);
uint8_t offset_in_buf = address_to_mark_used % 256;
printf("\twrite 256 bytes to address %ld, page %d.\n", address, page);
for(int i = 0; i < 256; i++) {
if (buf[i] != buf2[i]) {
printf("\tData mismatch detected at offset %d: %d != %d.\n", i, buf[i], buf2[i]);
}
}
HAL_GPIO_A3_clr();
spi_flash_read_data(header_page * 256, used_pages, 256);
used_pages[offset_in_buf] = used_byte;
HAL_GPIO_A3_clr();
spi_flash_command(CMD_ENABLE_WRITE);
wait_for_flash_ready();
HAL_GPIO_A3_clr();
spi_flash_write_data(header_page * 256, used_pages, 256);
wait_for_flash_ready();
}
static bool wait_for_flash_ready(void) {
HAL_GPIO_A3_clr();
bool ok = true;
uint8_t read_status_response[1] = {0x00};
do {
ok = spi_flash_read_command(CMD_READ_STATUS, read_status_response, 1);
} while ((read_status_response[0] & 0x3) != 0);
delay_ms(1); // why do i need this?
HAL_GPIO_A3_set();
return ok;
}
static void write_page(accelerometer_data_acquisition_state_t *state) {
if (state->next_available_page > 0) {
write_buffer_to_page((uint8_t *)(state->records), state->next_available_page);
wait_for_flash_ready();
state->next_available_page++;
}
state->pos = 0;
memset(state->records, 0xFF, sizeof(state->records));
}
static void log_data_point(accelerometer_data_acquisition_state_t *state, lis2dw_reading_t reading, uint8_t centiseconds) {
accelerometer_data_acquisition_record_t record;
record.data.x.record_type = ACCELEROMETER_DATA_ACQUISITION_DATA;
record.data.y.lpmode = ACCELEROMETER_LPMODE;
record.data.z.filter = ACCELEROMETER_FILTER;
record.data.x.accel = (reading.x >> 2) + 8192;
record.data.y.accel = (reading.y >> 2) + 8192;
record.data.z.accel = (reading.z >> 2) + 8192;
record.data.counter = 100 * (SECONDS_TO_RECORD - state->reading_ticks + 1) + centiseconds;
printf("logged data point for %d\n", record.data.counter);
state->records[state->pos++] = record;
if (state->pos >= 32) {
write_page(state);
}
}
static void start_reading(accelerometer_data_acquisition_state_t *state) {
printf("Start reading\n");
watch_enable_i2c();
lis2dw_begin();
lis2dw_set_data_rate(LIS2DW_DATA_RATE_25_HZ);
lis2dw_set_range(ACCELEROMETER_RANGE);
lis2dw_set_low_power_mode(ACCELEROMETER_LPMODE);
lis2dw_set_bandwidth_filtering(ACCELEROMETER_FILTER);
if (ACCELEROMETER_LOW_NOISE) lis2dw_set_low_noise_mode(true);
lis2dw_enable_fifo();
accelerometer_data_acquisition_record_t record;
watch_date_time_t date_time = watch_rtc_get_date_time();
state->starting_timestamp = watch_utility_date_time_to_unix_time(date_time, movement_get_current_timezone_offset());
record.header.info.record_type = ACCELEROMETER_DATA_ACQUISITION_HEADER;
record.header.info.range = ACCELEROMETER_RANGE;
record.header.info.temperature = lis2dw_get_temperature();
record.header.char1 = activity_types[state->activity_type_index][0];
record.header.char2 = activity_types[state->activity_type_index][1];
record.header.timestamp = state->starting_timestamp;
state->records[state->pos++] = record;
lis2dw_fifo_t fifo;
lis2dw_read_fifo(&fifo); // dump the fifo, this starts a fresh round of data in continue_reading
}
static void continue_reading(accelerometer_data_acquisition_state_t *state) {
printf("Continue reading\n");
lis2dw_fifo_t fifo;
lis2dw_read_fifo(&fifo);
fifo.count = min(fifo.count, 25); // hacky, but we need a consistent data rate; if we got a 26th data point, chuck it.
uint8_t offset = 4 * (25 - fifo.count); // also hacky: we're sometimes short at the start. align to beginning of next second.
// TODO: use the threshold interrupt for this, will mean we get consistent 25 Hz data as the accelerometer sees it.
for(int i = 0; i < fifo.count; i++) {
log_data_point(state, fifo.readings[i], i * 4 + offset);
}
}
static void finish_reading(accelerometer_data_acquisition_state_t *state) {
printf("Finish reading\n");
if (state->pos != 0) {
write_page(state);
}
lis2dw_set_data_rate(LIS2DW_DATA_RATE_POWERDOWN);
watch_disable_i2c();
state->repeat_ticks = state->repeat_interval;
}

View File

@@ -0,0 +1,117 @@
/*
* MIT License
*
* Copyright (c) 2022 Joey Castillo
*
* 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 ACCELEROMETER_DATA_ACQUISITION_FACE_H_
#define ACCELEROMETER_DATA_ACQUISITION_FACE_H_
/*
* ACCELEROMETER DATA ACQUISITION
*
* TODO: Add description here, including controls.
*/
#include "movement.h"
#define ACCELEROMETER_DATA_ACQUISITION_INVALID ((uint64_t)(0b11)) // all bits are 1 when the flash is erased
#define ACCELEROMETER_DATA_ACQUISITION_HEADER ((uint64_t)(0b10))
#define ACCELEROMETER_DATA_ACQUISITION_DATA ((uint64_t)(0b01))
#define ACCELEROMETER_DATA_ACQUISITION_DELETED ((uint64_t)(0b00)) // You can always write a 0 to any 1 bit
typedef union {
struct {
struct {
uint16_t record_type : 2; // see above, helps us identify record types when reading back
uint16_t range : 2; // accelerometer range (see lis2dw_range_t)
uint16_t temperature : 12; // raw value from the temperature sensor
} info;
uint8_t char1 : 8; // First character of the activity type
uint8_t char2 : 8; // Second character of the activity type
uint32_t timestamp : 32; // UNIX timestamp for the measurement
} header;
struct {
struct {
uint16_t record_type : 2; // duplicate; this is the same field as info above
uint16_t accel : 14; // X acceleration value, raw, offset by 8192
} x;
struct {
uint16_t lpmode : 2; // low power mode (see lis2dw_low_power_mode_t)
uint16_t accel : 14; // Y acceleration value, raw, offset by 8192
} y;
struct {
uint16_t filter : 2; // bandwidth filtering selection (see lis2dw_bandwidth_filtering_mode_t)
uint16_t accel : 14; // Z acceleration value, raw, offset by 8192
} z;
uint32_t counter : 16; // number of centiseconds since timestamp in header
} data;
uint64_t value;
} accelerometer_data_acquisition_record_t;
typedef enum {
ACCELEROMETER_DATA_ACQUISITION_MODE_IDLE,
ACCELEROMETER_DATA_ACQUISITION_MODE_COUNTDOWN,
ACCELEROMETER_DATA_ACQUISITION_MODE_SENSING,
ACCELEROMETER_DATA_ACQUISITION_MODE_SETTINGS,
} accelerometer_data_acquisition_mode_t;
typedef enum {
ACCELEROMETER_DATA_ACQUISITION_SETTINGS_PAGE_SOUND,
ACCELEROMETER_DATA_ACQUISITION_SETTINGS_PAGE_DELAY,
ACCELEROMETER_DATA_ACQUISITION_SETTINGS_PAGE_REPEAT,
// ACCELEROMETER_DATA_ACQUISITION_SETTINGS_PAGE_NAME,
} accelerometer_data_acquisition_settings_page_t;
typedef struct {
// mode
accelerometer_data_acquisition_mode_t mode;
accelerometer_data_acquisition_settings_page_t settings_page;
// current settings
uint8_t activity_type_index; // active activity type
bool beep_with_countdown; // should we beep at the countdown
uint8_t countdown_length; // how many seconds to count down
uint16_t repeat_interval; // how many seconds to wait for a repeat
// info about the flash chip
int16_t next_available_page;
// transient properties
uint8_t countdown_ticks;
uint8_t repeat_ticks;
uint8_t reading_ticks;
uint32_t starting_timestamp;
accelerometer_data_acquisition_record_t records[32];
uint16_t pos;
} accelerometer_data_acquisition_state_t;
void accelerometer_data_acquisition_face_setup(uint8_t watch_face_index, void ** context_ptr);
void accelerometer_data_acquisition_face_activate(void *context);
bool accelerometer_data_acquisition_face_loop(movement_event_t event, void *context);
void accelerometer_data_acquisition_face_resign(void *context);
#define accelerometer_data_acquisition_face ((const watch_face_t){ \
accelerometer_data_acquisition_face_setup, \
accelerometer_data_acquisition_face_activate, \
accelerometer_data_acquisition_face_loop, \
accelerometer_data_acquisition_face_resign, \
NULL, \
})
#endif // ACCELEROMETER_DATA_ACQUISITION_FACE_H_

View File

@@ -0,0 +1,151 @@
/*
* MIT License
*
* Copyright (c) 2024 Christian Buschau
*
* 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 <math.h>
#include <stdlib.h>
#include <string.h>
#include "alarm_thermometer_face.h"
#include "thermistor_driver.h"
static float _alarm_thermometer_face_update(bool in_fahrenheit) {
thermistor_driver_enable();
float temperature_c = thermistor_driver_get_temperature();
char buf[14];
if (in_fahrenheit) {
sprintf(buf, "%4.1f#F", temperature_c * 1.8 + 32.0);
} else {
sprintf(buf, "%4.1f#C", temperature_c);
}
watch_display_string(buf, 4);
thermistor_driver_disable();
return temperature_c;
}
static void _alarm_thermometer_face_clear(int last[]) {
for (size_t i = 0; i < LAST_SIZE; i++) {
last[i] = INT_MIN;
}
}
void alarm_thermometer_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(alarm_thermometer_state_t));
memset(*context_ptr, 0, sizeof(alarm_thermometer_state_t));
}
}
void alarm_thermometer_face_activate(void *context) {
alarm_thermometer_state_t *state = (alarm_thermometer_state_t *)context;
state->mode = MODE_NORMAL;
_alarm_thermometer_face_clear(state->last);
watch_display_string("AT", 0);
}
bool alarm_thermometer_face_loop(movement_event_t event, void *context) {
alarm_thermometer_state_t *state = (alarm_thermometer_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
_alarm_thermometer_face_update(movement_use_imperial_units());
break;
case EVENT_TICK:
if (watch_rtc_get_date_time().unit.second % 5 == 0) {
switch (state->mode) {
case MODE_NORMAL:
_alarm_thermometer_face_update(movement_use_imperial_units());
break;
case MODE_ALARM:
for (size_t i = LAST_SIZE - 1; i > 0; i--) {
state->last[i] = state->last[i - 1];
}
state->last[0] = roundf(_alarm_thermometer_face_update(movement_use_imperial_units()) * 10.0f);
bool constant = true;
for (size_t i = 1; i < LAST_SIZE; i++) {
if (state->last[i - 1] != state->last[i]) {
constant = false;
break;
}
}
if (constant) {
state->mode = MODE_FREEZE;
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
movement_play_alarm();
}
break;
case MODE_FREEZE:
break;
}
}
break;
case EVENT_ALARM_BUTTON_UP:
switch (state->mode) {
case MODE_NORMAL:
state->mode = MODE_ALARM;
watch_set_indicator(WATCH_INDICATOR_BELL);
_alarm_thermometer_face_clear(state->last);
break;
case MODE_FREEZE:
state->mode = MODE_NORMAL;
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
break;
case MODE_ALARM:
state->mode = MODE_NORMAL;
watch_clear_indicator(WATCH_INDICATOR_BELL);
_alarm_thermometer_face_update(movement_use_imperial_units());
break;
}
if (movement_button_should_sound()) {
watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
}
break;
case EVENT_ALARM_LONG_PRESS:
if (state->mode != MODE_FREEZE) {
movement_set_use_imperial_units(!movement_use_imperial_units());
_alarm_thermometer_face_update(movement_use_imperial_units());
}
break;
case EVENT_LOW_ENERGY_UPDATE:
if (!watch_sleep_animation_is_running()) {
state->mode = MODE_NORMAL;
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
watch_start_sleep_animation(1000);
}
if (watch_rtc_get_date_time().unit.minute % 5 == 0) {
_alarm_thermometer_face_update(movement_use_imperial_units());
watch_display_string(" ", 8);
}
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void alarm_thermometer_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,74 @@
/*
* MIT License
*
* Copyright (c) 2024 Christian Buschau
*
* 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 ALARM_THERMOMETER_FACE_H_
#define ALARM_THERMOMETER_FACE_H_
#include <limits.h>
#include "movement.h"
/*
* ALARM THERMOMETER
*
* This watch face shows the current temperature in degrees Celsius. Press and
* hold the alarm button to toggle between Celsius and Fahrenheit. Press and
* release the alarm button to start a "timer". The watch will sound an alarm
* when the temperature remains constant for at least 30 seconds and the
* temperature will stop updating until you press the alarm button. You can
* cancel the alarm by pressing the button again. If the temperature doesn't
* remain constant until the low energy timeout is reached, the alarm will stop.
* This is useful to measure e.g. the room temperature. If you lay off your
* watch from your wrist, it will take some time until it cools down, and will
* notify you when the measurement is constant enough.
* THIS WATCH FACE IS NOT INTENDED TO DIAGNOSE, TREAT, CURE OR PREVENT ANY
* DISEASE.
*/
#define LAST_SIZE 6
typedef enum {
MODE_NORMAL,
MODE_ALARM,
MODE_FREEZE
} alarm_thermometer_mode_t;
typedef struct {
int last[LAST_SIZE];
alarm_thermometer_mode_t mode;
} alarm_thermometer_state_t;
void alarm_thermometer_face_setup(uint8_t watch_face_index, void ** context_ptr);
void alarm_thermometer_face_activate(void *context);
bool alarm_thermometer_face_loop(movement_event_t event, void *context);
void alarm_thermometer_face_resign(void *context);
#define alarm_thermometer_face ((const watch_face_t){ \
alarm_thermometer_face_setup, \
alarm_thermometer_face_activate, \
alarm_thermometer_face_loop, \
alarm_thermometer_face_resign, \
NULL, \
})
#endif // ALARM_THERMOMETER_FACE_H_

View File

@@ -0,0 +1,171 @@
/*
* MIT License
*
* Copyright (c) 2022 CC
*
* 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 <math.h>
#include "lightmeter_face.h"
#include "watch_utility.h"
#include "watch_slcd.h"
uint16_t lightmeter_mod(uint16_t m, uint16_t n) { return (m%n + n)%n; }
void lightmeter_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(lightmeter_state_t));
lightmeter_state_t *state = (lightmeter_state_t*) *context_ptr;
state->waiting_for_conversion = 0;
state->lux = 0.0;
state->mode = 0;
state->iso = LIGHTMETER_ISO_100;
state->ap = LIGHTMETER_AP_4P0;
}
}
void lightmeter_face_activate(void *context) {
lightmeter_state_t *state = (lightmeter_state_t*) context;
state->waiting_for_conversion = 0;
lightmeter_show_ev(state); // Print most current reading
watch_enable_i2c();
return;
}
void lightmeter_show_ev(lightmeter_state_t *state) {
float ev = max(min(
log2(state->lux) +
lightmeter_isos[state->iso].ev +
LIGHTMETER_CALIBRATION,
99), -9);
int evt = round(2*ev); // Truncated EV
// Print EV
char strbuff[7];
watch_clear_all_indicators();
watch_display_string("EV ", 0);
sprintf(strbuff, "%2i", (uint16_t) abs(evt/2)); // Print whole part of EV
watch_display_string(strbuff, 2);
if(evt%2) watch_set_indicator(WATCH_INDICATOR_LAP); // Indicate half stop
if(ev<0) watch_set_pixel(1,9); // Indicate negative EV
// Handle lux mode
if(state->mode == 1) {
sprintf(strbuff, "%6.0f", min(state->lux, 999999.0));
watch_display_string(strbuff, 4);
return;
}
// Find and print best shutter speed
uint16_t bestsh = 0;
float besterr = 1.0/0.0;
float errbuf = 1.0/0.0;
float comp_ev = ev + lightmeter_aps[state->ap].ev;
for(uint16_t ind = 2; ind < LIGHTMETER_N_SHS; ind++) {
errbuf = comp_ev + lightmeter_shs[ind].ev;
if( fabs(errbuf) < fabs(besterr)) {
besterr = errbuf;
bestsh = ind;
}
}
if(besterr >= 0.5) watch_display_string(lightmeter_shs[LIGHTMETER_SH_HIGH].str, 4);
else if(besterr <= -0.5) watch_display_string(lightmeter_shs[LIGHTMETER_SH_LOW].str, 4);
else watch_display_string(lightmeter_shs[bestsh].str, 4);
// Print aperture
watch_display_string(lightmeter_aps[state->ap].str, 7);
return;
}
bool lightmeter_face_loop(movement_event_t event, void *context) {
lightmeter_state_t *state = (lightmeter_state_t*) context;
opt3001_Config_t c;
switch (event.event_type) {
case EVENT_TICK:
if(state->waiting_for_conversion) { // Check if measurement is ready...
c = opt3001_readConfig(lightmeter_addr);
if(c.ConversionReady) {
state->waiting_for_conversion = 0;
opt3001_t result = opt3001_readResult(lightmeter_addr);
state->lux = result.lux;
lightmeter_show_ev(state);
}
}
break;
case EVENT_ALARM_BUTTON_UP: // Increment aperture
state->ap = lightmeter_mod(state->ap+1, LIGHTMETER_N_APS);
lightmeter_show_ev(state);
break;
case EVENT_LIGHT_BUTTON_UP: // Decrement aperture
if(state->ap == 0) state->ap = LIGHTMETER_N_APS-1;
else state->ap = lightmeter_mod(state->ap-1, LIGHTMETER_N_APS);
lightmeter_show_ev(state);
break;
case EVENT_LIGHT_LONG_PRESS: // Cycle ISO
state->iso = lightmeter_mod(state->iso+1, LIGHTMETER_N_ISOS);
watch_clear_all_indicators();
watch_display_string("EV ", 0);
watch_display_string(lightmeter_isos[state->iso].str, 4);
break;
case EVENT_ALARM_LONG_PRESS: // Take measurement
opt3001_writeConfig(lightmeter_addr, lightmeter_takeNewReading);
state->waiting_for_conversion = 1;
watch_clear_all_indicators();
watch_display_string("EV ", 0);
watch_display_string(lightmeter_isos[state->iso].str, 4);
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
break;
case EVENT_MODE_LONG_PRESS: // Toggle mode
state->mode = !state->mode;
lightmeter_show_ev(state);
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void lightmeter_face_resign(void *context) {
(void) context;
opt3001_writeConfig(lightmeter_addr, lightmeter_off);
watch_disable_i2c();
return;
}

View File

@@ -0,0 +1,191 @@
/*
* MIT License
*
* Copyright (c) 2023 CC
*
* 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 LIGHTMETER_FACE_H_
#define LIGHTMETER_FACE_H_
/*
* Aperture-priority Light Meter Face
*
* Tested with the "Q3Q-SWAB-A1-00 Temperature + Test Points + OPT3001" flexboard.
* This flexboard could use a revision:
*
* - The thermistor components should be moved west a mm or flipped to the backside
* to avoid stressing the flexboard against the processor so much.
* - The 'no connect' pad falls off easily.
*
* Controls:
*
* - Trigger a measurement by long-pressing Alarm.
* Sensor integration is happening when the Signal indicator is on.
*
* - ISO setting can be cycled by long-pressing Light.
* During integration the current ISO setting will be displayed.
*
* - EV measurement in the top right: "LAP" indicates "half stop".
* So "LAP -1" means EV = -1.5. Likewise "LAP 13" means EV = +13.5
*
* - Aperture in the bottom right: the last 3 main digits are the f-stop.
* Adjust this number in half-stop increments using Alarm = +1/2 and Light = -1/2.
*
* - Best shutter speed in the bottom left: the first 3 digits are the shutter speed.
* Some special chars are needed here: "-" = seconds, "h" = extra half second, "K" = thousands.
* "HI" or "LO" if there's no shutter in the dictionary within 0.5 stops of correct exposure.
*
* - Mode long-press changes the main digits to show raw sensor lux measurements.
*/
#include "movement.h"
#include "opt3001.h"
#define LIGHTMETER_CALIBRATION 2.58
typedef struct {
char * str;
float ev;
} lightmeter_ev_t;
static const lightmeter_ev_t lightmeter_isos[] = {
{" i 25", -2},
{" i 50", -1},
{" i 100", 0},
{" i 160", 0.68},
{" i 200", 1},
{" i 400", 2},
{" i 800", 3},
{" i1600", 4}};
typedef enum {
LIGHTMETER_ISO_25, LIGHTMETER_ISO_50, LIGHTMETER_ISO_100, LIGHTMETER_ISO_160, LIGHTMETER_ISO_200, LIGHTMETER_ISO_400, LIGHTMETER_ISO_800, LIGHTMETER_ISO_1600,
LIGHTMETER_N_ISOS
} lightmeter_iso_t;
static const lightmeter_ev_t lightmeter_aps[] = {
{"1.4", 0},
{"1.8", -0.5},
{"2.0", -1},
{"2.4", -1.5},
{"2.8", -2},
{"3.3", -2.5},
{"4.0", -3},
{"4.8", -3.5},
{"5.6", -4},
{"6.7", -4.5},
{"8.0", -5},
{"9.5", -5.5},
{"11.", -6},
{"13.", -6.5},
{"16.", -7},
{"19.", -7.5},
{"22.", -8}};
typedef enum {
LIGHTMETER_AP_1P4, LIGHTMETER_AP_1P8, LIGHTMETER_AP_2P0, LIGHTMETER_AP_2P4, LIGHTMETER_AP_2P8, LIGHTMETER_AP_3P3, LIGHTMETER_AP_4P0, LIGHTMETER_AP_4P8, LIGHTMETER_AP_5P6, LIGHTMETER_AP_6P7, LIGHTMETER_AP_8P0, LIGHTMETER_AP_9P5,
LIGHTMETER_AP_11, LIGHTMETER_AP_13, LIGHTMETER_AP_16, LIGHTMETER_AP_19, LIGHTMETER_AP_22,
LIGHTMETER_N_APS
} lightmeter_ap_t;
static const lightmeter_ev_t lightmeter_shs[] = {
{"LO-", 99},
{"HI ", -99},
{"30-", 5.0},
{"20-", 4.5},
{"15-", 4.0},
{"11-", 3.5},
{"8- ", 3.0},
{"6- ", 2.5},
{"4- ", 2.0},
{"3- ", 1.5},
{"2- ", 1.0},
{"1h-", 0.5},
{"1 ", 0.0},
{"1h ", -0.5},
{"2 ", -1.0},
{"3 ", -1.5},
{"4 ", -2.0},
{"6 ", -2.5},
{"8 ", -3.0},
{"12 ", -3.5},
{"15 ", -4.0},
{"20 ", -4.5},
{"30 ", -5.0},
{"45 ", -5.5},
{"60 ", -6.0},
{"90 ", -6.5},
{"125", -7.0},
{"180", -7.5},
{"250", -8.0},
{"350", -8.5},
{"500", -9.0},
{"750", -9.5},
{"1K ", -10.0},
{"1K5", -10.5},
{"2K ", -11.0},
{"3K ", -11.5},
{"4K ", -12.0},
{"6K ", -12.5},
{"8K ", -13.0}};
typedef enum {
LIGHTMETER_SH_LOW, LIGHTMETER_SH_HIGH,
LIGHTMETER_SH_30S, LIGHTMETER_SH_20S, LIGHTMETER_SH_15S, LIGHTMETER_SH_11S, LIGHTMETER_SH_8S, LIGHTMETER_SH_6S, LIGHTMETER_SH_3S, LIGHTMETER_SH_4S, LIGHTMETER_SH_2S, LIGHTMETER_SH_1HS,
LIGHTMETER_SH_1, LIGHTMETER_SH_1H, LIGHTMETER_SH_2, LIGHTMETER_SH_3, LIGHTMETER_SH_4, LIGHTMETER_SH_6, LIGHTMETER_SH_8, LIGHTMETER_SH_12, LIGHTMETER_SH_15, LIGHTMETER_SH_20, LIGHTMETER_SH_30, LIGHTMETER_SH_45, LIGHTMETER_SH_60, LIGHTMETER_SH_90, LIGHTMETER_SH_125, LIGHTMETER_SH_180, LIGHTMETER_SH_250, LIGHTMETER_SH_350, LIGHTMETER_SH_500, LIGHTMETER_SH_750,
LIGHTMETER_SH_1K, LIGHTMETER_SH_1K5, LIGHTMETER_SH_2K, LIGHTMETER_SH_3K, LIGHTMETER_SH_4K, LIGHTMETER_SH_6K, LIGHTMETER_SH_8K,
LIGHTMETER_N_SHS
} lightmeter_sh_t;
typedef struct {
lightmeter_iso_t iso;
lightmeter_ap_t ap;
bool waiting_for_conversion;
float lux;
int mode;
} lightmeter_state_t;
static const opt3001_Config_t lightmeter_takeNewReading = {
.RangeNumber = 0B1100,
.ConversionTime = 0B1,
.Latch = 0B1,
.ModeOfConversionOperation = 0B01
};
static const opt3001_Config_t lightmeter_off = {
.ModeOfConversionOperation = 0B00
};
uint16_t lightmeter_mod(uint16_t m, uint16_t n);
void lightmeter_face_setup(uint8_t watch_face_index, void ** context_ptr);
void lightmeter_face_activate(void *context);
void lightmeter_show_ev(lightmeter_state_t *state);
bool lightmeter_face_loop(movement_event_t event, void *context);
void lightmeter_face_resign(void *context);
static const uint8_t lightmeter_addr = 0x44;
#define lightmeter_face ((const watch_face_t){ \
lightmeter_face_setup, \
lightmeter_face_activate, \
lightmeter_face_loop, \
lightmeter_face_resign, \
NULL, \
})
#endif // LIGHTMETER_FACE_H_

View File

@@ -0,0 +1,155 @@
/*
* MIT License
*
* Copyright (c) 2023 Mark Blyth
*
* 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 "minmax_face.h"
#include "thermistor_driver.h"
#include "watch.h"
static float _get_displayed_temperature_c(minmax_state_t *state){
float min_temp = 1000;
float max_temp = -1000;
for(int i = 0; i < LOGGING_DATA_POINTS; i++){
if(state->hourly_maxs[i] > max_temp){
max_temp = state->hourly_maxs[i];
}
if(state->hourly_mins[i] < min_temp){
min_temp = state->hourly_mins[i];
}
}
if(state->show_min) return min_temp;
return max_temp;
}
static void _minmax_face_log_data(minmax_state_t *logger_state) {
thermistor_driver_enable();
size_t pos = (size_t) watch_rtc_get_date_time().unit.hour;
float temp_c = thermistor_driver_get_temperature();
// If no data yet, initialise with current temperature
if(!logger_state->have_logged){
logger_state->have_logged = true;
for(int i=0; i<LOGGING_DATA_POINTS; i++){
logger_state->hourly_mins[i] = temp_c;
logger_state->hourly_maxs[i] = temp_c;
}
}
// On new hour, update lists to current temperature
else if(watch_rtc_get_date_time().unit.minute < 2){
logger_state->hourly_mins[pos] = temp_c;
logger_state->hourly_maxs[pos] = temp_c;
}
// Log hourly highs and lows
else if(logger_state->hourly_mins[pos] > temp_c){
logger_state->hourly_mins[pos] = temp_c;
}
else if(logger_state->hourly_maxs[pos] < temp_c){
logger_state->hourly_maxs[pos] = temp_c;
}
thermistor_driver_disable();
}
static void _minmax_face_update_display(float temperature_c, bool in_fahrenheit) {
char buf[14];
if (in_fahrenheit) {
sprintf(buf, "%4.0f#F", temperature_c * 1.8 + 32.0);
} else {
sprintf(buf, "%4.0f#C", temperature_c);
}
watch_display_string(buf, 4);
}
void minmax_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(minmax_state_t));
memset(*context_ptr, 0, sizeof(minmax_state_t));
}
}
void minmax_face_activate(void *context) {
minmax_state_t *state = (minmax_state_t *)context;
state->show_min = true;
watch_display_string("MN", 0); // Start with minimum temp
}
bool minmax_face_loop(movement_event_t event, void *context) {
minmax_state_t *state = (minmax_state_t *)context;
float temp_c;
switch (event.event_type) {
case EVENT_ACTIVATE:
temp_c = _get_displayed_temperature_c(state);
_minmax_face_update_display(temp_c, movement_use_imperial_units());
break;
case EVENT_LIGHT_LONG_PRESS:
movement_set_use_imperial_units(!movement_use_imperial_units());
temp_c = _get_displayed_temperature_c(state);
_minmax_face_update_display(temp_c, movement_use_imperial_units());
break;
case EVENT_ALARM_BUTTON_UP:
state->show_min = !state->show_min;
if(state->show_min){
watch_display_string("MN", 0);
} else {
watch_display_string("MX", 0);
}
temp_c = _get_displayed_temperature_c(state);
_minmax_face_update_display(temp_c, movement_use_imperial_units());
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
case EVENT_BACKGROUND_TASK:
_minmax_face_log_data(state);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void minmax_face_resign(void *context) {
(void) context;
}
movement_watch_face_advisory_t minmax_face_advise(void *context) {
(void) context;
// this will get called at the top of each minute; always request bg task
movement_watch_face_advisory_t retval = {
.wants_background_task = 1,
};
return retval;
}

View File

@@ -0,0 +1,69 @@
/*
* MIT License
*
* Copyright (c) 2023 Mark Blyth
*
* 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 MINMAX_FACE_H_
#define MINMAX_FACE_H_
#include "movement.h"
#include "watch.h"
#define LOGGING_DATA_POINTS (24)
/*
* Log for the min. and max. temperature over the last 24h.
*
* Temperature is logged once a minute, every minute. Results are
* stored, noting the highest and lowest temperatures observed within
* any given hour. The watch face then displays the minimum or maximum
* temperature recorded over the last 24h.
*
* A long press of the light button changes units between Celsius and
* Fahrenheit. Pressing the alarm button switches between displaying the
* minimum and maximum observed temperatures. If no buttons are pressed,
* the watch face will eventually time out and return home.
*/
typedef struct {
bool show_min;
bool have_logged;
float hourly_mins[LOGGING_DATA_POINTS];
float hourly_maxs[LOGGING_DATA_POINTS];
} minmax_state_t;
void minmax_face_setup(uint8_t watch_face_index, void ** context_ptr);
void minmax_face_activate(void *context);
bool minmax_face_loop(movement_event_t event, void *context);
void minmax_face_resign(void *context);
movement_watch_face_advisory_t minmax_face_advise(void *context);
#define minmax_face ((const watch_face_t){ \
minmax_face_setup, \
minmax_face_activate, \
minmax_face_loop, \
minmax_face_resign, \
minmax_face_advise, \
})
#endif // MINMAX_FACE_H_

View File

@@ -0,0 +1,79 @@
/*
* MIT License
*
* Copyright (c) 2022 Joey Castillo
*
* 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 "thermistor_testing_face.h"
#include "thermistor_driver.h"
#include "watch.h"
static void _thermistor_testing_face_update_display(bool in_fahrenheit) {
thermistor_driver_enable();
float temperature_c = thermistor_driver_get_temperature();
char buf[14];
if (in_fahrenheit) {
sprintf(buf, "%4.1f#F", temperature_c * 1.8 + 32.0);
} else {
sprintf(buf, "%4.1f#C", temperature_c);
}
watch_display_string(buf, 4);
thermistor_driver_disable();
}
void thermistor_testing_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
(void) context_ptr;
// force one setting: never enter low energy mode.
// I'm using this watch face to test the temperature sensor boards; there's no need for it.
movement_set_low_energy_timeout(0);
}
void thermistor_testing_face_activate(void *context) {
(void) context;
watch_display_string("TE", 0);
movement_request_tick_frequency(8);
}
bool thermistor_testing_face_loop(movement_event_t event, void *context) {
(void) context;
switch (event.event_type) {
case EVENT_ALARM_BUTTON_DOWN:
movement_set_use_imperial_units(!movement_use_imperial_units());
_thermistor_testing_face_update_display(movement_use_imperial_units());
break;
case EVENT_ACTIVATE:
case EVENT_TICK:
_thermistor_testing_face_update_display(movement_use_imperial_units());
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void thermistor_testing_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,54 @@
/*
* MIT License
*
* Copyright (c) 2022 Joey Castillo
*
* 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 THERMISTOR_TESTING_FACE_H_
#define THERMISTOR_TESTING_FACE_H_
/*
* THERMISTOR TESTING FACE
*
* This watch face is designed for testing temperature sensor boards.
* It displays temperature readings at a relatively fast rate of 8 Hz,
* and disables low energy mode so my testing device doesn't sleep.
* You more than likely want to use temperature_display_face instead.
*
* Press ALARM to toggle display of metric vs. imperial units.
*/
#include "movement.h"
void thermistor_testing_face_setup(uint8_t watch_face_index, void ** context_ptr);
void thermistor_testing_face_activate(void *context);
bool thermistor_testing_face_loop(movement_event_t event, void *context);
void thermistor_testing_face_resign(void *context);
#define thermistor_testing_face ((const watch_face_t){ \
thermistor_testing_face_setup, \
thermistor_testing_face_activate, \
thermistor_testing_face_loop, \
thermistor_testing_face_resign, \
NULL, \
})
#endif // THERMISTOR_TESTING_FACE_H_