From ee76baa6d8de7e0398895c11c7c05089b34e6b2c Mon Sep 17 00:00:00 2001 From: J Date: Sun, 22 Jan 2023 22:27:54 +0000 Subject: [PATCH] Uploaded files --- application.fam | 12 ++ clock_app.c | 336 ++++++++++++++++++++++++++++++++++++++++++++++++ clock_app.h | 39 ++++++ 3 files changed, 387 insertions(+) create mode 100644 application.fam create mode 100644 clock_app.c create mode 100644 clock_app.h diff --git a/application.fam b/application.fam new file mode 100644 index 00000000000..1eaa1732b2c --- /dev/null +++ b/application.fam @@ -0,0 +1,12 @@ +App( + appid="nightstand", + name="Nightstand", + apptype=FlipperAppType.EXTERNAL, + entry_point="clock_app", + requires=["gui"], + icon="A_Clock_14", + stack_size=2 * 1024, + fap_category="Misc", + order=81, +) + diff --git a/clock_app.c b/clock_app.c new file mode 100644 index 00000000000..80e16946cad --- /dev/null +++ b/clock_app.c @@ -0,0 +1,336 @@ +#include +#include + +#include +#include + +#include +#include + +#include "clock_app.h" + +/* + This is a modified version of the default clock app intended for use overnight + Up / Down control the displays brightness. Down at brightness 0 turns the notification LED on and off. +*/ + +int brightness = 5; +bool led = false; +NotificationApp* notif = 0; + +const NotificationMessage message_red_dim = { + .type = NotificationMessageTypeLedRed, + .data.led.value = 0xFF / 16, +}; + +const NotificationMessage message_red_off = { + .type = NotificationMessageTypeLedRed, + .data.led.value = 0x00, +}; + +static const NotificationSequence led_on = { + &message_red_dim, + &message_do_not_reset, + NULL, +}; + +static const NotificationSequence led_off = { + &message_red_off, + &message_do_not_reset, + NULL, +}; + +static const NotificationSequence led_reset = { + &message_red_0, + NULL, +}; + +void set_backlight_brightness(float brightness){ + notif->settings.display_brightness = brightness; + notification_message(notif, &sequence_display_backlight_on); +} + +void handle_up(){ + if(brightness < 100){ + led = false; + notification_message(notif, &led_off); + brightness += 5; + } + set_backlight_brightness((float)(brightness / 100.f)); +} + +void handle_down(){ + if(brightness > 0){ + brightness -= 5; + if(brightness == 0){ //trigger only on the first brightness 5 -> 0 transition + led = true; + notification_message(notif, &led_on); + } + } + else if(brightness == 0){ //trigger on every down press afterwards + led = !led; + if(led){ notification_message(notif, &led_on); } + else{ notification_message(notif, &led_off); } + } + set_backlight_brightness((float)(brightness / 100.f)); +} + +static void clock_input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) { + furi_assert(event_queue); + PluginEvent event = {.type = EventTypeKey, .input = *input_event}; + furi_message_queue_put(event_queue, &event, FuriWaitForever); +} + +static void clock_render_callback(Canvas* const canvas, void* ctx) { + //canvas_clear(canvas); + //canvas_set_color(canvas, ColorBlack); + + //avoids a bug with the brightness being reverted after the backlight-off period + set_backlight_brightness((float)(brightness / 100.f)); + + ClockState* state = ctx; + if(furi_mutex_acquire(state->mutex, 200) != FuriStatusOk) { + //FURI_LOG_D(TAG, "Can't obtain mutex, requeue render"); + PluginEvent event = {.type = EventTypeTick}; + furi_message_queue_put(state->event_queue, &event, 0); + return; + } + + FuriHalRtcDateTime curr_dt; + furi_hal_rtc_get_datetime(&curr_dt); + uint32_t curr_ts = furi_hal_rtc_datetime_to_timestamp(&curr_dt); + + char time_string[TIME_LEN]; + char date_string[DATE_LEN]; + char meridian_string[MERIDIAN_LEN]; + char timer_string[20]; + + if(state->time_format == LocaleTimeFormat24h) { + snprintf( + time_string, TIME_LEN, CLOCK_TIME_FORMAT, curr_dt.hour, curr_dt.minute, curr_dt.second); + } else { + bool pm = curr_dt.hour > 12; + bool pm12 = curr_dt.hour >= 12; + snprintf( + time_string, + TIME_LEN, + CLOCK_TIME_FORMAT, + pm ? curr_dt.hour - 12 : curr_dt.hour, + curr_dt.minute, + curr_dt.second); + + snprintf( + meridian_string, + MERIDIAN_LEN, + MERIDIAN_FORMAT, + pm12 ? MERIDIAN_STRING_PM : MERIDIAN_STRING_AM); + } + + if(state->date_format == LocaleDateFormatYMD) { + snprintf( + date_string, DATE_LEN, CLOCK_ISO_DATE_FORMAT, curr_dt.year, curr_dt.month, curr_dt.day); + } else if(state->date_format == LocaleDateFormatMDY) { + snprintf( + date_string, DATE_LEN, CLOCK_RFC_DATE_FORMAT, curr_dt.month, curr_dt.day, curr_dt.year); + } else { + snprintf( + date_string, DATE_LEN, CLOCK_RFC_DATE_FORMAT, curr_dt.day, curr_dt.month, curr_dt.year); + } + + bool timer_running = state->timer_running; + uint32_t timer_start_timestamp = state->timer_start_timestamp; + uint32_t timer_stopped_seconds = state->timer_stopped_seconds; + + furi_mutex_release(state->mutex); + + canvas_set_font(canvas, FontBigNumbers); + + if(timer_start_timestamp != 0) { + int32_t elapsed_secs = timer_running ? (curr_ts - timer_start_timestamp) : + timer_stopped_seconds; + snprintf(timer_string, 20, "%.2ld:%.2ld", elapsed_secs / 60, elapsed_secs % 60); + canvas_draw_str_aligned(canvas, 64, 8, AlignCenter, AlignCenter, time_string); // DRAW TIME + canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignTop, timer_string); // DRAW TIMER + canvas_set_font(canvas, FontSecondary); + canvas_draw_str_aligned(canvas, 64, 20, AlignCenter, AlignTop, date_string); // DRAW DATE + elements_button_left(canvas, "Reset"); + } else { + canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignCenter, time_string); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str_aligned(canvas, 65, 17, AlignCenter, AlignCenter, date_string); + + if(state->time_format == LocaleTimeFormat12h) + canvas_draw_str_aligned(canvas, 64, 47, AlignCenter, AlignCenter, meridian_string); + } + if(timer_running) { + elements_button_center(canvas, "Stop"); + } else if(timer_start_timestamp != 0 && !timer_running) { + elements_button_center(canvas, "Start"); + } +} + +static void clock_state_init(ClockState* const state) { + state->time_format = locale_get_time_format(); + + state->date_format = locale_get_date_format(); + + //FURI_LOG_D(TAG, "Time format: %s", state->settings.time_format == H12 ? "12h" : "24h"); + //FURI_LOG_D(TAG, "Date format: %s", state->settings.date_format == Iso ? "ISO 8601" : "RFC 5322"); + //furi_hal_rtc_get_datetime(&state->datetime); +} + +// Runs every 1000ms by default +static void clock_tick(void* ctx) { + furi_assert(ctx); + FuriMessageQueue* event_queue = ctx; + PluginEvent event = {.type = EventTypeTick}; + // It's OK to loose this event if system overloaded + furi_message_queue_put(event_queue, &event, 0); +} + +void timer_start_stop(ClockState* plugin_state){ + // START/STOP TIMER + FuriHalRtcDateTime curr_dt; + furi_hal_rtc_get_datetime(&curr_dt); + uint32_t curr_ts = furi_hal_rtc_datetime_to_timestamp(&curr_dt); + + if(plugin_state->timer_running) { + // Update stopped seconds + plugin_state->timer_stopped_seconds = curr_ts - plugin_state->timer_start_timestamp; + } else { + if(plugin_state->timer_start_timestamp == 0) { + // Set starting timestamp if this is first time + plugin_state->timer_start_timestamp = curr_ts; + } else { + // Timer was already running, need to slightly readjust so we don't + // count the intervening time + plugin_state->timer_start_timestamp = curr_ts - plugin_state->timer_stopped_seconds; + } + } + plugin_state->timer_running = !plugin_state->timer_running; +} + +void timer_reset_seconds(ClockState* plugin_state){ + if(plugin_state->timer_start_timestamp != 0) { + // Reset seconds + plugin_state->timer_running = false; + plugin_state->timer_start_timestamp = 0; + plugin_state->timer_stopped_seconds = 0; + } +} + +int32_t clock_app(void* p) { + UNUSED(p); + ClockState* plugin_state = malloc(sizeof(ClockState)); + + plugin_state->event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent)); + if(plugin_state->event_queue == NULL) { + FURI_LOG_E(TAG, "Cannot create event queue"); + free(plugin_state); + return 255; + } + //FURI_LOG_D(TAG, "Event queue created"); + + plugin_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + if(plugin_state->mutex == NULL) { + FURI_LOG_E(TAG, "Cannot create mutex"); + furi_message_queue_free(plugin_state->event_queue); + free(plugin_state); + return 255; + } + //FURI_LOG_D(TAG, "Mutex created"); + + clock_state_init(plugin_state); + + // Set system callbacks + ViewPort* view_port = view_port_alloc(); + view_port_draw_callback_set(view_port, clock_render_callback, plugin_state); + view_port_input_callback_set(view_port, clock_input_callback, plugin_state->event_queue); + + FuriTimer* timer = furi_timer_alloc(clock_tick, FuriTimerTypePeriodic, plugin_state->event_queue); + + if(timer == NULL) { + FURI_LOG_E(TAG, "Cannot create timer"); + furi_mutex_free(plugin_state->mutex); + furi_message_queue_free(plugin_state->event_queue); + free(plugin_state); + return 255; + } + //FURI_LOG_D(TAG, "Timer created"); + + // Open GUI and register view_port + Gui* gui = furi_record_open(RECORD_GUI); + gui_add_view_port(gui, view_port, GuiLayerFullscreen); + + furi_timer_start(timer, furi_kernel_get_tick_frequency()); + //FURI_LOG_D(TAG, "Timer started"); + + notif = furi_record_open(RECORD_NOTIFICATION); + float tmpBrightness = notif->settings.display_brightness; + + notification_message(notif, &sequence_display_backlight_enforce_on); + notification_message(notif, &led_off); + + // Main loop + PluginEvent event; + for(bool processing = true; processing;) { + FuriStatus event_status = furi_message_queue_get(plugin_state->event_queue, &event, 100); + + if(event_status != FuriStatusOk) continue; + + if(furi_mutex_acquire(plugin_state->mutex, FuriWaitForever) != FuriStatusOk) continue; + // press events + if(event.type == EventTypeKey) { + if(event.input.type == InputTypeLong) { + switch(event.input.key) { + case InputKeyLeft: + // Reset seconds + timer_reset_seconds(plugin_state); + break; + case InputKeyOk: + // Toggle timer + timer_start_stop(plugin_state); + break; + case InputKeyBack: + // Exit the plugin + processing = false; + break; + default: + break; + } + } + else if(event.input.type == InputTypeShort) { + switch(event.input.key) { + case InputKeyUp: + handle_up(); + break; + case InputKeyDown: + handle_down(); + break; + default: + break; + } + } + } /*else if(event.type == EventTypeTick) { + furi_hal_rtc_get_datetime(&plugin_state->datetime); + }*/ + + view_port_update(view_port); + furi_mutex_release(plugin_state->mutex); + } + + furi_timer_free(timer); + view_port_enabled_set(view_port, false); + gui_remove_view_port(gui, view_port); + furi_record_close(RECORD_GUI); + view_port_free(view_port); + furi_message_queue_free(plugin_state->event_queue); + furi_mutex_free(plugin_state->mutex); + free(plugin_state); + + set_backlight_brightness(tmpBrightness); + notification_message(notif, &sequence_display_backlight_enforce_auto); + notification_message(notif, &led_reset); + + return 0; +} \ No newline at end of file diff --git a/clock_app.h b/clock_app.h new file mode 100644 index 00000000000..693bdfac0cd --- /dev/null +++ b/clock_app.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#define TAG "Clock" + +#define CLOCK_ISO_DATE_FORMAT "%.4d-%.2d-%.2d" +#define CLOCK_RFC_DATE_FORMAT "%.2d-%.2d-%.4d" +#define CLOCK_TIME_FORMAT "%.2d:%.2d:%.2d" + +#define MERIDIAN_FORMAT "%s" +#define MERIDIAN_STRING_AM "AM" +#define MERIDIAN_STRING_PM "PM" + +#define TIME_LEN 12 +#define DATE_LEN 14 +#define MERIDIAN_LEN 3 + +typedef enum { + EventTypeTick, + EventTypeKey, +} EventType; + +typedef struct { + EventType type; + InputEvent input; +} PluginEvent; + +typedef struct { + LocaleDateFormat date_format; + LocaleTimeFormat time_format; + FuriHalRtcDateTime datetime; + FuriMutex* mutex; + FuriMessageQueue* event_queue; + uint32_t timer_start_timestamp; + uint32_t timer_stopped_seconds; + bool timer_running; +} ClockState;