Skip to main content

Architecture Overview

Overview

ZSWatch is built on an event-driven architecture powered by Zephyr's zbus publish/subscribe messaging system. Rather than tight coupling between modules, sensors, BLE services, managers, and applications communicate through named channels. Each module publishes data to a channel and any number of listeners can subscribe, making the system modular and easy to extend.

The main architectural layers are:

  • Sensors - read hardware and publish data to zbus channels
  • BLE services - receive data from the phone and publish to zbus; subscribe to channels to send data back
  • Managers - orchestrate system behavior (app lifecycle, power states, notifications)
  • Applications - subscribe to relevant channels and render UI via LVGL

System Block Diagram

Click the diagram (or open the zoomable view) to pan and zoom.

Event System (Zbus)

ZSWatch uses Zephyr's zbus for all inter-module communication. Modules publish typed structs to named channels, and any number of listeners or subscribers receive them asynchronously.

Key rules:

  • Listeners run in the publisher's context (often a BLE or sensor thread with limited stack). Always use k_work_submit() for non-trivial processing.
  • Channels are typed: each carries a specific struct (e.g., struct accel_data_event).
  • Decoupled: publishers don't know who listens; listeners don't know who publishes.

Channel Reference

ChannelDataDirectionDescription
accel_data_chanGesture/accel eventsSensor → AppsIMU interrupt-driven events (tap, tilt, step)
activity_state_data_chanPower state enumPower Mgr → App MgrACTIVE / INACTIVE / NOT_WORN_STATIONARY
battery_sample_data_chanmV, %, temp, chargingSensor → AppsBattery status updates
ble_comm_data_chanTyped BLE payloadBLE → AppsNotifications, music info, weather, time, etc.
environment_data_chanTemp, humidity, pressure, IAQSensor → AppsEnvironmental sensor readings
light_data_chanLux valueSensor → AppsAmbient light level
magnetometer_data_chanx/y/z componentsSensor → AppsMagnetometer heading data
music_control_data_chanPlay/pause/next/prevApp → BLEMusic UI commands sent to phone
pressure_data_chanPressure, temperatureSensor → AppsBarometric pressure readings
periodic_event_100ms_chanTickTimer → Subscribers100 ms periodic event
periodic_event_1s_chanTickTimer → Subscribers1-second periodic event
periodic_event_10s_chanTickTimer → Subscribers10-second periodic event
zsw_notification_mgr_chanNotificationNotif Mgr → AppsNew notification received
zsw_notification_mgr_remove_chanNotification IDNotif Mgr → AppsNotification dismissed/removed

Periodic Events

The system provides three shared periodic timer channels: 100 ms, 1 s, and 10 s. These drive most recurring work: sensor polling, UI refresh, battery sampling, etc.

Periodic channels use a lazy start/stop mechanism: the timer only runs when at least one observer is registered. Apps subscribe when they start and unsubscribe when they stop, so idle apps don't waste power.

// Subscribe to 1-second ticks (e.g., in app start_func)
zsw_periodic_chan_add_obs(&periodic_event_1s_chan, &my_listener);

// Unsubscribe (e.g., in app stop_func)
zsw_periodic_chan_rm_obs(&periodic_event_1s_chan, &my_listener);

Typical usage: a watchface subscribes to periodic_event_1s_chan to update the clock, and to periodic_event_10s_chan for refreshing weather or battery status.

Sensor Data Flow

The sensor data pipeline is straightforward:

  1. A periodic event (1 s or 10 s) fires
  2. Sensor drivers read hardware and publish to their zbus channel
  3. Apps/watchfaces that have subscribed receive the data in their listener callback
  4. The listener schedules a work item to update the UI on the main thread
Periodic Timer → Sensor Driver → zbus channel → App Listener → k_work → UI Update

Sensor modules (IMU, magnetometer, pressure, environment, light) are abstracted behind simple APIs in app/src/sensors/. The IMU is an exception: it is primarily interrupt-driven (gestures, step events) rather than polled.

BLE Communication

ZSWatch supports two phone platforms through different BLE profiles:

PlatformProtocolServices
AndroidGadgetBridge JSONNotifications, music, weather, time, calls, navigation
iOSApple ANCS + AMS + CTSNotifications (ANCS), media control (AMS), time (CTS)

Incoming BLE data is parsed and published to ble_comm_data_chan with a type field. Consumers filter by type:

  • Notification Manager - handles notification types, stores up to 10
  • Music app - handles music info/state types
  • Watchface - handles weather and time types

Outbound commands (e.g., music play/pause) flow in reverse: the app publishes to music_control_data_chan, and the BLE module picks it up and sends it to the phone.

Power Management

The power manager tracks user activity and drives the display and app lifecycle through three states:

StateDisplayAppsDescription
ACTIVEOnUI_VISIBLEUser is interacting with the watch
INACTIVEOffUI_HIDDENIdle timeout elapsed; screen off to save power
NOT_WORN_STATIONARYOffUI_HIDDENWatch not being worn; deeper power saving

State changes are published to activity_state_data_chan. The App Manager reacts by calling each app's ui_unavailable_func / ui_available_func callbacks, so apps can pause UI updates when the screen is off.

tip

Always guard UI updates with a state check:

if (app.current_state == ZSW_APP_STATE_UI_VISIBLE) {
// Safe to update LVGL objects
}

For details on building apps that use this architecture, see the Writing Apps guide.