Alarm - Alarm Clock

Overview

The Alarm app is a Utility-type application designed for wearable devices. It allows the user to create, edit, and delete alarms with configurable time, repeat schedule, and alert effect. When an alarm fires, the app wakes the screen, plays the configured buzzer or vibration pattern, and presents a ringing screen from which the user can stop or snooze the alarm.

The app does not use any sensors. All logic is time-based: the service runs continuously in the background, checking the current local time against the alarm list once per minute and triggering the corresponding effect when a match is found.

Key features include:

  • Up to 20 alarms stored in persistent JSON storage

  • Per-alarm configuration: time (HH:MM), repeat schedule, and alert effect

  • Repeat options: once, every day, weekdays, weekends, or a specific day of the week

  • Alert effects: buzzer only, vibration only, or buzzer + vibration

  • Snooze: re-triggers after 5 minutes, up to 5 times

  • The app can be launched on-demand (user opens from menu) or automatically when an alarm fires

  • If no active alarms exist and the GUI never starts within 5 seconds, the service exits without launching the display

Architecture

The app is structured as two independent components: a service that handles alarm persistence and time-based triggering, and a GUI that manages the alarm list and user interaction. Communication between them occurs through a message-based system using the UNA SDK kernel infrastructure.

High-Level Components

  1. Service Layer: Alarm persistence, time-based checking, effect playback

  2. GUI Layer: TouchGFX-based user interface, alarm CRUD operations

  3. SDK Integration: Kernel, file system, messaging, backlight, buzzer, vibration

  4. Data Persistence: JSON file for the alarm list

Component Interaction

[System Clock] ──► [AlarmManager] ──► [Service]
                                           ^
                                           |
                    [Message System] ──► [GUI]

The service runs as a separate thread, checking the alarm list each minute and dispatching effects through the kernel. The GUI runs on the TouchGFX framework, handling user input and displaying the alarm list and ringing screen.

Service Backend

The service backend is implemented in Service.hpp and Service.cpp. It owns an AlarmManager instance and acts as its observer, forwarding alarm events to the GUI through the messaging system.

Core Classes and Structures

Service Class

class Service : public AlarmManager::AlarmCallback
{
public:
    Service(SDK::Kernel &kernel);
    virtual ~Service();
    void run();

private:
    SDK::Kernel&          mKernel;
    bool                  mGuiStarted;
    CustomMessage::Sender mGuiSender;
    AlarmManager          mAlarmManager;
    Alarm                 mActiveAlarm;
    // ...
};

The service implements AlarmManager::AlarmCallback to receive two events:

  • onAlarm(alarm) β€” called when a scheduled alarm fires

  • onListChanged(list) β€” called whenever the alarm list changes (load or save)

Alarm Data Structure

Alarm is a plain struct shared between GUI and Service (defined in Libs/Header/Alarm.hpp):

struct Alarm {
    bool    on;           // Whether the alarm is enabled
    uint8_t timeHours;    // Hour (0–23)
    uint8_t timeMinutes;  // Minute (0–59)
    Repeat  repeat;       // Repetition schedule
    Effect  effect;       // Alert effect
};

Alarm::Repeat (11 options):

Value

JSON key

Meaning

REPEAT_NO

"no"

Fires once, then disabled automatically

REPEAT_EVERY_DAY

"every_day"

Every day

REPEAT_WEEK_DAYS

"week_days"

Monday – Friday

REPEAT_WEEKENDS

"weekends"

Saturday and Sunday

REPEAT_MONDAY … REPEAT_SUNDAY

"monday" … "sunday"

Specific day of the week

Alarm::Effect (3 options):

Value

JSON key

Behavior

EFFECT_BEEP_AND_VIBRO

"beep_vibro"

Buzzer + vibration

EFFECT_VIBRO

"vibro"

Vibration only

EFFECT_BEEP

"beep"

Buzzer only

Identity: Two Alarm objects are considered equal (for snooze tracking and duplicate detection) if timeHours, timeMinutes, and repeat match. The on flag and effect are excluded from the comparison.

