Service Lifecycle
Revision History
Revision |
Date of Changes |
Matter of Change |
Note |
Editor |
|---|---|---|---|---|
1.00 |
09.09.2026 |
Creating: the two-process model, how a service is started, what a resident service receives, how a service must end itself, GUI focus versus the display, and the conditions that stop an app |
Ross Ryles |
|
1.01 |
24.09.2026 |
Glance selection: |
Denys Saienko |
|
1.02 |
24.09.2026 |
Self-exit: Stopwatch and the clockfaces leave after a startup grace when no GUI comes up; Stopwatch’s resident branch is rarely taken, since its GUI offers exit only with the clock stopped |
Denys Saienko |
1. Overview
Every .uapp is two processes: a service that holds the app’s logic and state, and a
GUI that draws it. They have independent lifetimes, and that makes a resident
service — one that keeps state moving while nothing is on screen — a supported design.
The two things to take from this document:
A service is not stopped because its GUI closed, and it is not stopped for being idle. Nothing wakes a service because time has passed.
The first is why a resident service works. The second is why it is your job to pace it, and why ending the service is your responsibility.
1.1 Promised, and not promised
An app that relies only on the left column keeps working. The right column is how the current kernel happens to behave; it is not a promise, and a later release may change it.
Promised |
Not promised |
|---|---|
Your service is not stopped when its GUI closes; it receives |
That an idle service is never reclaimed. Do not rely on being allowed to stay resident with nothing to do |
Your service’s sensor subscriptions survive its GUI closing |
That a service exceeding its resource budget is left alone |
Your service keeps receiving what it subscribed to, and messages from its own GUI |
That any particular message arrives periodically — including any not listed in section 4.1 |
|
That every teardown is orderly. A reboot or power loss gives no notice at all |
|
Any particular ordering, timing or count of autostarted services |
1.2 Who needs this
Read it if your app is any of these:
state must keep moving with nobody looking — an alarm, a timer, a long-running count;
something must happen at a time, not merely be known about at a time;
the app is started with
APP_AUTOSTART On;the app is a
Glance, whose service runs with no GUI at all;the service holds a sensor subscription that should outlive a screen.
If your app only needs to show the right answer whenever the user opens it, you very likely do not need a resident service. See section 9.
2. The two processes
Service |
GUI |
|
|---|---|---|
Purpose |
logic, state, sensors, files |
drawing, buttons |
Started by |
the user opening the app; |
the user opening the app; the service’s own |
Started when the other is not |
yes — routinely |
no — the service is always started first |
Paced by |
nothing (see section 4.2) |
|
Ended by |
returning from |
returning from |
Ended by the user navigating away |
no |
no — it is suspended, not ended. See section 6 |
The service is started first, always: no path starts a GUI without its service.
stateDiagram-v2
[*] --> ServiceRunning : user opens the app / APP_AUTOSTART / glances screen
ServiceRunning --> SrvAndGui : GUI loaded (launch, or REQUEST_APP_RUN_GUI)
SrvAndGui --> OnScreen : COMMAND_APP_GUI_RESUME
OnScreen --> SrvAndGui : COMMAND_APP_GUI_SUSPEND (user navigated away)
SrvAndGui --> ServiceRunning : the GUI exits — service gets COMMAND_APP_NOTIF_GUI_STOP
ServiceRunning --> [*] : the service returns from run()
OnScreen --> [*] : COMMAND_APP_STOP, or the service exits
note right of ServiceRunning
A resident service. Nothing here
times out today. Leaving this state
is the app's own decision.
end note
note right of SrvAndGui
Loaded but off screen. The GUI is
NOT unloaded here and gets no ticks.
end note
3. Starting a service
3.1 On demand
The ordinary case. The user picks the app; the service is loaded and started, then the GUI.
Once the GUI process has been loaded — before it has drawn anything or come on screen —
the service receives COMMAND_APP_NOTIF_GUI_RUN. That is your cue to send it the current
state, because a freshly loaded GUI knows nothing.
Note the order: there is a window on every single launch during which the service is running and the GUI is not. Any “no GUI, so nobody wants me” exit test must be guarded against that window — see section 5.2.
3.2 Autostart
APP_AUTOSTART On in CMakeLists.txt (see sdk-setup.md) sets a bit in the
packed header. The watch reads it and starts the app’s service — and only the service —
at boot, after it has scanned the installed apps, and again on leaving USB mass-storage mode.
No user action, no GUI, nothing on screen.
This is the sanctioned form of a resident service. The service runs from boot until it returns or one of the conditions in section 7 occurs.
When such a service needs the user, it asks for its own GUI:
auto *msg = mKernel.comm.allocateMessage<SDK::Message::RequestAppRunGui>();
if (msg) {
mKernel.comm.sendMessage(msg); // fire-and-forget: the default timeout is 0
mKernel.comm.releaseMessage(msg);
}
// Do NOT assume the GUI is usable here. Wait for COMMAND_APP_NOTIF_GUI_RUN, which is the
// authoritative signal, and send the GUI its state from that branch.
The watch both loads the GUI and brings it to the screen, so this is how a background service takes the display. Whichever app was on screen is suspended. Alarm is the worked example.
Note the timeout: sendMessage(msg) defaults to timeoutMs = 0, which sends without waiting
and leaves getResult() unfilled. Passing a non-zero timeout makes the call block until the
watch has answered, after which getResult() is SUCCESS, FAIL or TIMEOUT. Either is
valid, but even SUCCESS only means the GUI process was loaded — treat
COMMAND_APP_NOTIF_GUI_RUN as the point from which you may talk to it.
Two limits worth knowing:
A variant alias cannot autostart. The autostart bit is cleared when an alias is resolved, so a code-less alias of an autostart app is not itself autostarted.
Autostart is not required for residency. Any app started normally may stay resident after its GUI closes; Stopwatch does exactly that. Autostart is specifically about running before and without any GUI.
3.3 The glances screen
For an app whose APP_TYPE is Glance, opening the glances screen starts the service
and never the GUI. The service then receives:
Message |
Meaning |
|---|---|
|
your glance is on screen — configure it and subscribe |
|
periodic, while your glance is on screen |
|
your glance is off screen |
EVENT_GLANCE_STOP does not unload you. It is a notification, and what it means for
your app is your decision. Exiting is the usual and expected answer — see
GlanceHR, which disconnects its sensors and returns. A
glance that deliberately keeps a subscription running is a legitimate choice, but then
nothing else will ask it to stop.
Set APP_TYPE Glance for an app whose service should be started for the glances screen.
An app of another type is left alone there, unless its image was packed by an earlier SDK
and runs on kernel 1.4.0 or earlier — see section 10.
4. What a resident service gets
“Resident” here means the service is running and no GUI of its own is loaded — after
COMMAND_APP_NOTIF_GUI_STOP, or from boot under APP_AUTOSTART.
4.1 What still arrives
Source |
Still delivered while resident? |
|---|---|
Sensor data you subscribed to ( |
Yes — at the rate you asked for, ungated |
Accessory / external-sensor events |
Yes |
Your own custom messages from your GUI |
Yes, when a GUI exists |
Lifecycle commands ( |
Yes |
|
Never — that goes to the GUI process, and only while it is on screen |
A periodic wake-up of your own |
No. Nothing paces a service |
Sensor subscriptions belong to the process, not to the screen. A service subscription is untouched when the GUI closes, and samples keep arriving at the rate you asked for.
Read that as a responsibility, not a licence. A resident service holding a GPS or heart-rate subscription keeps that sensor powered and duty-cycled with nothing on screen. Disconnect what you no longer need — a resident service should hold the cheapest subscription that still does its job, or none at all.
Handle only the message types you asked for, and ignore the rest. Your service may be
handed messages this table does not list, and which types those are is not part of the
contract. A default: branch that ignores unknown types — and still releases them — is the
only forward-compatible shape. Never treat an unrequested message as a clock.
4.2 What paces a resident service — nothing
There is no periodic message you can rely on, and time passing is not an event. Never build on an unbounded wait for work that is due at a time:
// Correct only for a service that genuinely has nothing to do between messages.
mKernel.comm.getMessage(msg, 0xFFFFFFFF); // may block for the life of the app
Blocking like that is what lets the chip reach its low-power state with your service still resident, so it is the right choice for a service driven purely by events. It is the wrong choice the moment something must happen on the clock.
Use a bounded wait and do time-driven work in the timeout branch:
static constexpr uint32_t kTickMs = 1000; // your pacing choice — see the note below
void Service::run()
{
SDK::Timer guiInitTimeout(TIMER_SECONDS(5)); // the startup grace, see section 5.2
guiInitTimeout.start();
while (true) {
SDK::MessageBase *msg;
if (mKernel.comm.getMessage(msg, kTickMs)) {
switch (msg->getType()) {
case SDK::MessageType::COMMAND_APP_STOP:
mKernel.comm.releaseMessage(msg);
return; // release, then leave
case SDK::MessageType::COMMAND_APP_NOTIF_GUI_RUN:
mGuiStarted = true;
publish(); // a new GUI knows nothing
break;
case SDK::MessageType::COMMAND_APP_NOTIF_GUI_STOP:
mGuiStarted = false;
break;
default:
handle(msg); // must ignore unknown types
break;
}
mKernel.comm.releaseMessage(msg);
} else {
// Timed out: no message arrived. Time-driven work goes here.
advance(mKernel.sys.getTimeMs());
if (!mGuiStarted && guiInitTimeout.expired() && !hasWorkOutstanding()) {
LOG_INFO("Nothing to do and no GUI, exiting service\n");
return;
}
}
}
}
Two rules for the time-driven part:
Derive elapsed time from
sys.getTimeMs(); never accumulate it.getTimeMs()is milliseconds since boot and moves monotonically whether or not your loop woke up on schedule. Counting your own wake-ups drifts, and drifts worst exactly when the system is busy.kTickMsis a power decision. It is a wake-up everykTickMsfor as long as your service is resident. Pick the longest period that meets your requirement, and prefer having a period at all only if something must happen; if the answer merely has to be correct when read, compute it on demand instead.
5. Ending a service
5.1 The two ways out
Return from
run()(or callsys.exit()). This is the app deciding it is done.COMMAND_APP_STOP— the watch tearing the app down. Release the message, free what you hold, and return.
Act on COMMAND_APP_STOP promptly. The watch waits a grace period for your service to
acknowledge it by exiting, and force-terminates the thread if you do not — which can
strand resources you were holding. Note also that pending messages are discarded before
COMMAND_APP_STOP is delivered, so do not expect to drain a backlog first.
Write the exit condition before anything else. Nothing in the current kernel ends an idle service — there is no idle timeout — so a service with no reachable path out of
run()stays until the watch reboots. Do not read that as permission: an app that can always exit on its own is the only shape guaranteed to keep working, and a later kernel may reclaim a service that never exits or outstays its resource budget.
5.2 Guard every exit against startup
The service starts before the GUI on every normal launch, so “no GUI has started” is
briefly true every single time. An unguarded if (!mGuiStarted) return; exits during launch
and the app appears to fail to open. Every example that exits on its own therefore uses a
startup grace — the activity examples with
SDK::Timer as shown in section 4.2, Alarm, Timer,
Stopwatch and the clockfaces with plain getTimeMs() arithmetic. Either is fine.
Three conditions, all load-bearing: the GUI is not up, the launch window has passed, and your own work is finished. Drop any one and you get either a service that exits mid-launch or one that never exits at all.
5.3 The asymmetry that catches people
Which process exits determines what happens to the other:
The process that exits |
Effect |
|---|---|
The GUI returns from |
The GUI is unloaded. The service keeps running and receives |
The service returns from |
The whole app goes, GUI included. |
So a service must not return from run() as a way of going quiet while its GUI is loaded: it
takes the GUI with it. Worse, the GUI is unloaded without notice on this path — it does
not receive COMMAND_APP_STOP and its onStop() does not run, so anything it would do on
the way out is simply lost. Every example checks that the GUI is gone before releasing
itself.
That is why a service’s exit test belongs where the GUI is known to be gone: the
COMMAND_APP_NOTIF_GUI_STOP branch, or a check guarded on no GUI having started, as the
startup-grace exits in section 5.4 are.
5.4 The patterns in the examples
Example |
Wait |
Exits when |
|---|---|---|
bounded |
after a 5 s grace, no GUI started and no active alarms |
|
bounded |
after a startup grace, idle and the GUI closed |
|
bounded |
after a 5 s grace, no GUI started |
|
unbounded |
|
|
bounded until the GUI runs, then unbounded |
after a 5 s grace, no GUI started; or |
|
bounded (next minute or sooner) |
after a 5 s grace, no GUI started; or |
Read the activity apps’ condition precisely: the exit is guarded on the GUI never having
started, not on whether an activity is in progress. A recording activity is safe only
because its GUI does not exit mid-activity. If you copy that pattern into an app whose GUI
can close while work is outstanding, add the hasWorkOutstanding() term yourself.
Stopwatch shows the shape for state that must outlive the screen: a running clock keeps the
service resident through COMMAND_APP_NOTIF_GUI_STOP, and a returning GUI is handed the state
on COMMAND_APP_NOTIF_GUI_RUN. In Stopwatch that branch is rarely taken, since its GUI offers
the exit control only once the clock has stopped; copy it when your GUI can close while work
is outstanding. Its normal exit is COMMAND_APP_NOTIF_GUI_STOP, a message that never arrives
if no GUI ever ran, so until the first GUI it waits with a bounded timeout and leaves once a
startup grace has run out. Copy both halves: an unbounded wait is safe only after a GUI has
come up to send the message it is waiting for.
6. GUI focus: suspend and resume
COMMAND_APP_GUI_SUSPEND and COMMAND_APP_GUI_RESUME mean “your GUI is / is no longer the
thing on screen”. They are navigation, delivered when the user moves off your app’s screen
and back onto it.
They are not the display going dark. The backlight is a timed pulse, independent of app lifecycle: while the screen is dark with your app still in front, your GUI is still the app on screen. The SDK exposes no screen-off signal to an app.
Suspended |
On screen |
|
|---|---|---|
Loaded in memory |
yes |
yes |
|
do not rely on receiving them — see section 10 |
yes |
Button events |
no |
yes |
Draws to the display |
no |
yes |
A suspended GUI is not unloaded. Navigating away from your app suspends its GUI and
nothing more: the GUI keeps its RAM, and your service does not receive
COMMAND_APP_NOTIF_GUI_STOP. A GUI is unloaded only when it ends itself (sys.exit(), which
is what an app’s own exit control does), when the service exits, or on a teardown from
section 7. If your app should release itself when the user walks away from it, the GUI has to
take that decision and exit; no one else will.
The service is told nothing about focus. Suspend and resume go to the GUI process only. A
service cannot distinguish “the user navigated away” from “the user is still looking” — it
learns only about COMMAND_APP_NOTIF_GUI_RUN and COMMAND_APP_NOTIF_GUI_STOP. If your
service needs to know, your GUI must tell it with a custom message.
Should a GUI handle these? Usually not directly — the SDK’s TouchGFX port consumes both and
calls SDK::Interface::IGuiLifeCycleCallback
onSuspend() / onResume(). Override those when you want the hooks. The one that reliably
earns its keep is onResume(): recompute from the service’s state on the way back in rather
than trusting anything cached, the GUI-side counterpart of publishing on
COMMAND_APP_NOTIF_GUI_RUN.
The practical consequence of suspend is that anything paced by GUI ticks may stop until resume. Never put app logic that must keep running in the GUI’s frame handler; that is the service’s job.
7. What ends an app whatever it wants
Two kinds, and the difference matters.
Orderly teardown — your service receives COMMAND_APP_STOP and a grace period:
Condition |
|---|
Watch shutdown |
Low-battery shutdown |
Factory reset |
Entering USB mass-storage mode (autostart services are restarted on the way out) |
No notice at all — the app simply stops running:
Condition |
|---|
Reboot, including a reboot commanded over BLE and one taken to apply a firmware update |
Watchdog reset, or any unexpected reset |
Power loss and battery removal |
Persist as you go. Because the second list gives you nothing, any state a resident service must not lose has to be written when it changes, not on the way out. There is no “last chance to save” callback that covers every case.
Notably not on either list: launching another app, opening the launcher, opening or leaving the glances screen, and the screen timing out. None of them stop a resident service.
One more, and it applies to GUIs only: a GUI that has never produced a frame is unloaded a
few seconds after it was loaded or last brought on screen. The service survives it and
receives COMMAND_APP_NOTIF_GUI_STOP. Note “never” — a GUI that drew once and then wedged is
not covered.
8. Memory, and what happens when it runs out
RAM is charged in two places, and both can fail:
At load. Each process’s code, data, bss and stack are allocated when it is loaded. The stack is yours to size with
UNA_APP_SERVICE_STACK_SIZE(default 10 KB); the rest follows from what you built. If there is not enough room, the launch fails — an already-running service is not evicted to make space for a new app.At runtime. Each process has its own cap on outstanding allocations. Past it,
mallocandnewreturnnullptrfor that process alone. Check for it; on a watch this is a condition to handle, not an abstract possibility. This cap is distinct fromUNA_APP_SERVICE_RAM_LENGTH, which is the linker’s address-space budget rather than an allocation.
There is no reclaim of a resident service under memory pressure today, so the practical consequence is that a service which stays resident when it did not need to makes later launches fail rather than being cleaned up itself. Treat your resource footprint while resident as something you are accountable for, and see the “not promised” column in section 1.1 before designing around the absence of enforcement.
9. Choosing a design
Both designs below are correct. Pick on this question: must something happen at a time, or must something merely be correct when read?
Resident service |
Recompute on open |
|
|---|---|---|
Shape |
|
exit once the GUI has closed; on reopen, derive from |
Costs |
a wake-up per period; RAM held; sensors held if subscribed |
nothing while closed |
Needed for |
notifications, alerts, writing a file at a moment, a transition the wearer must not miss |
elapsed time, totals, anything derivable from a stored timestamp |
Survives a reboot |
only what you persisted |
naturally, since it was already persisted |
Risk |
leaks if the exit condition is wrong |
none |
If your app’s state is a function of a start timestamp and the current time, recompute on open. Persist the timestamp, derive the rest, and let the service exit. That is Stopwatch’s own approach to its reading — derived from a shared monotonic clock, never accumulated — and it is why a stopped stopwatch costs nothing. It also survives the reboot that would take a resident service down without warning. A resident service earns its thread only when something must occur while nobody is looking.
10. Behaviour not to rely on
Two behaviours have changed since kernel 1.4.0, or are close enough to the edge of the contract that you should not build on them. Neither affects service lifecycle itself.
GUI ticks while suspended. On 1.4.0 a GUI that is loaded but off screen still receives
EVENT_GUI_TICK. That is incidental, not promised, and it has already changed: treat ticks as
something you receive only while on screen. If your GUI does work in its tick handler that
must continue while the app is off screen, move that work to the service now — see section 6.
Which apps the glances screen starts. Target APP_TYPE Glance for a glance and give the
glances screen nothing to do with your app otherwise. On 1.4.0 an app of another type could
have its service started on a visit to the glances screen depending on the flags its .uapp
carried. An image packed by this SDK does not carry that flag, so only images packed by an
earlier SDK are affected. Either way, give every service a self-exit that does not depend
on a GUI arriving.
11. Checklist
Before shipping a service that outlives its GUI:
[ ] There is a reachable path out of
run()in every state the service can be in — including “started with no GUI, and none ever arrives”.[ ] That path does not depend on a message that might never be sent.
[ ] The exit test is guarded by a startup grace, so a normal launch cannot trip it.
[ ]
COMMAND_APP_STOPreleases the message and returns without blocking.[ ] Unknown message types are ignored and released, not treated as a clock.
[ ] Time-driven work reads
sys.getTimeMs()rather than counting wake-ups.[ ] The wait period is the longest that meets the requirement.
[ ] Sensor subscriptions held while resident are the cheapest that do the job, and are dropped when they are not needed.
[ ] State that must not be lost is persisted when it changes — a reboot gives no notice.
[ ] Allocation failure (
nullptr) is handled.[ ] The service does not return from
run()while its own GUI is loaded.[ ] State is published to a returning GUI on
COMMAND_APP_NOTIF_GUI_RUN.[ ] If the app should release itself when the user walks away, the GUI exits — being navigated away from does not close it.
12. See also
Alarm — autostart service, autonomous GUI launch
Timer — bounded wait, self-release, background fire
Stopwatch — residency tied to state, extrapolated reading
GlanceHR — a service with no GUI at all
sdk-setup.md —
APP_AUTOSTART,APP_TYPE, stack and RAM settingsSensorsLayer — subscribing, and what a subscription costs
CommandMessages.hpp— the lifecycle messagesIAppComm.hpp—getMessage()and its timeoutTimer.hpp—SDK::Timer, for grace periods and deadlinesIGuiLifeCycleCallback.hpp— GUI hooks