plugins: add timer/countdown plugin

rockbox-gerrit-noreply--- via rockbox-cvs <[email protected]> Thu, 16 Apr 2026 21:37:55 -0400
Newsgroups gmane.comp.systems.archos.rockbox.cvs
Message-ID <[email protected]>
commit a1040cda5db6b21651f72b1f4090edee503ab6f2
Author: seroteunine <[email protected]>
Date:   Mon Apr 13 23:25:58 2026 +0200

    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

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"
   </voice>
 </phrase>
+<phrase>
+  id: LANG_COUNTDOWN_TIMER_SET
+  desc: countdown_timer plugin - header shown on the setup screen where the user enters the countdown duration
+  user: core
+  <source>
+    *: "SET TIMER"
+  </source>
+  <dest>
+    *: "SET TIMER"
+  </dest>
+  <voice>
+    *: "Set timer"
+  </voice>
+</phrase>
+<phrase>
+  id: LANG_COUNTDOWN_TIMER_RUNNING
+  desc: countdown_timer plugin - status label shown while the countdown is active
+  user: core
+  <source>
+    *: "RUNNING"
+  </source>
+  <dest>
+    *: "RUNNING"
+  </dest>
+  <voice>
+    *: "Running"
+  </voice>
+</phrase>
+<phrase>
+  id: LANG_COUNTDOWN_TIMER_PAUSED
+  desc: countdown_timer plugin - status label shown while the countdown is paused
+  user: core
+  <source>
+    *: "PAUSED"
+  </source>
+  <dest>
+    *: "PAUSED"
+  </dest>
+  <voice>
+    *: "Paused"
+  </voice>
+</phrase>
+<phrase>
+  id: LANG_COUNTDOWN_TIMER_OVERTIME
+  desc: countdown_timer plugin - status label shown when the countdown has passed zero and is counting up
+  user: core
+  <source>
+    *: "OVERTIME"
+  </source>
+  <dest>
+    *: "OVERTIME"
+  </dest>
+  <voice>
+    *: "Overtime"
+  </voice>
+</phrase>
+<phrase>
+  id: LANG_COUNTDOWN_TIMER_FINISHED
+  desc: countdown_timer plugin - status label shown at the moment the countdown expires
+  user: core
+  <source>
+    *: "FINISHED"
+  </source>
+  <dest>
+    *: "FINISHED"
+  </dest>
+  <voice>
+    *: "Finished"
+  </voice>
+</phrase>
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"
   </voice>
 </phrase>
+<phrase>
+  id: LANG_COUNTDOWN_TIMER_SET
+  desc: countdown_timer plugin - header shown on the setup screen where the user enters the countdown duration
+  user: core
+  <source>
+    *: "SET TIMER"
+  </source>
+  <dest>
+    *: "SET TIMER"
+  </dest>
+  <voice>
+    *: "Set timer"
+  </voice>
+</phrase>
+<phrase>
+  id: LANG_COUNTDOWN_TIMER_RUNNING
+  desc: countdown_timer plugin - status label shown while the countdown is active
+  user: core
+  <source>
+    *: "RUNNING"
+  </source>
+  <dest>
+    *: "RUNNING"
+  </dest>
+  <voice>
+    *: "Running"
+  </voice>
+</phrase>
+<phrase>
+  id: LANG_COUNTDOWN_TIMER_PAUSED
+  desc: countdown_timer plugin - status label shown while the countdown is paused
+  user: core
+  <source>
+    *: "PAUSED"
+  </source>
+  <dest>
+    *: "PAUSED"
+  </dest>
+  <voice>
+    *: "Paused"
+  </voice>
+</phrase>
+<phrase>
+  id: LANG_COUNTDOWN_TIMER_OVERTIME
+  desc: countdown_timer plugin - status label shown when the countdown has passed zero and is counting up
+  user: core
+  <source>
+    *: "OVERTIME"
+  </source>
+  <dest>
+    *: "OVERTIME"
+  </dest>
+  <voice>
+    *: "Overtime"
+  </voice>
+</phrase>
+<phrase>
+  id: LANG_COUNTDOWN_TIMER_FINISHED
+  desc: countdown_timer plugin - status label shown at the moment the countdown expires
+  user: core
+  <source>
+    *: "FINISHED"
+  </source>
+  <dest>
+    *: "FINISHED"
+  </dest>
+  <voice>
+    *: "Finished"
+  </voice>
+</phrase>
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}
-- 
rockbox-cvs mailing list
[email protected]
https://lists.haxx.se/mailman/listinfo/rockbox-cvs