AlarmManager Class

AlarmManager handles all alarm logic independently of the service loop. It is responsible for:

  • Loading and saving the alarm list from a JSON file (alarms.json)

  • Checking once per minute whether any enabled alarm is due

  • Tracking snoozed alarms and re-triggering them after a 5-minute interval

  • Notifying an observer (AlarmCallback) on each alarm fire and on list changes

class AlarmManager {
public:
    class AlarmCallback {
    public:
        virtual void onAlarm(const Alarm& alarm) {}
        virtual void onListChanged(const std::vector<Alarm>& list) {}
    };

    void     load();
    void     attachCallback(AlarmCallback* pCallback);
    uint32_t execute(const std::tm& tmNow);
    bool     saveAlarmList(const std::vector<Alarm>& list);
    void     disableAlarm(const Alarm& alarm);
    void     disableAllActiveAlarm();
    void     snoozeAlarm(const Alarm& alarm);
    void     snoozeAllActiveAlarm();
    bool     hasActiveAlarms() const;
};

Constants:

Constant

Value

Meaning

kSnoozedTimeMinutes

5

Minutes between snooze re-triggers

kMaxSnoozeCount

5

Maximum snooze re-trigger count per alarm

kInitialCount

20

Initial vector capacity (also the maximum for message transfer)

Alarm Checking Logic

AlarmManager::execute() is called once per iteration of the service loop. It delegates to checkAlarms() and returns the milliseconds until the next full minute:

uint32_t AlarmManager::execute(const std::tm& tmNow)
{
    checkAlarms(tmNow.tm_hour, tmNow.tm_min, tmNow.tm_wday, tmNow);
    uint32_t nextCheckMs = (60 - tmNow.tm_sec) * 1000;
    return nextCheckMs;
}

checkAlarms() performs two passes per call:

Pass 1 β€” regular alarms: For each enabled alarm that matches the current hour, minute, and day schedule, and has not been snoozed yet:

  1. Calls mObserver->onAlarm(alarm)

  2. Adds the alarm to the snoozed-alarm tracking list (addSnoozedAlarm)

  3. For one-time alarms (REPEAT_NO): sets alarm.on = false and saves to file immediately

Pass 2 β€” snoozed alarms: Checks each entry in mSnoozedAlarms. When nextTriggerHour:nextTriggerMinute matches the current time:

  • Decrements snoozeCount

  • If snoozeCount > 0: re-triggers onAlarm and advances the next trigger time by kSnoozedTimeMinutes

  • If snoozeCount == 0: removes the entry (snooze exhausted)

Snooze design note: snoozeAlarm() and snoozeAllActiveAlarm() are intentional no-ops. The alarm is already inserted into mSnoozedAlarms when it first fires in checkAlarms. Re-triggering after the snooze interval is handled automatically by execute(). The GUI only needs to call these methods to communicate intent; no additional action is required on the service side.

Alarm Persistence

Alarms are stored in alarms.json in the application’s file system. The JSON schema is:

{
  "alarms": [
    { "on": true,  "time_h": 7, "time_m": 30, "repeat": "week_days", "effect": "beep_vibro" },
    { "on": false, "time_h": 9, "time_m": 0,  "repeat": "weekends",  "effect": "vibro"      }
  ]
}

The internal serialisation buffer is 2048 bytes. A worst-case file with 20 alarms (all using the longest field values: false, 23:59, "wednesday", "beep_vibro") produces ~1612 bytes of compact JSON, which fits comfortably within this limit. If the file exceeds the buffer, loading fails gracefully with an error log.

Service Lifecycle and App Startup

The Alarm service is configured with APP_AUTOSTART On, which instructs the kernel to launch the service automatically at boot. This is essential for time-based alarm detection: the service must be running in the background at all times so it can check the alarm list each minute, even when the GUI is not open.

The service’s run() method handles two startup scenarios:

