SessionEvent.toJson() (Dart), toMap() (Kotlin), and toDictionary() (Swift).
Phone-side implementations:
- Flutter: the plugin’s native relays (iOS and Android), used when
SynheartSessionhas aWatchBiosignalProvider. - Kotlin:
WatchSessionRelay(Wear OS). - Swift: none. A native iOS app sends the commands itself with
WCSession.
Transport
Notes:
- The Flutter plugin sends
start_sessiononly if the watch is reachable at that moment (iOS:WCSession.isReachable; Android: at least one connected node). - The Wearable Data Layer delivers messages only between apps with the same package name and signing certificate. A node can accept a message that no app answers; the Kotlin relay therefore gives up with a
sensor_unavailableerror if no event arrives within 10 seconds. - Neither side uses
transferUserInfoorDataClient.
Phone → watch
start_session
include_raw_samples and raw_emit_interval_sec are not sent; there is no raw-stream opt-in on the wire.
A Swift watch app can build a SessionConfig directly from this dictionary with SessionConfig(from:) (extra keys such as command are ignored).
stop_session
session_summary.
There is no status command. Watch status is read locally on the phone (WCSession properties on iOS, connected nodes on Android) — see getWatchStatus() (Flutter) and WatchSessionRelay.status() (Kotlin).
Watch → phone
Session events
Each message is one event map:
Example frame (the keys inside
metrics depend on the watch app):
metrics is a flat map. The phone SDKs pass it through without validating keys, so a watch app can add its own.
session_summary and session_error are terminal: the Kotlin relay’s flow completes after either, and the Flutter relays stop forwarding for that session.
Decoding differences:
- Kotlin decodes events with
SessionEvent.fromMap; an event that fails to decode is logged and dropped. An unknownerror_codebecomesINVALID_STATE. - Flutter decodes with
SessionEvent.fromJson. Itssession_errordecoder reads the code fromcode, noterror_code, and itsbiosignal_framedecoder reads samples withtimestamp_ms,bpm,rr_interval_ms,accelerometer, andspo2(the Kotlin and Swift engines writerr_intervals_msas a list, plussourceanddevice_id). - In Flutter, decoded events feed
WatchBiosignalProvideras heart-rate input; they are not passed to the host stream. See Lifecycle.
Heart-rate samples (Wear OS)
On/synheart/session/hr_sample, one reading per message:
WatchSessionRelay.hrSamples() (WatchHrSample(timestampMs, bpm)), independent of any session flow. A missing ts_ms is replaced with the phone’s current time.
Flutter channel surface
The Dart layer talks to the native relays throughMethodChannel("ai.synheart.session/methods") and EventChannel("ai.synheart.session/events"):
getWatchStatus() returns the connectivity snapshot:
Ordering
session_startedprecedes frames and the summary for a givensession_id.seqincreases within a session; gaps can appear if the transport drops a message.session_summaryandsession_errorare mutually exclusive.- Use
emitted_at_msand sample timestamps for chronological alignment rather than arrival order.
Privacy posture
- Raw samples are not requested over the wire.
- No PII fields are defined —
session_idis a host-supplied identifier (Dart defaults to a UUID v4), andwindow_labelis free-form; keep it free of PII.