Building a Wave App
A Wave App is a small QML application that runs directly on a Spirit System Wave radio's touchscreen. It's written and packaged with Wave Studio's Developer tab, then deployed straight to a radio on your network. This guide covers the project layout, how an app gets loaded, and how to call into the radio's firmware from your own QML.
Overview
A Wave App is a single QML component (plus, optionally, extra QML files and images) that gets pushed onto the radio's own screen, in the same window as the built-in Spirit System UI. That's what makes it powerful: your app isn't sandboxed behind a narrow plugin API โ it has direct access to the exact same signals the built-in menus use to read telemetry, change model settings, trigger haptics, play sounds, and more. The full list is in the API Reference.
Wave Studio's Developer tab can scaffold two different kinds of project โ an ordinary Application, launched on demand from the app drawer, or a Home Screen, which replaces the radio's default home screen outright. See Project types below.
With that power comes responsibility: a handful of signals are destructive or safety-relevant (firmware flashing, factory reset, powering the unit off, changing RF channel/power). Those are called out individually in the reference and summarized in Signals to be careful with below.
How apps run
The radio's whole UI is one continuous QML scope, Spirit UI, rooted at an
ApplicationWindow that owns a StackView filling the screen. Every
built-in screen (model settings, Wi-Fi, the app drawer, โฆ) is just a QML file pushed onto
that same stack.
When you launch an app โ from the on-device app drawer, or from Wave Studio's Play
button โ the firmware pushes your app.qml onto that same stack, inside
that same window. It is not a separate window, a separate QML engine, or a sandboxed
context. Because of that, plain unqualified names inside your app.qml โ properties
like font_size_20pt, or signals like signalVibePulse(...) โ resolve
straight up the QML scope chain to the Spirit UI root, exactly as if
you'd written them inside the built-in UI itself.
Your app.qml is a page on the radio's own stack, not a separate app. Anything the root window exposes as a property or a signal, you can read or call directly, with no import and no prefix.
Project types
Create Project first asks you to pick a project type โ two big buttons, same
style as the Log Viewer/Replay welcome screens. The type you pick controls which template
files get scaffolded, and how Play packages and uploads the project later; it's
derived from whichever entry file a project's src/ actually contains
(app.qml vs screen.qml), not stored separately.
A screen the pilot opens on demand from the app drawer, alongside the built-in menus. Most Wave Apps are this type.
Replaces the radio's default home screen outright, for the model currently selected on the radio. See Home Screen apps.
Application project structure
Scaffolded with exactly three required files, shown in the Developer tab's Project Files list:
Your app's entry point โ the QML component that gets pushed onto the radio's stack. Edited in Wave Studio's built-in syntax-highlighted editor.
Exactly 64ร64 pixels. Shown in the on-device app drawer and in Wave Studio's Applications tab. Replaceable from the icon.png preview page.
Your app's version, edited as X.Y.Z (each part 0โ255) โ
stored on disk in the radio's own space-separated format.
You can add more files: extra .qml files (via the toolbar's New File
button) for multi-screen apps, or extra .png/.jpg images (via
Add Image) for backgrounds, sprites, or icons your QML references. Every file
you add โ other than the three required ones โ can be removed again with a right-click.
A file with unsaved changes shows a trailing * in the list until you save it
(Ctrl+S, or just press Play).
Home Screen project structure
Scaffolded with a single required file โ no icon.png, no version:
The whole home screen โ loaded in place of the radio's built-in
BaseView.qml. Can't be removed, since a Home Screen project has nothing to
fall back to without it.
There's no launcher icon or version file because a Home Screen isn't listed anywhere the way
an installed Application is โ it has no app-drawer entry and no per-model settings file, so
there's nothing for either to attach to. You can still add extra .qml files and
images the same way as an Application project.
Your first app
This is exactly what Create Project generates โ a centered label inside a themed frame:
import QtQuick 2.12
import QtQuick.Controls 2.12
import "qrc:/qml/Item/Frame"
import "qrc:/qml/Item/Window"
import "qrc:/qml/Item/Button"
Item {
property string name: "Wave App Template"
GroupFrame {
id: frame
x: 30
y: 30
width: 740
height: 270
}
Label {
anchors.centerIn: frame
font.pixelSize: font_size_20pt
text: "Hello World!"
color: "white"
}
}
Note font_size_20pt on the label โ that's a plain root property from
Spirit UI, read with no prefix at all. The same applies to every theme
property (theme_font, theme_frameopacity, โฆ) and every signal in the
API Reference.
Calling signals
Every entry in the API Reference is a signal declared on the root window and wired, in the firmware, straight to a slot that actually does the work (sends a protocol command, writes a config value, starts a background thread, โฆ). From your QML you call it exactly like a function:
MenuButton {
text: "Buzz"
onClicked: {
signalVibePulse(150) // pulse the haptic motor for 150ms
}
}
Some signals report their result back asynchronously through a matching handler or property
on the root window rather than a return value โ for example signalModelGet(id)
answers via onModelCur: { ... }, and signalStatsGet() answers via the
statsCtx property. Where that applies, it's noted in the signal's entry in the
reference.
Home Screen apps
A Home Screen project replaces what the pilot sees the moment the radio isn't showing a menu
or an app โ normally the built-in BaseView.qml. This is exactly what
screen.qml looks like fresh out of Create Project:
import QtQuick 2.12
import QtQuick.Controls 2.12
import "qrc:/qml/Item"
import "qrc:/qml/Item/Label"
import "qrc:/qml/Item/Window"
Item {
property string name: "My Home Menu"
Loader {
anchors.left: parent.left
anchors.leftMargin: 25
y: 30
sourceComponent: ModelWindow {
enabled: !system_lock
}
}
Loader {
anchors.right: parent.right
anchors.rightMargin: 25
y: 30
sourceComponent: Component {
Widget {
position: 0
}
}
}
}
Just like an Application's app.qml, screen.qml is loaded straight
into the Spirit UI root scope โ every root property and signal in the
API Reference and every ready-made
component is available unqualified, exactly as shown in Your first
app. The difference is where it lands: it becomes the bottom (initial) item of
the same shared StackView every app screen is pushed onto, rather than a page the
pilot navigates to on top of it โ so it should stay out of the way of taps meant for the
model/telemetry windows and side panel it's showing, and respect system_lock
(true while the pilot has the screen deliberately locked) the way the template does.
The root currentHomePath property always holds whichever path is currently
loaded there โ your own home screen's path when it's active, or
/qml/Base/BaseView.qml when it isn't. If your app ever needs to navigate back to
the home screen explicitly rather than just popping the stack, push
currentHomePath (falling back to the built-in path when it's empty) instead of
hardcoding either one โ that way it still lands on a custom Home Screen if the current model
has one, exactly as the built-in language/country-change dialogs do.
Press Play and Wave Studio tars screen.qml under a literal
top-level base/ directory (not your project's name) and uploads it as
<name>.swm. The radio removes whatever Home Screen override the currently
selected model already had, extracts the new one in its place, and picks it up the next time
it loads the home screen โ there's no separate execute step, unlike an Application.
To go back to the built-in home screen, open a Home Screen project and use the toolbar's Delete Home Screen button (only shown while one is open and a radio is connected). It removes the override for whichever model is currently selected on the radio itself โ not necessarily the model your project happens to target โ so it's meant as "stop customizing the home screen for this model" rather than an undo tied to a specific upload.
Ready-made components
The three imports in the template pull in a small set of themed building blocks used throughout the built-in UI, so your app can match it without redrawing chrome from scratch:
GroupFrame, DlgFrame โ themed panel backgrounds.
TitleWindow, DlgWindow, OptWindow, ModelWindow, TelemWindow โ titled containers and dialogs.
MenuButton, FeatureButton, DlgButton โ themed buttons.
These all reference the same root theme properties (theme_frameopacity,
theme_font, โฆ) your own QML can use directly, so a custom control you write will
follow the radio's current color theme automatically.
There's more where those came from โ Item/CheckBox, Item/Icon,
Item/Adjuster, and Item/Curve cover toggles, tappable icons, a
value-picker-backed numeric control, and the channel-curve display used throughout the model
menus. The full Component Reference lists every property and
signal for all eight categories, including which signals you handle yourself (most of them)
versus which ones the root window answers for you.
Building & deploying
- Create Project in Wave Studio's Developer tab โ pick Application or Home Screen, then a name (16 characters max). It scaffolds the required files for that type from the templates above.
- Edit your entry file (and any extra files/images you add) in the built-in editor. Save anytime with Ctrl+S.
- Press Play. Wave Studio saves the current file and packages everything
under
src/into a tar archive. For an Application, that's uploaded as<name>.appand executed immediately; for a Home Screen, it's uploaded as<name>.swmand takes over the currently selected model's home screen โ see Home Screen apps. - An Application now also shows up in the on-device app drawer and in Wave Studio's Applications tab, where it can be re-launched or uninstalled later. A Home Screen has no separate listing โ it's simply what the radio shows outside of menus and apps, until you replace it again or use Delete Home Screen.
Signals to be careful with
Because your app runs with the same access as the built-in UI, a few signals can affect the physical device in ways a normal app should never trigger without very deliberate, explicit user confirmation. They're flagged individually in the reference; the highest-risk ones are:
signalPowerOff() and signalUIRestart() interrupt whatever the pilot
is doing, mid-use. signalRFTest() disrupts the live RF control link. A handful of
other firmware- and factory-level signals exist that are even more sensitive than these and are
deliberately left out of this reference entirely - if you find yourself reaching for something
that reflashes firmware, wipes stored data, or touches raw RF/production-test hardware, stop and
reconsider before calling it from an app.
See the API Reference for the full set of danger and caution badges, with the reasoning for each.
Wave