Tutorial : Countdown Timer

The Countdown Timer is an iRoar application that lets you set the countdown time, and alerts you when the timer has counted down. This tutorial walks through the development of the application step-by-step. The Packager tool will be used to edit the manifest, as well as to build and run the application.

1. Create the manifest

To create a new project, open the Packager tool available in the iRoar SDK on Windows.

packager.jpg

From the tool,

  • Fill in the ID and title of your application. You can also enter information such as description, publisher, version, and select application's category and language from the provided list.
  • Design the application's icon.
  • Under More Options, click on the Browse button located next to application Folder text box and select the folder you want to save your application. You may optionally change the output folder.
  • Click on Build button to save and compile the manifest.

A build error will appear. This is expected since we have not started programming! Let's continue on to the next step to create the countdown timer.

2. Writing the timer code

Create a .p file in your selected application folder and open it in a text editor. Now let's define a few constants and variables.

// Time interval in milliseconds to delay UI update
const UI_UPDATE_TIME = 3300
// Defines the icons used in the app
new displayIcons[]{} = [
"001c 0022 003a 0022 001c", // app icon
"001c 0022 002a 002a 001c", // 3 oclock icon
"001c 0022 002e 0022 001c", // 6 oclock icon
"001c 002a 002a 0022 001c", // 9 oclock icon
"0008 0004 0008 0010 0020" // tick icon
]
// Defines the different states of the application
const state_t:{
TIMER_IDLE = 0, // Timer did not start
TIMER_START, // Timer has started countdown
TIMER_PAUSE, // Timer is paused
TIMER_RESUME, // Timer is resumed
TIMER_RINGING // Ringing, countdown has stopped
}
// Indicates current state of the countdown timer
new timer_state = TIMER_IDLE
// Timer for updating icon on device
new icon_timer = -1
// Indicates the buffer for storing custom icons
new icon_buffer[5] = 0

Next, create the initialization and cleanup code.

// Called by framework when application starts, typically after a power state change
@app_start(app_id)
{
init()
}
// Called by framework when application ends
{
cleanup()
}
// Initializes the application and user interface
init()
{
// Light up the LEDs for play, next and previous buttons
// Overrides the system default handling for the Roar and MicBeam buttons
// Display the application icon
led_custom_icon_set(displayIcons[0], ANIM_NONE)
}
// Cleanup resources when application exits or when focus is lost
cleanup()
{
timer_cleanup(icon_timer)
}
// Remove a timer object by ID
timer_cleanup(timerId)
{
if (timerId != -1)
{
timer_delete(timerId)
}
timerId = -1
}
// Displays a custom icon
led_custom_icon_set(icon[], anim_type)
{
new len = sizeof icon_buffer
new offset = 0
// draw_custom_icon is asynchronous
offset = draw_custom_icon(icon_buffer, len, 0, icon)
led_matrix_set(icon_buffer, offset, 1, animation_t:anim_type, 0, DIR_DEFAULT)
}
// Update icon display in 3s
led_icon_update()
{
timer_cleanup(icon_timer)
icon_timer = timer_create(UI_UPDATE_TIME, 1)
}

Whenever the user switches to this application on the iRoar device, the application will receive the focus event. In this event, we will setup the user interface. Also in the code below, we will implement the user's interaction with the Countdown Timer by handling the button events.

// Count down time duration in minutes for press and hold events
const LONG_PRESS_TIME = 30
// Indicates whether app is in focus
new app_focused = 0
// Called by framework when an event occurs
@event_observer(event_id, param1, param2)
{
switch (event_id)
{
{
on_focus(param1)
}
{
on_timer(param1, param2)
}
{
switch (param2)
{
{
on_button_hold(param1)
}
{
on_button_tap(param1)
}
}
}
}
}
// Handles the focus event
on_focus(param1)
{
app_focused = param1
if (param1 == 1)
{
init() //app is in focus
}
// on lost focus, allow the timer to continue counting down
}
// Handles the press and hold event
on_button_hold(param1)
{
switch (param1)
{
{
decrease_time(LONG_PRESS_TIME)
}
{
increase_time(LONG_PRESS_TIME)
}
{
goto_state(TIMER_IDLE)
led_custom_icon_set(displayIcons[4], ANIM_NONE)
led_icon_update()
}
}
}
// Handles the button tap event
on_button_tap(param1)
{
switch (param1)
{
{
toggle_state()
}
{
decrease_time(1)
}
{
increase_time(1)
}
}
}
// Handles the event when play button is pressed
toggle_state()
{
switch (state_t:timer_state)
{
case TIMER_START:
goto_state(TIMER_PAUSE)
case TIMER_PAUSE:
goto_state(TIMER_RESUME)
case TIMER_RESUME:
goto_state(TIMER_PAUSE)
case TIMER_IDLE:
goto_state(TIMER_START)
case TIMER_RINGING:
goto_state(TIMER_IDLE)
}
}

