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
This commit is contained in:
seroteunine 2026-04-13 23:25:58 +02:00 committed by Solomon Peachy
parent cc7418dd8b
commit a1040cda5d
8 changed files with 438 additions and 0 deletions

View file

@ -16984,3 +16984,73 @@
*: "U S B" *: "U S B"
</voice> </voice>
</phrase> </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>

View file

@ -17099,3 +17099,73 @@
*: "Swap Left & Right" *: "Swap Left & Right"
</voice> </voice>
</phrase> </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>

View file

@ -21,6 +21,7 @@ chopper,games
clix,games clix,games
clock,apps clock,apps
codebuster,games codebuster,games
countdown_timer,apps
credits,viewers credits,viewers
cube,demos cube,demos
cue_playlist,viewers cue_playlist,viewers

View file

@ -7,6 +7,7 @@ db_folder_select.c
tagcache/tagcache.c tagcache/tagcache.c
#endif #endif
chessclock.c chessclock.c
countdown_timer.c
credits.c credits.c
cube.c cube.c
cue_playlist.c cue_playlist.c

View file

@ -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;
}
}
}

View file

@ -760,6 +760,7 @@ Yegor Chernyshov
Javier Gutiérrez Gertrúdix Javier Gutiérrez Gertrúdix
Sergey Puskov Sergey Puskov
Eivind Ødegård Eivind Ødegård
Teun van Dalen
The libmad team The libmad team
The wavpack team The wavpack team

View file

@ -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.

View file

@ -297,3 +297,5 @@ option from the \setting{Context Menu} (see \reference{ref:Contextmenu}).}
\input{plugins/stopwatch.tex} \input{plugins/stopwatch.tex}
\input{plugins/text_editor.tex} \input{plugins/text_editor.tex}
\input{plugins/countdown_timer.tex}