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ο
Service Layer: Alarm persistence, time-based checking, effect playback
GUI Layer: TouchGFX-based user interface, alarm CRUD operations
SDK Integration: Kernel, file system, messaging, backlight, buzzer, vibration
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 firesonListChanged(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 |
|---|---|---|
|
|
Fires once, then disabled automatically |
|
|
Every day |
|
|
Monday β Friday |
|
|
Saturday and Sunday |
|
|
Specific day of the week |
Alarm::Effect (3 options):
Value |
JSON key |
Behavior |
|---|---|---|
|
|
Buzzer + vibration |
|
|
Vibration only |
|
|
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 |
|---|---|---|
|
5 |
Minutes between snooze re-triggers |
|
5 |
Maximum snooze re-trigger count per alarm |
|
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:
Calls
mObserver->onAlarm(alarm)Adds the alarm to the snoozed-alarm tracking list (
addSnoozedAlarm)For one-time alarms (
REPEAT_NO): setsalarm.on = falseand saves to file immediately
Pass 2 β snoozed alarms: Checks each entry in mSnoozedAlarms. When nextTriggerHour:nextTriggerMinute matches the current time:
Decrements
snoozeCountIf
snoozeCount > 0: re-triggersonAlarmand advances the next trigger time bykSnoozedTimeMinutesIf
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):
Service starts, loads alarms from
alarms.jsonReceives
COMMAND_APP_NOTIF_GUI_RUNβ callsonStartGUI():Sets
mGuiStarted = trueIf a pending active alarm exists (
mActiveAlarm.on), sends it to GUI viaACTIVATED_ALARMSends the full alarm list to GUI via
ALARM_LIST
Enters the minute-loop, checking for alarms and processing GUI messages
Scenario B β Alarm fires automatically (GUI not yet running):
Service starts, loads alarms
First
execute()call detects a due alarm βonAlarm(alarm):Sends
RequestAppRunGuito kernel (kernel launches GUI)Saves
mActiveAlarm = alarm
Kernel starts GUI;
COMMAND_APP_NOTIF_GUI_RUNarrives βonStartGUI()sendsACTIVATED_ALARM+ALARM_LISTto GUIGUI 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, lifecycleView: 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)mStayInAppflag set βMainView; (3) otherwise βexitApp().mStayInAppis set totruewhen 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): Ifid < list.size(), overwrites in-place. Ifid == 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. SendsRequestSetCapabilitiesto the kernel withenMusicControl = trueandenUsbChargingScreen = 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()afterApp::Config::kScreenTimeoutSteps(30 seconds). Resets on any button press.
Screens (6 total)ο
Screen |
Class |
Purpose |
|---|---|---|
Main |
|
Alarm list with scroll-wheel selection |
Menu |
|
Action menu for a selected alarm (Toggle / Edit / Delete) |
Edit |
|
Time and options editor (time wheel + option wheels) |
Saved |
|
Confirmation after save (auto-dismisses β next priority screen) |
Deleted |
|
Confirmation after delete (auto-dismisses β next priority screen) |
Ringing |
|
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().
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 underlineToggleβ 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 rateOn timeout, the active presenterβs
onIdleTimeout()is called, which navigates viaswitchToNextPriorityScreen()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 |
|---|---|---|
|
Service β GUI |
On GUI start; after any list change on service side |
|
GUI β Service |
User saves or deletes an alarm |
|
Service β GUI |
Alarm fires |
|
GUI β Service |
|
|
GUI β Service |
User taps Stop on |
|
GUI β Service |
User taps Snooze on |
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 triggeringServiceβ service entry point and alarm event handlingAlarm.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:
AlarmManagerhandles all timing logic independently;Servicehandles only transport and lifecycleZero heap allocation in the message path: Fixed-size
AlarmListarray fits entirely within a single pool blockResilient 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