At this point, we have handled the user inputs for the Countdown Timer. When the user taps on the PLAY button, the timer will change to the next state. When the user taps or press and hold the PREV or NEXT buttons, the countdown time will be adjusted. When the user press and hold the PLAY button, the timer will be reset.

Next, let's implement the logic for the timer to work.

// Maximum number of minutes to countdown from
const MAX_COUNTDOWN_TIME = 9999
// Minimum number of seconds to countdown from
const MIN_COUNTDOWN_TIME = 60
// Time interval in milliseconds to update countdown time
const COUNTDOWN_UPDATE_TIME = 1000
// Countdown timer
new countdown_timer = -1
// Indicates number of seconds to countdown from
new countdown_time = MIN_COUNTDOWN_TIME
// Indicates countdown time that was last used
new last_set_time = MIN_COUNTDOWN_TIME
// Indicates elapsed time in seconds after countdown has started
new elapsed_sec = 0
// Indicates if ringing has been delayed by other events
new delayed_ring = 0
// Handles the timer events
on_timer(param1, param2)
{
if (param1 == countdown_timer && (param2 == COUNTDOWN_UPDATE_TIME))
{
if (state_t:timer_state == TIMER_START ||
state_t:timer_state == TIMER_RESUME )
{
elapsed_sec--
if (elapsed_sec <= 0)
{
stop_timer()
on_alarm_ring()
}
else
{
led_custom_icon_set(displayIcons[elapsed_sec % 4], ANIM_NONE)
}
}
}
if ((param1 == icon_timer) && (param2 == UI_UPDATE_TIME))
{
timer_cleanup(icon_timer)
if (state_t:timer_state == TIMER_PAUSE)
{
//skip count down. Show pause icon
led_icon_set(ICON_PAUSE)
}
else if (state_t:timer_state == TIMER_IDLE)
{
led_custom_icon_set(displayIcons[0], ANIM_NONE)
}
else
{
led_custom_icon_set(displayIcons[elapsed_sec % 4], ANIM_NONE)
}
}
}
// Play alarm ringtone
on_alarm_ring()
{
// unlock speaker and grab focus
system_lock(false)
if (app_focused == 0)
{
delayed_ring = 1
}
else
{
goto_state(TIMER_RINGING)
}
}
// Handles the state changes
goto_state(newstate)
{
switch (newstate)
{
case TIMER_START:
start_timer()
case TIMER_PAUSE:
led_icon_set(ICON_PAUSE)
case TIMER_IDLE:
reset()
case TIMER_RINGING:
ring_alarm()
// TIMER_RESUME has no specific action
}
timer_state = newstate
}
// Start a timer to countdown
start_timer()
{
countdown_timer = timer_create(COUNTDOWN_UPDATE_TIME, -1)
last_set_time = countdown_time
elapsed_sec = countdown_time
}
// Stops the timer
stop_timer()
{
timer_cleanup(countdown_timer)
led_custom_icon_set(displayIcons[0], ANIM_NONE)
}
// Increase the timer time by delta minutes
increase_time(delta)
{
if (countdown_time + delta * 60 <= MAX_COUNTDOWN_TIME)
countdown_time += delta * 60
elapsed_sec += delta * 60
if (elapsed_sec > MAX_COUNTDOWN_TIME) elapsed_sec = MAX_COUNTDOWN_TIME
last_set_time = countdown_time
led_number_set(countdown_time / 60)
led_icon_update()
}
// Decrease the timer time by delta minutes
decrease_time(delta)
{
if (countdown_time - delta * 60 >= 1)
countdown_time -= delta * 60
elapsed_sec -= delta * 60
if (elapsed_sec <= 0) elapsed_sec = 1
last_set_time = countdown_time
led_number_set(countdown_time / 60)
led_icon_update()
}
// Stops and resets the countdown time
reset()
{
countdown_time = last_set_time
elapsed_sec = countdown_time
stop_timer()
}
// Rings the alarm
ring_alarm()
{
// to add a sound
led_custom_icon_set(displayIcons[0], ANIM_BLINK)
led_icon_update()
}
// Displays scrolling number
led_number_set(number)
{
new displaytext{8} = 0
new numbertext{8} = 0
valstr(numbertext, number)
//add padding
new anim_type = ANIM_NONE
if (number > 9)
{
strcat(displaytext, numbertext)
strcat(displaytext, " ")
anim_type = ANIM_SCROLL
}
else
{
strcat(displaytext, numbertext)
}
new buffer[16] = 0
new len = sizeof buffer
new offset = 0
offset = draw_text(buffer, len, 0, displaytext)
led_matrix_set(buffer, offset, 1, animation_t:anim_type, 0, DIR_RIGHT_TO_LEFT)
}
// Displays a predefined icon
led_icon_set(icon_t:icon)
{
new len = sizeof icon_buffer
new offset = 0
offset = draw_icon(icon_buffer, len, offset, icon)
led_matrix_set(icon_buffer, offset, 1, ANIM_NONE, 0, DIR_DEFAULT)
}