Scenario A β€” User opens app from menu (GUI already starting):

  1. Service starts, loads alarms from alarms.json

  2. Receives COMMAND_APP_NOTIF_GUI_RUN β†’ calls onStartGUI():

    • Sets mGuiStarted = true

    • If a pending active alarm exists (mActiveAlarm.on), sends it to GUI via ACTIVATED_ALARM

    • Sends the full alarm list to GUI via ALARM_LIST

  3. Enters the minute-loop, checking for alarms and processing GUI messages

Scenario B β€” Alarm fires automatically (GUI not yet running):

  1. Service starts, loads alarms

  2. First execute() call detects a due alarm β†’ onAlarm(alarm):

    • Sends RequestAppRunGui to kernel (kernel launches GUI)

    • Saves mActiveAlarm = alarm

  3. Kernel starts GUI; COMMAND_APP_NOTIF_GUI_RUN arrives β†’ onStartGUI() sends ACTIVATED_ALARM + ALARM_LIST to GUI

  4. GUI navigates directly to RingingView

Early exit: If mGuiStarted is still false after 5 seconds and hasActiveAlarms() returns false, the service exits without displaying anything.

Effect Playback

When the GUI sends ACTIVATED_EFFECT, the service plays the configured pattern:

Backlight: always on (brightness 100%, auto-off after 4000 ms).

Vibration pattern (for EFFECT_VIBRO and EFFECT_BEEP_AND_VIBRO):

ALERT_750MS_100 β€” 250ms pause β€” ALERT_750MS_100 β€” 250ms pause β€” ALERT_750MS_100

Buzzer pattern (for EFFECT_BEEP and EFFECT_BEEP_AND_VIBRO):

750ms@100% β€” 250ms@0% β€” 750ms@100% β€” 250ms@0% β€” 750ms@100%

Stopping the effect (stopRinging()) sends empty RequestBuzzerPlay and RequestVibroPlay messages, which cancels any active pattern.

GUI

The GUI is built using the TouchGFX framework and follows the Model-View-Presenter pattern.

Project Structure

Alarm/Software/Apps/TouchGFX-GUI/
β”œβ”€β”€ AlarmGUI.touchgfx     # TouchGFX Designer project
β”œβ”€β”€ application.config    # Application configuration
β”œβ”€β”€ target.config         # Target hardware settings
β”œβ”€β”€ touchgfx.cmake        # CMake integration
β”œβ”€β”€ gui/                  # Custom GUI code
β”œβ”€β”€ assets/               # Images, fonts, texts
β”œβ”€β”€ generated/            # Auto-generated TouchGFX code
└── simulator/            # Simulator builds

Model-View-Presenter Pattern

  • Model: Model.hpp/cpp β€” Alarm state, service communication, lifecycle

  • View: Screen view classes β€” UI rendering and input

  • Presenter: Screen presenter classes β€” Logic binding model and view

Model

The Model class serves as the central data hub and gateway to the service:

class Model : public touchgfx::UIEventListener,
              public SDK::Interface::IGuiLifeCycleCallback,
              public SDK::Interface::ICustomMessageHandler
{
public:
    void tick();
    void handleKeyEvent(uint8_t key);
    void resetIdleTimer();
    void exitApp();
    void switchToNextPriorityScreen();

    // Alarm access
    const Alarm&        getActiveAlarm() const;
    void                playAlarm();
    void                stopAlarm();
    void                snoozeAlarm();
    std::vector<Alarm>& getAlarmList();
    void                setAlarmEditId(size_t id);
    size_t              getAlarmEditId();
    void                saveAlarm(size_t id, Alarm alarm);
    void                deleteAlarm(size_t id);
};

