Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt

Use this file to discover all available pages before exploring further.

The phone↔watch protocol is defined in synheart-session/protos/session.proto, package synheart.session.v1.

Envelope

Every message on the wire is a SessionMessage with a oneof payload:
message SessionMessage {
  oneof payload {
    StartSession      start_session       = 1;   // phone → watch
    StopSession       stop_session        = 2;   // phone → watch
    GetStatus         get_status          = 3;   // phone → watch
    SessionStarted    session_started     = 10;  // watch → phone
    SessionFrame      session_frame       = 11;  // watch → phone
    SessionSummary    session_summary     = 12;  // watch → phone
    SessionError      session_error       = 13;  // watch → phone
    SessionStatus     session_status      = 14;  // watch → phone (response to GetStatus)
    BiosignalBatch    biosignal_batch     = 15;  // watch → phone (raw streaming opt-in)
  }
}
Field numbers 1–9 are reserved for phone→watch; 10+ for watch→phone.

Phone → watch

StartSession

message StartSession {
  string         session_id        = 1;
  Mode           mode              = 2;
  uint32         duration_sec      = 3;
  ComputeProfile profile           = 4;
  string         window_label      = 5;  // optional
  bool           enable_raw_stream = 6;  // default false
}

message ComputeProfile {
  uint32 window_sec        = 1;
  uint32 emit_interval_sec = 2;
}
The wire ComputeProfile carries only window_sec and emit_interval_sec. The Dart-side ComputeProfile.rawEmitIntervalSec is phone-side only — it doesn’t cross the wire, because the watch derives raw cadence from enable_raw_stream + its own buffer policy.
enable_raw_stream = true activates BiosignalBatch emission from watch to phone. Default is false — raw biosignals must not transmit unless explicitly enabled.

StopSession

message StopSession { string session_id = 1; }
Halts the watch engine. The watch responds with the final SessionSummary and transitions to TERMINATED.

GetStatus

Empty message. Watch responds with SessionStatus.

Watch → phone

SessionStarted

message SessionStarted {
  string session_id    = 1;
  uint64 started_at_ms = 2;
}
Fired once after the watch engine reaches ACTIVE (sensors hot, first sample admitted).

SessionFrame

message SessionFrame {
  string             session_id     = 1;
  uint64             seq            = 2;  // monotonic per session
  uint64             emitted_at_ms  = 3;
  SessionMetrics     metrics        = 4;
  BehaviorSnapshot   behavior       = 5;  // optional
}

SessionMetrics

message SessionMetrics {
  double hr_mean_bpm     = 1;
  double hr_sdnn_ms      = 2;
  double rmssd_ms        = 3;
  uint32 sample_count    = 4;
  uint64 start_ms        = 5;
  uint64 end_ms          = 6;

  optional double motion_rms_g          = 7;   // accel-equipped wearables
  optional uint32 motion_sample_count   = 8;
  optional double active_energy_kcal    = 9;   // iOS HKLiveWorkoutBuilder
}
The Dart SDK projects this into a flat Map<String, dynamic> on SessionFrame.metrics.

BehaviorSnapshot

message BehaviorSnapshot {
  double typing_cadence            = 1;
  double inter_key_latency         = 2;
  uint32 burst_length              = 3;
  double scroll_velocity           = 4;
  double scroll_acceleration       = 5;
  double scroll_jitter             = 6;
  double tap_rate                  = 7;
  uint32 app_switches_per_minute   = 8;
  double foreground_duration       = 9;
  double idle_gap_seconds          = 10;
  double stability_index           = 11;
  double fragmentation_index       = 12;
  uint64 timestamp                 = 13;
}
Mirrors the Dart BehaviorSnapshot field-for-field.

SessionSummary

message SessionSummary {
  string           session_id          = 1;
  uint32           duration_actual_sec = 2;
  SessionMetrics   metrics             = 3;
  BehaviorSnapshot behavior            = 4;  // optional
}
Fires once at session end.

SessionError

message SessionError {
  string    session_id = 1;
  ErrorCode code       = 2;
  string    message    = 3;
}
ErrorCode enum: ERROR_PERMISSION_DENIED, ERROR_SENSOR_UNAVAILABLE, ERROR_LOW_BATTERY, ERROR_OS_TERMINATED, ERROR_INVALID_STATE. See Errors.

SessionStatus

message SessionStatus {
  string       session_id = 1;
  SessionState state      = 2;
  bool         active     = 3;
  uint64       last_seq   = 4;
}
Response to GetStatus. The Dart-side status returned by getStatus() carries only (sessionId, active, lastSeq); state is currently dropped at the SDK boundary.

BiosignalBatch

message BiosignalSample {
  uint64     timestamp_ms = 1;
  SignalType type         = 2;   // SIGNAL_HR_BPM | SIGNAL_IBI_MS | SIGNAL_HRV_RMSSD
  float      value        = 3;
}

message BiosignalBatch {
  string                   session_id = 1;
  repeated BiosignalSample samples    = 2;
}
Sent as a batch to amortise transport cost. Per the streaming RFC: “Samples must be batched. No per-sample transport.”

Transport mapping

The proto is transport-agnostic; the SDK uses each platform’s native companion channel:
PlatformTransportNotes
iOS + watchOSWatchConnectivity.WCSessionsendMessage for live, transferUserInfo for queued.
Android + Wear OSWearable.MessageClient / DataClientSame pattern: MessageClient for live, DataClient for queued.
The Dart side sees both transports through a single Flutter MethodChannel("ai.synheart.session/methods") + EventChannel("ai.synheart.session/events") pair.

Channel surface

class SessionChannel {
  Future<void> startSession(SessionConfig config);
  Future<void> stopSession(String sessionId);
  Future<SessionStatus?> getStatus();
  Future<WatchStatus?> getWatchStatus();
  Stream<SessionEvent> get events;  // EventChannel-backed
}
getWatchStatus() returns the connectivity snapshot:
class WatchStatus {
  final bool supported;   // OS supports watch companion
  final bool reachable;   // watch is reachable now
  final bool paired;      // a watch is paired
  final bool installed;   // companion app is installed on the watch
}

Ordering guarantees

  • SessionStarted precedes any frames or summary for a given session_id.
  • seq is monotonic non-decreasing within a single session id; gaps may exist if the transport drops frames.
  • SessionSummary and SessionError are mutually exclusive — the watch emits one or the other to terminate.
  • BiosignalBatch events are not ordered relative to SessionFrame events; consumers must use timestamp_ms for chronological alignment.

Privacy posture

  • Default enable_raw_stream = false. Opt-in only.
  • When raw streaming is enabled, the producing HSI block must include raw_biosignals_allowed = true.
  • No PII fields cross the wire — session_id is a host-supplied identifier (default UUID v4), window_label is free-form but documented as no-PII.
  • LifecycleSessionState semantics.
  • ProvidersWatchBiosignalProvider and how it consumes this protocol.
  • ErrorsErrorCode taxonomy.