Awesome! The countdown timer is almost done, let's move on to add an alarm sound.

3. Add Sounds

The iRoar device supports MP3, MP4 and WAV audio formats. You may use any sound files of this format within your application. However, do keep the size of the sound files small for easier and faster deployment.

To add an alarm ringtone for the timer, create a sounds\ folder in your application Folder. Place your favorite alarm ringtone in this folder. Add the code below to store the full path to the file.

// Indicates the relative path for sounds
new sound_pathname{MAX_STRING_LENGTH} = 0
// Defines the path to load the sound file
// You may rename timer.mp3 to the filename of your selected ringtone
new sound_timer{32} = "/sounds/timer.mp3"

Within your init() function, get the absolute path for the application and save it.

// Store the path to the sounds
app_get_sdcard_path(sound_pathname)
strcat(sound_pathname, sound_timer)

Within the ring_alarm() function, play the sound file.

new playbackcontext
audio_play_this(sound_pathname, playbackcontext)

Next, add the play started event handler in the event_observer function.

{
on_play(param1)
}

Handle the play started event to reset the countdown time when the alarm ringtone has completed playback.

// Handles the play event
on_play(param1)
{
// playback has stopped
if (param1 == 0 && state_t:timer_state == TIMER_RINGING )
{
goto_state(TIMER_IDLE)
}
}

4. Build, Run and Debug

Building and running the Countdown Timer application is easy. Ensure that your iRoar is powered on, connected to your PC with a USB cable, and a microSD card is inserted. With the Packager tool, open this project, and click on the RUN button to build and transfer the application to the iRoar. If you include one or more sound files that is above 1MB, the transfer may take some time. Please wait for the transfer to complete. You may optionally run the tools\irdb.exe console application to view debug outputs.

On the iRoar device, switch to the Countdown Timer application that you have just created. Press on PLAY button to start countdown. After 1min when time is up, you will hear the alarm ring.

5. Add UI on the mobile device

We have just completed a basic Countdown Timer application that runs on the iRoar device. For a better user experience, you can create a HTML page to include more details about your application, as well as usage instructions.

Create a html\ folder in your application Folder, then create a instructions.html file. Within the HTML, you may link to the addon.js script included in the iRoar SDK. To create a standardized style for your HTML, use loadDefaultCSS(). The iRoar Dashboard application includes this addon.js and its dependencies, so you do not have to include it within your html\ folder.

<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0;" />
<title>Countdown Timer</title>
<script type="text/javascript" src="../../resources/v1_0/js/addon.js"></script>
<script type="text/javascript">
loadDefaultCSS();
</script>
</head>
<body>
<div class="main">
<div class="addonDesc">A general purpose countdown timer for use in the gym, kitchen, school and many more. You can set the time duration in 1 min intervals.</div>
<div class="table">
<div class="row">
<div class="cell prevIcon"></div>
<div class="cell playIcon"></div>
<div class="cell nextIcon"></div>
</div>
<div class="row">
<div class="cell shortptrIcon">
<div class="shortptrDesc">
<div class="actionTitle">Tap</div>
<div class="actionDesc">decrease 1min</div>
<div class="actionTitle">Press and Hold</div>
<div class="actionDesc">decrease 30min</div>
</div>
</div>
<div class="cell midptrIcon">
<div class="midptrDesc">
<div class="actionTitle">Tap</div>
<div class="actionDesc">start / pause</div>
<div class="actionTitle">Press and Hold</div>
<div class="actionDesc">reset</div>
</div>
</div>
<div class="cell longptrIcon">
<div class="longptrDesc">
<div class="actionTitle">Tap</div>
<div class="actionDesc">increase 1min</div>
<div class="actionTitle">Press and Hold</div>
<div class="actionDesc">increase 30min</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