Key Model behaviours:

  • switchToNextPriorityScreen(): determines where to navigate after completing an action. Priority order: (1) alarm ringing β†’ RingingView; (2) mStayInApp flag set β†’ MainView; (3) otherwise β†’ exitApp(). mStayInApp is set to true when the user presses any key while no alarm is ringing, ensuring the app stays visible after the user completes an edit, save, or delete flow.

  • saveAlarm(id, alarm): If id < list.size(), overwrites in-place. If id == list.size() (new alarm), first checks for duplicates by identity (time + repeat); if a match is found, overwrites it instead of appending. Otherwise appends.

  • setCapabilities(): Called in the constructor. Sends RequestSetCapabilities to the kernel with enMusicControl = true and enUsbChargingScreen = true, enabling the system to overlay music controls and USB charging status while this app is active.

  • Idle timer: Counts down at the frame rate; fires modelListener->onIdleTimeout() after App::Config::kScreenTimeoutSteps (30 seconds). Resets on any button press.

Screens (6 total)

Screen

Class

Purpose

Main

MainView

Alarm list with scroll-wheel selection

Menu

MenuView

Action menu for a selected alarm (Toggle / Edit / Delete)

Edit

EditView

Time and options editor (time wheel + option wheels)

Saved

SavedView

Confirmation after save (auto-dismisses β†’ next priority screen)

Deleted

DeletedView

Confirmation after delete (auto-dismisses β†’ next priority screen)

Ringing

RingingView

Active alarm: shows alarm time; buttons to Stop or Snooze

Message Handling System

The Model implements ICustomMessageHandler to receive asynchronous messages from the service:

bool Model::customMessageHandler(SDK::MessageBase* msg)
{
    switch (msg->getType()) {
        case CustomMessage::ALARM_LIST: {
            auto* m = static_cast<CustomMessage::AlarmList*>(msg);
            mAlarmList.assign(m->alarms, m->alarms + m->count);
            modelListener->onAlarmListUpdated(mAlarmList);
        } break;

        case CustomMessage::ACTIVATED_ALARM: {
            auto* m  = static_cast<CustomMessage::ActivatedAlarm*>(msg);
            mActiveAlarm = m->alarm;
            modelListener->onAlarmActivated(mActiveAlarm);
        } break;

        default: break;
    }
    return true;
}

ModelListener::onAlarmActivated has a default implementation that unconditionally navigates to RingingView β€” no presenter needs to override it. The active alarm data is available via model->getActiveAlarm().

Screen Navigation

All screen changes use gotoXxxScreenNoTransition() β€” no animated transitions. Animations exist only within containers: TimeWheel and OptionWheel scroll movement.

Screen Flow:

MainView ──[R1: open menu]──► MenuView
    β”‚                             β”‚           β”‚           β”‚
    β”‚                         [Toggle]      [Edit]     [Delete]
    β”‚                             β”‚           β”‚           β”‚
    β”‚                         MainView    EditView    DeletedView
    β”‚                                         β”‚       (auto β†’ switchToNextPriorityScreen)
    β”‚                                   [R1: save]
    β”‚                                     SavedView
    β”‚                                  (auto β†’ switchToNextPriorityScreen)
    β”‚
    └──[R2: exit]──► exitApp()

RingingView ──[R1: Stop]──►   stopAlarm()   β†’ switchToNextPriorityScreen()
             ──[R2: Snooze]──► snoozeAlarm() β†’ switchToNextPriorityScreen()

switchToNextPriorityScreen():
    alarm ringing  ──► RingingView
    mStayInApp     ──► MainView
    else           ──► exitApp()

When an alarm fires, ModelListener::onAlarmActivated navigates from any active screen directly to RingingView. If the user is currently in EditView, RingingPresenter::deactivate() calls model->stopAlarm() to ensure the effect is cleaned up if the user dismisses without pressing Stop.

Custom Containers

The app uses two categories of containers: ready-made SDK widgets from Templates/TouchGFX-Widgets (imported as .tpkg packages) and app-specific containers built for alarm editing.

SDK Widget Containers

Imported from Templates/TouchGFX-Widgets and used without modification:

  • Buttons β€” button arc indicators (L1/L2/R1/R2)

  • Title β€” screen title with underline

  • Toggle β€” on/off toggle switch

