From a1040cda5db6b21651f72b1f4090edee503ab6f2 Mon Sep 17 00:00:00 2001 From: seroteunine Date: Mon, 13 Apr 2026 23:25:58 +0200 Subject: [PATCH] plugins: add timer/countdown plugin -New countdown timer plugin with pause, overtime support -Add full name to credits and manual entry -Make status strings translatable Change-Id: I1437b2e5ac5ede292bdab8d36e58b81326ea2ba3 --- apps/lang/english-us.lang | 70 +++++++++ apps/lang/english.lang | 70 +++++++++ apps/plugins/CATEGORIES | 1 + apps/plugins/SOURCES | 1 + apps/plugins/countdown_timer.c | 242 +++++++++++++++++++++++++++++ docs/CREDITS | 1 + manual/plugins/countdown_timer.tex | 51 ++++++ manual/plugins/main.tex | 2 + 8 files changed, 438 insertions(+) create mode 100644 apps/plugins/countdown_timer.c create mode 100644 manual/plugins/countdown_timer.tex diff --git a/apps/lang/english-us.lang b/apps/lang/english-us.lang index db078e77af..d1f8c108ce 100644 --- a/apps/lang/english-us.lang +++ b/apps/lang/english-us.lang @@ -16984,3 +16984,73 @@ *: "U S B" + + id: LANG_COUNTDOWN_TIMER_SET + desc: countdown_timer plugin - header shown on the setup screen where the user enters the countdown duration + user: core + + *: "SET TIMER" + + + *: "SET TIMER" + + + *: "Set timer" + + + + id: LANG_COUNTDOWN_TIMER_RUNNING + desc: countdown_timer plugin - status label shown while the countdown is active + user: core + + *: "RUNNING" + + + *: "RUNNING" + + + *: "Running" + + + + id: LANG_COUNTDOWN_TIMER_PAUSED + desc: countdown_timer plugin - status label shown while the countdown is paused + user: core + + *: "PAUSED" + + + *: "PAUSED" + + + *: "Paused" + + + + id: LANG_COUNTDOWN_TIMER_OVERTIME + desc: countdown_timer plugin - status label shown when the countdown has passed zero and is counting up + user: core + + *: "OVERTIME" + + + *: "OVERTIME" + + + *: "Overtime" + + + + id: LANG_COUNTDOWN_TIMER_FINISHED + desc: countdown_timer plugin - status label shown at the moment the countdown expires + user: core + + *: "FINISHED" + + + *: "FINISHED" + + + *: "Finished" + + diff --git a/apps/lang/english.lang b/apps/lang/english.lang index 685dffb133..45ed8938fc 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -17099,3 +17099,73 @@ *: "Swap Left & Right" + + id: LANG_COUNTDOWN_TIMER_SET + desc: countdown_timer plugin - header shown on the setup screen where the user enters the countdown duration + user: core + + *: "SET TIMER" + + + *: "SET TIMER" + + + *: "Set timer" + + + + id: LANG_COUNTDOWN_TIMER_RUNNING + desc: countdown_timer plugin - status label shown while the countdown is active + user: core + + *: "RUNNING" + + + *: "RUNNING" + + + *: "Running" + + + + id: LANG_COUNTDOWN_TIMER_PAUSED + desc: countdown_timer plugin - status label shown while the countdown is paused + user: core + + *: "PAUSED" + + + *: "PAUSED" + + + *: "Paused" + + + + id: LANG_COUNTDOWN_TIMER_OVERTIME + desc: countdown_timer plugin - status label shown when the countdown has passed zero and is counting up + user: core + + *: "OVERTIME" + + + *: "OVERTIME" + + + *: "Overtime" + + + + id: LANG_COUNTDOWN_TIMER_FINISHED + desc: countdown_timer plugin - status label shown at the moment the countdown expires + user: core + + *: "FINISHED" + + + *: "FINISHED" + + + *: "Finished" + + diff --git a/apps/plugins/CATEGORIES b/apps/plugins/CATEGORIES index c9b1781a15..bf646a5bf9 100644 --- a/apps/plugins/CATEGORIES +++ b/apps/plugins/CATEGORIES @@ -21,6 +21,7 @@ chopper,games clix,games clock,apps codebuster,games +countdown_timer,apps credits,viewers cube,demos cue_playlist,viewers diff --git a/apps/plugins/SOURCES b/apps/plugins/SOURCES index 3a57e3f9e5..797601e4dd 100644 --- a/apps/plugins/SOURCES +++ b/apps/plugins/SOURCES @@ -7,6 +7,7 @@ db_folder_select.c tagcache/tagcache.c #endif chessclock.c +countdown_timer.c credits.c cube.c cue_playlist.c diff --git a/apps/plugins/countdown_timer.c b/apps/plugins/countdown_timer.c new file mode 100644 index 0000000000..b520047bf3 --- /dev/null +++ b/apps/plugins/countdown_timer.c @@ -0,0 +1,242 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2026 Teun van Dalen + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "plugin.h" +#include "lib/pluginlib_actions.h" +#include "lib/pluginlib_exit.h" + +const struct button_mapping *plugin_contexts[] = { pla_main_ctx }; + +#define MAX_MINUTES 99 +#define MAX_SECONDS 59 + +typedef enum { + STATE_SETUP, + STATE_RUNNING, + STATE_PAUSED, + STATE_FINISHED, +} timer_state_t; + +typedef struct { + int set_minutes; + int set_seconds; + int current_field; + long remaining_ticks; + long last_tick; + timer_state_t state; +} timer_ctx_t; + +static inline int get_button(void) +{ + return pluginlib_getaction(HZ / 10, plugin_contexts, + ARRAYLEN(plugin_contexts)); +} + +static void draw(const timer_ctx_t *ctx) +{ + char time_str[12]; + const char *status_str; + int disp_min, disp_sec; + int w, h, x, y; + + rb->lcd_clear_display(); + + if (ctx->state == STATE_SETUP) { + disp_min = ctx->set_minutes; + disp_sec = ctx->set_seconds; + if (ctx->current_field == 0) + rb->snprintf(time_str, sizeof(time_str), "[%02d]:%02d", + disp_min, disp_sec); + else + rb->snprintf(time_str, sizeof(time_str), "%02d:[%02d]", + disp_min, disp_sec); + status_str = rb->str(LANG_COUNTDOWN_TIMER_SET); + } else if (ctx->state == STATE_FINISHED) { + rb->snprintf(time_str, sizeof(time_str), "00:00"); + status_str = rb->str(LANG_COUNTDOWN_TIMER_FINISHED); + } else { + if (ctx->remaining_ticks >= 0) { + int remaining_secs = (ctx->remaining_ticks + HZ - 1) / HZ; + disp_min = remaining_secs / 60; + disp_sec = remaining_secs % 60; + rb->snprintf(time_str, sizeof(time_str), "%02d:%02d", + disp_min, disp_sec); + } else { + int over_secs = ((-ctx->remaining_ticks) + HZ - 1) / HZ; + disp_min = over_secs / 60; + disp_sec = over_secs % 60; + rb->snprintf(time_str, sizeof(time_str), "-%02d:%02d", + disp_min, disp_sec); + } + + if (ctx->state == STATE_RUNNING && ctx->remaining_ticks < 0) + status_str = rb->str(LANG_COUNTDOWN_TIMER_OVERTIME); + else if (ctx->state == STATE_RUNNING) + status_str = rb->str(LANG_COUNTDOWN_TIMER_RUNNING); + else + status_str = rb->str(LANG_COUNTDOWN_TIMER_PAUSED); + } + + /* Draw status string above center */ + rb->lcd_getstringsize(status_str, &w, &h); + x = (LCD_WIDTH - w) / 2; + y = (LCD_HEIGHT / 2) - h - 2; + rb->lcd_putsxy(x, y, status_str); + + /* Draw time string at center */ + rb->lcd_getstringsize(time_str, &w, &h); + x = (LCD_WIDTH - w) / 2; + y = LCD_HEIGHT / 2; + rb->lcd_putsxy(x, y, time_str); + + rb->lcd_update(); +} + +enum plugin_status plugin_start(const void *parameter) +{ + timer_ctx_t ctx = { + .set_minutes = 10, + .set_seconds = 0, + .current_field = 0, + .remaining_ticks = 60 * HZ, + .last_tick = 0, + .state = STATE_SETUP, + }; + bool overtime_alerted = false; + int button; + + (void)parameter; + + while (true) { + if (ctx.state == STATE_RUNNING) { + long now = *rb->current_tick; + ctx.remaining_ticks -= now - ctx.last_tick; + ctx.last_tick = now; + + if (ctx.remaining_ticks < 0 && !overtime_alerted) { + overtime_alerted = true; + ctx.state = STATE_FINISHED; + draw(&ctx); + rb->audio_pause(); +#ifdef HAVE_BACKLIGHT + rb->backlight_on(); +#endif + rb->beep_play(1000, 200, 1000); + rb->sleep(HZ / 4); + rb->beep_play(1000, 200, 1000); + rb->sleep(HZ / 4); + rb->beep_play(1000, 400, 1000); + rb->sleep(HZ + HZ / 2); + rb->audio_resume(); + ctx.state = STATE_RUNNING; + /* Reset last_tick so the beep/sleep time is not counted + * against remaining_ticks on the next iteration. */ + ctx.last_tick = *rb->current_tick; + } + } + + draw(&ctx); + button = get_button(); + + switch (ctx.state) { + case STATE_SETUP: { + bool time_changed = false; + switch (button) { +#ifdef HAVE_SCROLLWHEEL + case PLA_SCROLL_FWD: + case PLA_SCROLL_FWD_REPEAT: +#endif + case PLA_UP: + case PLA_UP_REPEAT: + if (ctx.current_field == 0) + ctx.set_minutes = (ctx.set_minutes + 1) % (MAX_MINUTES + 1); + else + ctx.set_seconds = (ctx.set_seconds + 1) % (MAX_SECONDS + 1); + time_changed = true; + break; +#ifdef HAVE_SCROLLWHEEL + case PLA_SCROLL_BACK: + case PLA_SCROLL_BACK_REPEAT: +#endif + case PLA_DOWN: + case PLA_DOWN_REPEAT: + if (ctx.current_field == 0) + ctx.set_minutes = (ctx.set_minutes + MAX_MINUTES) % (MAX_MINUTES + 1); + else + ctx.set_seconds = (ctx.set_seconds + MAX_SECONDS) % (MAX_SECONDS + 1); + time_changed = true; + break; + case PLA_LEFT: + case PLA_LEFT_REPEAT: + case PLA_RIGHT: + case PLA_RIGHT_REPEAT: + ctx.current_field = (ctx.current_field + 1) % 2; + break; + case PLA_SELECT: + if (ctx.set_minutes > 0 || ctx.set_seconds > 0) { + ctx.remaining_ticks = (ctx.set_minutes * 60 + ctx.set_seconds) * HZ; + ctx.last_tick = *rb->current_tick; + ctx.state = STATE_RUNNING; + } + break; + default: + exit_on_usb(button); + break; + } + if (time_changed) + ctx.remaining_ticks = (ctx.set_minutes * 60 + ctx.set_seconds) * HZ; + break; + } + + case STATE_RUNNING: + switch (button) { + case PLA_SELECT: + ctx.state = STATE_PAUSED; + break; + default: + exit_on_usb(button); + break; + } + break; + + case STATE_FINISHED: + exit_on_usb(button); + break; + + case STATE_PAUSED: + switch (button) { + case PLA_SELECT: + ctx.last_tick = *rb->current_tick; + ctx.state = STATE_RUNNING; + break; + case PLA_UP: + case PLA_UP_REPEAT: + case PLA_CANCEL: + case PLA_EXIT: + return PLUGIN_OK; + default: + exit_on_usb(button); + break; + } + break; + } + } +} diff --git a/docs/CREDITS b/docs/CREDITS index 06045db966..f1affc6eb8 100644 --- a/docs/CREDITS +++ b/docs/CREDITS @@ -760,6 +760,7 @@ Yegor Chernyshov Javier Gutiérrez Gertrúdix Sergey Puskov Eivind Ødegård +Teun van Dalen The libmad team The wavpack team diff --git a/manual/plugins/countdown_timer.tex b/manual/plugins/countdown_timer.tex new file mode 100644 index 0000000000..c022203a92 --- /dev/null +++ b/manual/plugins/countdown_timer.tex @@ -0,0 +1,51 @@ +% $Id$ % +\subsection{Countdown_timer} + +A countdown timer. Set the desired duration, start the countdown, and the +\dap{} will alert you with a beep sequence when time is up. The timer +defaults to 10 minutes on launch. + +\subsubsection{Setting the timer} + +When the plugin starts, the display shows the time to count down from with +the active field highlighted in brackets (e.g.\ \texttt{[10]:00}). + +\begin{btnmap} + \opt{scrollwheel}{\PluginScrollFwd{} / \PluginScrollBack{} or } + \PluginUp{} / \PluginDown + \opt{HAVEREMOTEKEYMAP}{& \PluginRCUp{} / \PluginRCDown} + & Increase / decrease the selected field \\ + + \PluginLeft{} / \PluginRight + \opt{HAVEREMOTEKEYMAP}{& \PluginRCLeft{} / \PluginRCRight} + & Switch between the minutes and seconds fields \\ + + \PluginSelect + \opt{HAVEREMOTEKEYMAP}{& \PluginRCSelect} + & Start the countdown \\ +\end{btnmap} + +Minutes can be set from 0 to 99 and seconds from 0 to 59. The timer cannot +be started if both fields are zero. Values wrap around when incremented or +decremented past their limits. + +\subsubsection{Running the timer} + +Once started, the remaining time counts down in \texttt{MM:SS} format. + +\begin{btnmap} + \PluginSelect + \opt{HAVEREMOTEKEYMAP}{& \PluginRCSelect} + & Pause / resume the timer \\ + + \nopt{IPOD_4G_PAD,IPOD_3G_PAD}{\PluginCancel} + \opt{IPOD_4G_PAD,IPOD_3G_PAD}{\ButtonMenu} + \opt{HAVEREMOTEKEYMAP}{& \PluginRCCancel} + & Quit (only available while paused) \\ +\end{btnmap} + +When the countdown reaches zero the \dap{} pauses playback, turns on the +backlight, and plays three beeps. After the alert the timer continues +running into overtime, displaying the elapsed overtime as +\texttt{-MM:SS}. Playback resumes automatically after the beep sequence. +The timer can be quit by pausing it and pressing the quit button. diff --git a/manual/plugins/main.tex b/manual/plugins/main.tex index d6601b8a5b..8ca4b86e6b 100644 --- a/manual/plugins/main.tex +++ b/manual/plugins/main.tex @@ -297,3 +297,5 @@ option from the \setting{Context Menu} (see \reference{ref:Contextmenu}).} \input{plugins/stopwatch.tex} \input{plugins/text_editor.tex} + +\input{plugins/countdown_timer.tex}