To enable the user to control the Countdown Timer from your mobile device, you may simply add a settings.html. In this settings page, you could add status displays, custom css styles and widget controls. The user can view your instructions and settings from the iRoar Dashboard application. For the complete source code, you can refer to the apps\timer\html in the iRoar SDK.

timer.jpg
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0;" />
<title>Countdown Timer</title>
<script type="text/javascript" src="../../resources/v1_0/js/addon.js"></script>
<script type="text/javascript" src="timer.js"></script>
<script type="text/javascript" src="timerctrl.js"></script>
<link rel="stylesheet" type="text/css" href="timer.css" />
</head>
<body>
<div>
<div class="content">
<div id="timerloaded">
<div id="timertop">
<button class="timerbutton" id="resetbutton">
<span id="resetText">RESET</span>
</button>
<div id="timertime">
<button class="timerbutton" id="timeinput">
<span class="large light" id="timerdisplay"></span>
</button>
</div>
</div>
<div id="timerbottom">
<button class="timerbutton" id="timercontrol">
<span class="light" id="timercontroldisplay">START</span>
</button>
</div>
</div>
<div id="timermain">
<div id="fadeIn">
<div id="timerloading"></div>
</div>
</div>
<!--TODO insert number input control overlay -->
</div>
</div>
</body>
</html>

6. Communicating between devices

The iRoar SDK provides APIs for you to send Bluetooth data packets from your mobile device to the iRoar device, and vice versa. Any data of string type can be passed between the devices. For example, you can create an interface for the user to set the countdown time or start the countdown from the mobile device.

To begin,

  • include a link to the addon.js script in the settings.html.
  • create a javascript file and create the DataTransfer object.
  • handle the response and error events to respond to data sent from the iRoar device.

The code below demonstrates how to retrieve data from the application running on the iRoar device.

var dataTransfer;
var lastsetTime;
var deviceCommand = {
state: "state",
time: "time",
lastsettime: "default",
reset: "reset",
error: "error"
}
// Load default CSS styles and initialize the DataTransfer object
function initialize() {
loadDefaultCSS();
if (dataTransfer == null) {
dataTransfer = new DataTransfer();
dataTransfer.addEventListener(DataEvent.response, onData);
dataTransfer.addEventListener(DataEvent.error, onError);
}
// query for the current state of the countdown timer
getDeviceSettings();
}
// On receiving data from the device
function onData(e) {
// if return type or data is not of string data type
if (e.detail.type != DataType.string || typeof (e.detail.data) != 'string')
return;
// parse string data
var commands = e.detail.data.split("&");
for (var i = 0; i < commands.length; i++) {
var cmd = commands[i];
var namevalue = cmd.split("=");
if (namevalue[0].toLowerCase() == deviceCommand.time) {
time = namevalue[1];
updateUITime();
}
else if (namevalue[0].toLowerCase() == deviceCommand.state) {
state = namevalue[1];
updateUIState();
}
else if (namevalue[0].toLowerCase() == deviceCommand.default) {
time = namevalue[1];
lastsetTime = time;
}
else if (namevalue[0].toLowerCase() == deviceCommand.error) {
var error = namevalue[1].toString();
updateUITime();
}
}
}
// On receiving error from the device
function onError(e) {
state = timerState.idle;
refresh();
}
// Sends command to iRoar device to get all current settings
function getDeviceSettings() {
dataTransfer.postAddonData(transid++, "get " + deviceCommand.state);
dataTransfer.postAddonData(transid++, "get " + deviceCommand.time);
dataTransfer.postAddonData(transid++, "get " + deviceCommand.lastsettime);
}
// Updates time on UI
function updateUITime() {
...
}
// Updates current state on UI
function updateUIState() {
..
}
// Refreshes the UI
function refresh() {
updateUIState();
updateUITime();
}
// Starts the script
initialize();