App-Specific Containers

Built specifically for this app and found in gui/src/containers/:

TimeWheel β€” dual scroll-wheel for picking hours (0–23) and minutes (0–59). Composed of two independent scroll columns with a highlighted center item (TimeWheelHoursCenterItem, TimeWheelMinutesCenterItem) and adjacent items (TimeWheelHoursItem, TimeWheelMinutesItem).

OptionWheel β€” generic vertical scroll-wheel for selecting one value from a list. Used for Repeat (11 items) and Effect (3 items) in EditView. Center item rendered by OptionWheelCenterItem; adjacent items by OptionWheelItem.

CountdownTimer β€” self-registering per-frame countdown that fires a callback when it reaches zero. Used by SavedView and DeletedView for auto-dismiss after a fixed delay.

Input Handling

Button Mapping:

  • L1: Scroll up / previous item

  • L2: Scroll down / next item

  • R1: Primary action (confirm / open menu / save)

  • R2: Secondary action (exit / snooze)

Any button press resets the idle timer. If no alarm is ringing at the time of the press, mStayInApp is set to true so the app does not exit after the current flow completes.

Idle Timeout

The app implements automatic timeout to conserve battery:

  • Timeout: App::Config::kScreenTimeoutSteps = 30 seconds at the frame rate

  • On timeout, the active presenter’s onIdleTimeout() is called, which navigates via switchToNextPriorityScreen()

  • Timer resets on any button press (handleKeyEvent)

TouchGFX Integration with UNA SDK

TouchGFXCommandProcessor

SDK::TouchGFXCommandProcessor is a singleton that bridges the SDK kernel and the TouchGFX event loop. Custom messages received during waitForFrameTick() (called from OSWrappers::waitForVSync()) are placed in an internal queue. FrontendApplication::handleTickEvent() calls callCustomMessageHandler() first, then model.tick(), ensuring pending service messages are dispatched to presenters before the view updates.

Kernel Provider Architecture

SDK::KernelProviderGUI is a singleton that stores a pointer to the SDK::Kernel object. It is initialised before the GUI starts and retrieved by the Model constructor:

Model::Model()
    : mKernel(SDK::KernelProviderGUI::GetInstance().getKernel())
    , mSrvSender(mKernel)
{
    SDK::TouchGFXCommandProcessor::GetInstance().setAppLifeCycleCallback(this);
    SDK::TouchGFXCommandProcessor::GetInstance().setCustomMessageHandler(this);
    setCapabilities();
}

Custom Message System

All service↔GUI messages are declared in Libs/Header/Commands.hpp as constexpr SDK::MessageType::Type constants. Every message struct inherits from SDK::MessageBase. All structs are wrapped in #pragma pack(push, 4) for alignment consistency with the message pool.

namespace CustomMessage {

static constexpr size_t kMaxAlarms = 20;  // must match AlarmManager::kInitialCount

// Service <-> GUI
constexpr SDK::MessageType::Type ALARM_LIST       = 0x00000001;

// Service --> GUI
constexpr SDK::MessageType::Type ACTIVATED_ALARM  = 0x00000002;

// GUI --> Service
constexpr SDK::MessageType::Type ACTIVATED_EFFECT = 0x00000003;
constexpr SDK::MessageType::Type ALARM_STOP       = 0x00000004;
constexpr SDK::MessageType::Type ALARM_STOP_ALL   = 0x00000005;
constexpr SDK::MessageType::Type ALARM_SNOOZE     = 0x00000006;
constexpr SDK::MessageType::Type ALARM_SNOOZE_ALL = 0x00000007;

} // namespace CustomMessage

AlarmList transfers the entire alarm list as a fixed-size C-array to avoid heap allocation in the message pool path:

struct AlarmList : public SDK::MessageBase {
    Alarm   alarms[kMaxAlarms];
    uint8_t count;
    // sizeof = 32 (MessageBase) + 20*sizeof(Alarm) + 1 = 133 bytes β†’ Pool 3 (256 bytes)
};