You can define more commands to send to the device upon user input, such as time has been changed, timer state has started, or timer has been reset. The code below demonstrates how to send a state change or reset event to the application running on iRoar device.

Insert this code in the initialize() function

this.addEventListener("load", onLoad);

Next, add handlers to the Start and Reset buttons.

// Identifies the data packet sent.
var transid = 0;
// Handles document load, add event handlers
function onLoad() {
var element = document.getElementById('timeinput');
if (element != null) {
document.getElementById('timercontrol').onclick = onStateChanged;
document.getElementById('resetbutton').onclick = onReset;
}
refresh();
}
// Handles event when user clicks on reset
function onReset() {
updateSettingsOnDevice(deviceCommand.reset);
}
// Handles event when user clicks on the start button
function onStateChanged() {
var newstate = null;
if (state == timerState.idle)
newstate = timerState.start;
else if (state == timerState.start)
newstate = timerState.pause;
else if (state == timerState.pause)
newstate = timerState.resume;
else if (state == timerState.resume)
newstate = timerState.pause;
else if (state == timerState.ringing) {
newstate = timerState.idle;
time = lastsetTime;
updateUITime()
}
if (newstate != null) {
state = newstate;
updateSettingsOnDevice(deviceCommand.state);
updateUIState();
}
}
// Sends command to device to update settings
function updateSettingsOnDevice(settings) {
var cmd = "set " + settings;
if (settings == deviceCommand.state) {
cmd += "=" + state;
}
else if (settings == deviceCommand.reset) {
// no additional param
}
else {
cmd += "-1";
}
dataTransfer.postAddonData(transid++, cmd);
}

From the application running on the iRoar device, handle and respond to the commands sent from the mobile device.

In your .p file, insert this case in the event_observer function,

{
on_data(param1, param2)
}

Then, add the code below to interpret the data received.

// Max size of buffer in bytes to contain the data from mobile device
const MAX_BUF_SIZE = 256
// Defines commands that will be sent to mobile device
new trans_data[]{} = [
"time=",
"state=",
"default=",
"reset=",
"error -1"
]
// Indicates the packet number for each data sent to connected device
new packet = 0
// Contains data received
new data_buffer{MAX_BUF_SIZE} = 0
// Contains data to be sent
new send_buffer{MAX_BUF_SIZE} = 0
// Handles data received from mobile device
on_data(param1, param2)
{
new id = param1
new size = param2
new type = 0
new transid = 0
new result = get_data(id, data_buffer, size, transid, type)
if (bool:result == true && size > 0)
{
result = execute_command(data_buffer, trans_data_t:type)
}
else
{
printf("Error retrieving data buffer")
}
}
// Respond to the command sent from mobile device
execute_command(data[], trans_data_t:type)
{
if (type != STRING_DATA)
{
return false;
}
if (strcmp(data, "get state", true, strlen("get state")) == 0)
{
return send_state()
}
else if (strcmp(data, "set state", true, strlen("set state")) == 0)
{
return request_set_state(data)
}
else if (strcmp(data, "get default", true, strlen("get default")) == 0)
{
return send_default_time()
}
else if (strcmp(data, "set reset", true, strlen("set reset")) == 0)
{
goto_state(TIMER_IDLE)
return true
}
return false
}
// Sends current state to current connnected device
send_state()
{
valstr(send_buffer, state_t:timer_state)
strcopy(data_buffer, trans_data[1])
strcat(data_buffer, send_buffer, MAX_BUF_SIZE)
return send_data(data_buffer, strlen(data_buffer)+1, packet++, STRING_DATA)
}
// Handles the request to change state
request_set_state(buf[])
{
new temp = 0
new i = strfind(buf, "=", true, 0)
if (i == -1) return false
new tbuf{4} = 0
strmid(tbuf, buf, i+1, i+2, sizeof tbuf);
temp = strval(tbuf, 0)
goto_state(temp)
return true
}
// Sends default time to current connnected device
send_default_time()
{
valstr(send_buffer, last_set_time)
strcopy(data_buffer, trans_data[2])
strcat(data_buffer, send_buffer, MAX_BUF_SIZE)
return send_data(data_buffer, strlen(data_buffer)+1, packet++, STRING_DATA)
}

7. Other Notes

The full source code for the Countdown Timer is available in apps\timer folder of the iRoar SDK. You may refer to other user experience considerations such as

  • handling alarm ring during a phone call
  • handling alarm ring when the app is not in focus
  • enabling the user to enter time from the mobile device