Message summary:

Message

Direction

Trigger

ALARM_LIST

Service β†’ GUI

On GUI start; after any list change on service side

ALARM_LIST

GUI β†’ Service

User saves or deletes an alarm

ACTIVATED_ALARM

Service β†’ GUI

Alarm fires

ACTIVATED_EFFECT

GUI β†’ Service

RingingPresenter::activate() β†’ model->playAlarm()

ALARM_STOP / ALARM_STOP_ALL

GUI β†’ Service

User taps Stop on RingingView

ALARM_SNOOZE / ALARM_SNOOZE_ALL

GUI β†’ Service

User taps Snooze on RingingView

Message Sender

CustomMessage::Sender provides type-safe message sending for both directions. Each method allocates a typed message, fills fields, sends, and releases:

class Sender {
public:
    Sender(const SDK::Kernel& kernel);

    // Service <-> GUI
    bool listUpd(const std::vector<Alarm>& list);

    // Service --> GUI
    bool alarmActivated(const Alarm& alarm);

    // GUI --> Service
    bool activateEffect(const Alarm& alarm);
    bool stop(const Alarm& alarm);
    bool stopAll();
    bool snooze(const Alarm& alarm);
    bool snoozeAll();
};

Build and Setup

Build System Overview

Primary Build File: CMakeLists.txt in Alarm/Software/Apps/Alarm-CMake/

# App configuration
set(APP_NAME "Alarm")
set(APP_TYPE "Utility")
set(APP_AUTOSTART On)
set(DEV_ID "UNA")
set(APP_ID "A19C2A7E4F8B6D31")

# Include SDK build scripts
include($ENV{UNA_SDK}/cmake/una-app.cmake)
include($ENV{UNA_SDK}/cmake/una-sdk.cmake)

Build Targets

Service Build:

set(SERVICE_SOURCES
    ${LIBS_SOURCES}
    ${UNA_SDK_SOURCES_COMMON}
    ${UNA_SDK_SOURCES_APPSYSTEM}
    ${UNA_SDK_SOURCES_JSON}
)
una_app_build_service(${APP_NAME}Service.elf)

GUI Build:

set(GUI_SOURCES
    ${TOUCHGFX_SOURCES}
    ${UNA_SDK_SOURCES_COMMON}
    ${UNA_SDK_SOURCES_GUI}
)
una_app_build_gui(${APP_NAME}GUI.elf)

Complete App:

una_app_build_app()

Dependencies

SDK Components:

  • UNA SDK common, GUI, app-system, and JSON sources

  • Kernel and messaging systems

  • File system interface

App Libraries (Libs/):

  • AlarmManager β€” alarm persistence and time-based triggering

  • Service β€” service entry point and alarm event handling

  • Alarm.hpp β€” shared alarm data structure

Development Setup

See SDK Setup and Build Overview for comprehensive development environment setup, build instructions, and toolchain requirements.

Simulator Build

The TouchGFX project includes a Visual Studio simulator build located in simulator/msvs/Application.vcxproj.

Conclusion

The Alarm app demonstrates a lightweight but complete UNA application that operates independently of sensor hardware. Its architecture is simpler than activity-tracking apps β€” no sensor connections, no FIT recording, no real-time data streams β€” but it showcases key SDK patterns: background service with autonomous GUI launch, bidirectional messaging for CRUD operations, priority-based screen routing, and a clean separation between the alarm engine (AlarmManager) and the transport layer (Service).

Key architectural strengths include:

  • Separation of Concerns: AlarmManager handles all timing logic independently; Service handles only transport and lifecycle

  • Zero heap allocation in the message path: Fixed-size AlarmList array fits entirely within a single pool block

  • Resilient app lifecycle: Service can start, trigger an alarm, and launch the GUI autonomously without user interaction

  • Priority-based navigation: switchToNextPriorityScreen() ensures a ringing alarm always surfaces above any other screen the user was on