Start here · Call map · Pairing · API · Build · Evidence
The project's README from github.com/LordixDemon/apple_watch_connector. As one markdown file: README.md.
Android · macOS · Linux · Windows
Native protocols. Physical hardware. A shared Companion.
Bluetooth · Pairing · IDS · Native Faces · Flutter · Java · Rust
Start here · Call map · Pairing · API · Build · Evidence
Android Magisk module: Download Bridge 0.2.427 ZIP · Installation
Connect Apple Watch to Android, Linux and Windows through native Bluetooth/IDS protocols and a shared Flutter Companion. macOS uses the same interface with a limited Bluetooth backend.
This project reconstructs the path from discovery and pairing to activation, reconnection, preference exchange, and native watch-face management. Companion presents observed Watch state and submits commands on all four platforms. Android Bridge owns the phone connection and keys. Linux and Windows use a Rust desktop backend, native HCI brokers and the shared Java protocol engine; macOS uses Rust and CoreBluetooth. Linux and Windows pairing uses the six-digit Watch code; camera pairing is available on Android.
Release preparation now has a shared Java protocol-runtime module, isolated
research/debug probes, Linux artifact auditing and shared Android distribution
signing. See the release guide. Fresh pairing,
activation and same-pair encrypted reconnect are physically verified on Linux
and Windows with the tested hardware; desktop application service coverage
remains partial.
The Linux Rust backend separates observed state, worker I/O/lifetime and command routing. Stop/failure states reject late ready events; EOF and bounded cleanup prevent stale readiness and abandoned workers. Portable Java modules share strict dependency locks. Linux packages include a verified schema-2 inventory and use deterministic archive metadata. See stage 93. Linux now exposes Bluetooth adapter selection in All Watches, remembers the address across HCI index changes and requires reselection when saved hardware is missing. See adapter selection.
Documentation snapshot: October 13, 2026. The main verified hardware is Apple Watch Ultra 2,
Watch7,5, watchOS26.2 / 23S303. Source versions: Bridge 0.2.429 / code 629, Companion 1.0.86+87, Rust workspace 0.1.1. Source versions do not establish installed device versions. Android camera pairing, Linux pairing on Mint 22.3 and Windows pairing on Windows 10 / Realtek0bda:b00ehave separate hardware verification.
Development is ongoing. The hardware journal confirms camera pairing, activation, and a visible Watch face. Full Watch.app parity, every iPhone feature, and compatibility with every Apple Watch model remain incomplete. macOS discovery is implemented; the complete physical pairing → activation → IDS path is unfinished.
This table distinguishes source implementation from retained hardware evidence. Consult the feature registry and the acceptance plan for details. Older entries can describe stages that have since been superseded.
Release status: experimental RC. See the platform support matrix and tested versions and changelog. Application services listed below apply to Android unless explicitly stated otherwise. Linux/Windows currently expose connection/setup controls; macOS exposes BLE discovery/link. The UI's Available Features panel reads declared backend support, independently of connection readiness.
| Area | Implemented path | Evidence and limits |
|---|---|---|
| Android discovery / pairing | Actual discovery, explicit selection, PIN and optical PSK paths | Complete camera pairing, activation, and visible face in the optical report; verified on the specific hardware |
| Activation / Setup | PBBridge, initial properties, Watch language, PairedSync, completion policy | Activation, UI completion, and operational readiness are separate gates. Completion report distinguishes native evidence and owner confirmation |
| Operational reconnect | Saved activated pair, encrypted link, IDS, separate foreground service | Connect does not replay pairing or Setup. Connected state and fresh battery were physically verified |
| Battery / About | Native observations and SystemSettings requests | Battery evidence; absent values remain unknown |
| Wi-Fi | Current phone network → native V2 ADD | Network reception and Watch join were confirmed in the hardware report; limited to the supported WPA2-PSK/CCMP network |
| Native faces | Inventory, select, duplicate, remove, reorder, update, add, resources, export | Native inventory is separate from local designs. Mutations require readback; family/preview coverage is evolving |
| Watch.app UI / previews | Native catalogs, options, palettes/shades, Photos, monograms, AppIntent metadata | Full visual parity and optimization remain open. Stage 90 swatch components are research output pending production UI integration |
| Settings | RIGHT_WRIST, INVERT_SCREEN, TIME_24_HOUR |
Exact NPS key whitelist; other settings screens do not establish native writes |
| Notifications | Bulletin transport, Android listener, revision-bound actions | Delivery and handlers exist; all apps, attachments, sound, and actual actions need their own acceptance checks |
| Find devices | Phone → Watch sound; Watch → Phone effects/result | Receipts and physical effects are distinct. Sound/flash depend on Android permissions and phone state |
| Health | Encrypted ingress, replay, peer/registry binding, decode, defaults mirror | Does not establish complete Health/Activity import, correct anchors, or all data types |
| Calls / SMS / contacts | Research codecs and some Flutter methods | Current Companion IPC does not implement triggerCall or callAction; full calls/audio are unverified |
| StateReplicator | Authenticated discovery, pinned QUIC, native framing, handshake | Complete native snapshot/file reception and durable persistence remain unfinished |
| macOS | Shared Flutter UI, Rust C ABI, CoreBluetooth discovery/BLE, portable pairing codecs | Complete physical pairing/IDS awaits the authorized bootstrap transport |
| Linux | Rust HCI broker + shared Java protocol + Flutter UI | Physical pairing, activation and reconnect verified on Mint 22.3; Wi-Fi credentials and application services remain incomplete |
| Windows | Native Rust USB HCI + shared Java protocol + Flutter UI; DPAPI storage | PIN, activation and encrypted reconnect physically verified on Windows 10 / Realtek 0bda:b00e / Watch7,5; other hardware and full application service coverage remain unverified |
.
├── apple-watch-companion/ Shared Flutter UI; Android IPC and desktop Rust FFI
├── apple-watch-bridge/
│ ├── core/ Java wire codecs, crypto, policies, mirrors, JVM tests
│ ├── protocol-runtime/ Shared controller/session/stdin runtime; no Android SDK
│ ├── hal/ Android root entry, Bluetooth HAL and file ownership adapters
│ └── app/ Android services, Keystore/SQLite, permissions, IPC
├── crates/
│ ├── watch-protocol/ Rust framing, FE25 advertisements, BT_CL normal handoff
│ ├── watch-pairing/ Portable uIKE control/PIN security; no OS I/O
│ ├── watch-core/ Sessions, observations, transport interface
│ ├── watch-transport-macos/ CoreBluetooth ownership and callbacks
│ ├── watch-transport-linux/ Desktop worker, Linux HCI and Windows USB HCI brokers
│ ├── watch-transport-windows/ WinRT discovery/BLE diagnostic transport
│ ├── watch-core-ffi/ Versioned C ABI for Flutter desktop
│ └── watch-replicator-quic/ Socket-free pinned QUIC engine and Android JNI
├── src/ Compatibility facade and USB framing utilities
├── tools/ Builds, native captures, asset builders, verification
├── linux-watch-host/ Linux protocol host
├── windows-watch-host/ Windows protocol host and DPAPI storage
└── docs/ Architecture, protocol documentation and verification
Build boundaries are defined in Gradle settings and the Cargo workspace. artifacts/, target/, build/, and historical source-before/ trees contain many files outside the current application sources.
flowchart LR
User[User] --> UI[Flutter Companion]
UI --> Facade[WatchBridgeService / BridgeTransport]
Facade --> Android[PlatformBridgeTransport]
Android --> Channels[CompanionFlutterBridge]
Channels --> Binder[BridgeIpcClient / Messenger]
Binder --> IPC[CompanionBridgeService]
IPC --> Setup[BridgeSetupService / Engine]
IPC --> Op[OperationalWatchService]
Setup --> Root[RootBluetoothHalHost]
Op --> Root
Root --> HAL[HalTransportSession]
HAL --> Core[Java core: codecs / coordinators]
HAL <--> Watch[Apple Watch]
Facade --> DesktopUI[RustBridgeTransport]
DesktopUI --> ABI[watch-core-ffi]
ABI --> Rust[watch-core / watch-pairing / watch-protocol]
ABI --> Desktop[watch-transport-linux / desktop backend]
Desktop --> Worker[LinuxProtocolHost / WindowsProtocolHost]
Worker --> HAL
HAL <--> Broker[Linux HCI USER / Windows USB HCI]
Broker <--> Watch
Rust --> CB[watch-transport-macos / CoreBluetooth]
CB -. unfinished pairing bootstrap .-> Watch
HAL --> JNI[watch-replicator-quic: JNI]
One Android Bridge APK contains app, hal, protocol-runtime, core, and the QUIC .so. Companion is a separate APK. The Java package dev.applewatchandroid.bridge remains shared across modules, with compile dependencies app → hal → protocol-runtime → core and app → core. Linux and Windows workers reuse protocol-runtime and core with platform controller and storage adapters. Rust owns desktop worker lifetime and public state; Flutter accesses it through the C ABI.
Flutter owns the user connection flow. The Bridge launcher redirects into Companion. Closing a screen leaves the operational foreground service running. One owner holds the HAL; one serial transport state machine orders packets.
Details: Bridge architecture, Companion architecture, macOS Rust core.
These maps cover implemented cross-layer transitions and significant calls. Arrows show work direction; some transitions are asynchronous queues. Source conditions and runtime evidence determine whether a particular branch executes.
sequenceDiagram
actor Owner as User
participant UI as Flutter screen / controller
participant Dart as BridgeCommands
participant Channel as PlatformBridgeTransport
participant Native as CompanionFlutterBridge / BridgeIpcClient
participant IPC as CompanionBridgeService
participant Op as OperationalWatchService
participant HAL as HalCommandReader / HalTransportSession
participant IDS as NormalLinkIdsSessionBridge
participant Watch as Watch service
Owner->>UI: Select action
UI->>Dart: Typed method + baseline/target
Dart->>Channel: invoke(method, args)
Channel->>Native: MethodChannel
Native->>IPC: Messenger COMMAND=3, version=1, requestId
IPC->>IPC: Signature, payload, pair/epoch, policy checks
IPC->>Op: sendCommand(command, requestId)
Op-->>IPC: Accepted into local queue?
IPC-->>UI: QUEUED / REJECTED / UNAVAILABLE
Op->>HAL: BRIDGE_COMMAND_V1 + encoded request
HAL->>IDS: sendApplicationProtobuf / sendApplicationData
IDS->>Watch: Authenticated application message
Watch-->>IDS: ACK / response / later observation
IDS-->>HAL: SessionEvent
HAL-->>Op: Typed status / native observation
Op-->>IPC: BridgeIpcDispatcher + public state
IPC-->>Native: STATE=102
Native-->>UI: EventChannel → decoded observation
Pairing/activation use BridgeSetupService. Large .watchface packages pass through NativeFaceTransferDispatcher before the mutation command. Binding Companion to IPC does not start HAL or create a pair.
Watch packet
→ Bluetooth HAL callback / HCI ACL
→ HalTransportSession
→ NormalLinkIdsSessionBridge.accept…
→ normal link: ERTM / NetworkRelay / authenticated ESP
→ IDS: IPv6 / TCP / NWSC / control or application stream
→ IdsModernSessionCoordinator.SessionEvent
→ handleIncomingProtobufEvent / feature receiver / typed observation
→ root stdout IPC → OperationalWatchService
→ BridgeIpcDispatcher + CompanionSessionState
→ CompanionBridgeService.state() / STATE=102
→ BridgeIpcClient → BridgeStateProjection
→ EventChannel → WatchBridgeService / BridgeObservation
→ WatchObservationController / WatchConnectionProvider / feature controller
→ Provider → widgets
A publication is decoded as a whole before its projections are emitted. Transport loss or pair changes invalidate connected/native state. Late replies from an earlier epoch must not restore stale state.
| Value / event | Meaning |
|---|---|
QUEUED |
Bridge accepted the request into its local queue |
HAL_ACCEPTED |
Root transport accepted the structured request |
IDS_QUEUED |
IDS allocated the message, correlated by message UUID |
APP_ACK_RECEIVED |
The matching application acknowledgment arrived |
APP_RESPONSE_RECEIVED |
The matching application response arrived; its meaning is protocol-specific |
NATIVE_FACE_APPLIED |
Complete correlated Watch readback confirmed the face transaction |
APPLIED |
A value accepted by the Flutter confirmed wrapper; cannot be inferred from queue/ACK |
EXPIRED, FAILED, REJECTED, UNKNOWN |
Distinct incomplete outcomes; UNKNOWN does not rule out a partial effect |
OBSERVED |
Public-state read response; individual field freshness still matters |
The stage enum is defined in BridgeCommandCodec. This is not one universal linear pipeline: requests have different stages, and a persisted snapshot can be old.
BLE advertisement / FE25 setup metadata
→ explicitly selected fresh Watch candidate
→ HCI LE connection
→ L2CAP
→ BT_CL fixed signaling CID 0x003a
→ VERSION / services / CREATE / ACCEPT
→ dynamic pairing pipe
→ uIKE security + PIN/optical authentication
→ OOB / LE Secure Connections SMP / durable Bluetooth bond
→ encrypted normal-link registration
Application protobuf / binary plist / resource / native face payload
↕ IDS application stream + ServiceMap topic
↕ NWSC service connector + TCP + inner IPv6
↕ authenticated ESP / Class-C or Class-D IPsec
↕ NetworkRelay
↕ L2CAP ERTM on negotiated dynamic channels
↕ HCI ACL
↕ Android Bluetooth HAL / controller
↕ BLE link
NormalLinkIdsSessionBridge defines the boundary: normal link owns IKE/ESP/NetworkRelay/ERTM; IDS owns decrypted IPv6/TCP, NWSC, and control/data streams. IKE establishes SAs; ordinary application payloads travel through ESP.
A topic is not a separate TCP connection. Services sharing priority and protection class use the same canonical connector. Class-C/D identify protected routes, not separate apps or radio channels.
Companion calls beginPairing or beginOpticalPairing. CompanionConnectionPolicy checks Bluetooth permission, known saved identity, and exclusive ownership. A normal begin does not replace a saved pair. Explicit replacePairOptically requires the current saved pair UUID and a verified encrypted replacement backup.
BridgeSetupService / BridgeSetupEngine → RootBluetoothHalHost → HalTransportSession.initialize() → runTransportHandshake().
The root host validates invocation and acquires the exclusive HAL lease. Transport configures the controller, receives stable local identity from the app, and sets ACL buffer/credits and event masks. MainActivity does not own this process.
Scanning yields advertisements. Valid setup metadata identifies product/watchOS/candidate information. WatchDiscoverySelection publishes bounded temporary tokens. Companion submits selectDiscoveredWatch, and freshness is rechecked before use. Manufacturer data alone does not establish pairability.
After LE connection, BT_CL negotiates version/services/CREATE/ACCEPT. Dynamic CIDs come from negotiation, not constants. In HAL this reaches probeControlSaInit().
Calls in HalTransportSession:
runTransportHandshake()
→ probeControlSaInit()
→ probeAdditionalKeyExchange()
→ probeControlIkeAuth()
→ selected authentication branch
These negotiate security and validate authenticated payloads. They do not yet establish a durable bond, IDS readiness, or activation.
flowchart TD
Control[Authenticated control transport] --> Choice{Pairing method}
Choice --> PIN[probePinAuthMethod / awaitWatchPin]
PIN --> PPK[SPAKE2+ / PPK pairing path]
PPK --> Pair[probePairingSession]
Choice --> Camera[Camera → native optical reader]
Camera --> Handle[Single-use opaque handle in native Companion]
Handle --> Code[110-byte OpticalPairingCode]
Code --> Bind[Bind to fresh setup advertisement]
Bind --> PSK[probeOpticalPairing: PSK/1, authentication at MID2]
PSK --> Notify[exchangePairingPrivateNotifies]
Pair --> Notify
Notify --> SMP[completeLeSecureConnectionsSmp]
SMP --> Store[Encrypted durable pair / bond]
Store --> Normal[completeModernNormalRegistration]
Normal --> IDS[attachModernIdsSession]
PIN and optical pairing have different authentication paths and message counters. Optical code is bound to fresh setup advertisement data. Decoding an image does not authenticate a pair.
The Android camera reader runs in a separate native :optical process. Dart receives a public name and a temporary handle. Explicit Pair/Replace transfers native payload through signature-protected IPC. The handle is single-use; its documented lifetime is 120 seconds and request deadline 10 seconds.
The current spatial reader uses a local hash-pinned transitional 23G71 firmware backend generated at build time. An independent portable spatial detector remains unfinished. Arm64 is supported; other native ABIs reject initialization. Required binary inputs cannot be replaced with arbitrary files.
exchangePairingPrivateNotifies() leads to OOB/SMP and completeLeSecureConnectionsSmp(). The app seals records and returns persistence results to root. BOND-STORED / PAIRING-SESSION-STORED differ from merely requesting persistence; a storage failure cannot advance a successful commit.
completeModernNormalRegistration() → attachModernIdsSession() establishes the normal pipe and protected routes. NetworkRelay/IKE Class-D and Class-C precede IDS control Hello, service routing, and data lanes. Device-info and NanoRegistry property exchange follow.
Sources: optical result, typed setup state chain, PairingSessionRecord.
| Step | Owner / data | Transition condition |
|---|---|---|
| 1. IDS bootstrap | IdsModernSessionCoordinator, IdsBootstrapState, device info |
Authenticated identity and live control/data lanes |
| 2. Initial registry | AppleWatchInitialSetupIdsAdapter, NanoRegistry properties |
Required properties received and identity reconciled |
| 3. Language / locale | WatchLocaleSnapshot, preference archive |
Authenticated Watch language/locale only; no Android-locale fallback |
4. Commit isPaired |
Initial setup coordinator and durable record | Required checkpoints/barriers valid |
| 5. Activation | HalPostCommitOrchestrator, ActivationProxyWorker, PBBridge |
Actual activation result; owner input travels separately |
| 6. Normal / initial sync prep | PBBridge 36, then 21 / response 18 | Correlated response; not Setup completion |
| 7. Wi-Fi | InitialWifiSyncWorker |
Supported network, same pair/generation, live setup Class-C lane |
| 8. PairedSync | NPS domain, active/completed sync state | Native observer/saga conditions, not progress wording alone |
| 9. Exit Setup | Buddy / Carousel and completion policy | Separate native evidence or explicit owner confirmation of a visible face |
| 10. Normal reconnect | Operational policy | Same eligible saved pair; setup transport safely stopped |
flowchart TD
Activation[Activation] --> PB36[PBBridge 36: Normal / notifyActivationCompleted]
PB36 --> PB21[PBBridge 21: PrepareInitialSync]
PB21 --> PB18[PBBridge 18: correlated response]
Scheduler[Phone PairedSync scheduler] --> NPS[NPS type 0: com.apple.pairedsync]
NPS --> Prefs[CFPreferences / synchronize]
Prefs --> Note[PSYWatchSyncStateChangedNotification]
Note --> UIObserver[PSYSyncStateObserver]
Note --> SagaObserver[PSYSyncClientStateObserver]
UIObserver --> Finished[BuddyFinished.syncDidComplete]
SagaObserver --> Saga[NanoRegistry PairedSync saga]
Finished --> Exit[exitCriteria / haptic / Start or Crown]
Exit --> Dismiss[completeAndDismiss / suspendApp / buddyDone]
Dismiss --> Clock[Carousel / watch face]
Saga --> IDSComplete[Local IDS sync completion]
There are two independent observer branches. PBBridge 4 activation, response 18, AppACK, and sent progress do not independently establish visible Clock. Exact handlers and build distinctions (23S303, 23U67, 23G71) are retained in SETUP_CHAIN.md.
SetupProgressState.Phase defines:
IDLE, STARTING, DISCOVERING, CONNECTING, PIN_REQUIRED, SECURITY, IDS,
REGISTRY, CONFIGURING, ACTIVATING, ACTIVATION_INPUT, ACTIVATED, SYNCING,
WAITING_FOR_WATCH, VERIFYING_RECONNECT, VERIFIED, FAILED, STOPPING, STOPPED
This is projection vocabulary, not one mandatory order for every scenario. BridgeSetupEngine prevents regression and clears late owner-input fields.
PairingSessionRecord preserves durable wire numbering:
| Value | Checkpoint | Value | Checkpoint |
|---|---|---|---|
| 1 | PAIRING_MATERIAL_PERSISTED |
10 | INITIAL_PROPERTIES_RECEIVED |
| 2 | SMP_BONDED_RAW |
11 | READY_TO_COMMIT_IS_PAIRED |
| 3 | STOCK_BOND_IMPORTED |
12 | IS_PAIRED_COMMITTED |
| 4 | STOCK_ENCRYPTED_RECONNECT |
13 | ACTIVATION_CONFIRMED |
| 5 | NETWORK_RELAY_PRELUDE_NEGOTIATED |
14 | IS_SETUP_CONFIRMED |
| 6 | CLASS_D_ESTABLISHED |
15 | PAIRED_SYNC_COMPLETE |
| 7 | CLASS_C_ESTABLISHED |
16 | SETUP_COMPLETE |
| 8 | IDS_CONTROL_READY |
17 | CLOCK_VISIBLE_CONFIRMED |
| 9 | IDS_DATA_READY |
18 | OPERATIONAL_HEALTH_CONFIRMED |
Flags, evidence, and identity are checked as well. A high ordinal alone does not admit operational mode. Owner confirmation is stored separately from native evidence. Unobserved legacy terminal records are not automatically eligible for normal reconnect.
sequenceDiagram
participant UI as Companion
participant Policy as CompanionConnectionPolicy
participant Op as OperationalWatchService
participant Store as Encrypted pair / bond store
participant HAL as Root host / HalTransportSession
participant Watch as Watch
UI->>Policy: connectWatch(pairId)
Policy->>Policy: Saved eligible matching pair, idle owner
Policy->>Op: START_OPERATIONAL
Op->>Store: Load and validate existing records
Op->>HAL: Start with operational pairing identity
HAL->>HAL: requireMatchingActivatedPair
HAL->>Watch: Reconnect + encryption + normal registration
HAL->>Watch: NetworkRelay / Class-C,D / IDS bootstrap
Watch-->>HAL: Authenticated Hello / state
HAL-->>Op: Ready epoch + observations
Op-->>UI: Connected + current native values
Note over Op,Watch: Setup / Albert / Buddy replay disabled
OperationalWatchService owns root lifetime, readiness epoch, the bounded command queue, and reconnect. Feature controllers receive OperationalSessionAccess; they cannot start a second root owner or replace the pair.
When retries are allowed, failure-count delays are 15 → 30 → 60 seconds. IDS policy rejection or incomplete paired commit may prohibit reconnect. A timer does not override these conditions.
Disconnect cleans up and releases ownership. A second flow cannot acquire HAL during STOPPING. A new reconnect has a new transport epoch; earlier commands are invalidated.
This registry lists the current Android IPC handlers and separate local adapter methods. Arguments are summarized; exact validation/size rules live in the linked implementation. This is an internal API between two signed applications.
| Layer | Contract |
|---|---|
| Flutter commands | MethodChannel dev.applewatchandroid.companion/bridge |
| Flutter observations | EventChannel dev.applewatchandroid.companion/events |
| Native service | dev.applewatchandroid.bridge.CompanionBridgeService |
| Permission | dev.applewatchandroid.bridge.permission.COMPANION_IPC, signature protection |
| Messenger request | 1 = SUBSCRIBE, 2 = GET_STATE, 3 = COMMAND |
| Messenger reply | 101 = RESPONSE, 102 = STATE |
| Envelope | version=1, canonical requestId UUID; commands include method and typed Bundle args |
| Admission | Companion package and matching signature; at most 4 subscribed clients |
| Companion client | At most 32 pending requests, 5-second deadlines, binding-death recovery |
| Root command | BRIDGE_COMMAND_V1: + Base64 binary request {id, epoch, deadlineMs, command} |
| Root status | BRIDGE_COMMAND_STATUS_V1: + Base64 typed status |
Local text/file adapters use separate …/text and …/face_files channels; these operations are not Watch IDS commands.
| Method | Path / condition |
|---|---|
connectWatch |
START_OPERATIONAL: same eligible saved pair |
disconnectWatch |
STOP_SESSION: setup or operational owner |
beginPairing |
START_SETUP: no saved pair |
beginOpticalPairing |
START_SETUP: native optical payload and no saved pair |
replacePairOptically |
Explicit replacement: matching saved pair, optical code, verified backup |
resumeSetup |
Matching saved pair not yet operationalEligible |
submitPin |
Forward to the current setup owner |
selectDiscoveredWatch |
Forward a temporary discovery token to setup |
activationResponse |
Forward a challenge response to setup |
confirmSetup |
Activated matching pair; safe owner-confirmed completion |
finishSetup |
Explicit REJECTED policy; legacy force-finish disabled |
auditStockBond |
Separate setup diagnostic flow for matching pair |
importStockBond |
Guarded stock bond import flow |
alignStockIdentity |
Guarded stock identity diagnostic flow |
probeStockReconnect |
Guarded stock reconnect probe |
Sources: connection policy, IPC service.
| Method | Arguments / command | Completion evidence |
|---|---|---|
pingWatch |
PING_WATCH → FindMyLocalDevice |
Correlated response; physical sound verified separately |
refreshWatchState |
REQUEST_DEVICE_ABOUT |
SystemSettings About response / telemetry |
refreshFaceCollection |
REQUEST_FACE_COLLECTION |
Complete accepted native collection snapshot |
syncWifi |
SYNC_CURRENT_WIFI |
V2 archive delivery; network join checked separately |
setActiveFace |
faceId → NATIVE_FACE_SELECT: |
Newly confirmed selected UUID |
duplicateNativeFace |
sourceFaceId; new UUID from requestId |
New face/archive in readback |
removeNativeFace |
faceId |
UUID absent from readback; final face cannot be removed |
reorderNativeFaces |
Exact permutation in faceIds |
Full matching order in readback |
updateNativeFace |
faceId, binary configuration |
Matching configuration in readback |
addNativeFace |
.watchface archive, up to 128 KiB inline |
New target UUID and archive in readback |
setNativeMonogram |
pairId, epoch, revision, text |
New paired monogram observation |
setNativePigmentVisibility |
Pair/epoch, timestamps, full baseline sets, changes |
Paired pigment mirror; queue/ACK insufficient |
setWatchSetting |
setting, boolean value |
Native NPS observation of the exact key |
sendNotification |
title, message, sectionId, sectionDisplayName |
Native bulletin path; card/action checked separately |
stopPhonePing |
Local Android effect stop | STOPPED on successful local stop |
The operational service checks readiness/policy; root validates the envelope again. Face operations also require known current inventory. Root rejects legacy SET_ACTIVE_FACE:: native collection protocol is required.
| Method | Behavior |
|---|---|
beginNativeFaceImport |
Creates pair/epoch upload with declared total; optional faceId/baselineHash for resources |
appendNativeFaceImport |
Sequential chunks with offset validation |
finishNativeFaceImport |
Validates/seals package, queues native add/resources command |
cancelNativeFaceImport |
Cancels an unfinished upload |
exportNativeFace |
Chunked saved native package export with offset/total/SHA-256 |
getState |
Returns client.observedState; does not request fresh Watch state |
requestConnectionPermissions |
Opens the Bridge permission screen for connection scope |
requestPhoneFlashPermission |
Opens the permission screen; OPENED does not mean granted |
startOpticalDecoder |
Starts native optical worker for declared width/height |
processOpticalFrame |
Supplies a bounded packed UV frame; not an IDS packet |
stopOpticalDecoder |
Closes reader/worker resources |
discardOpticalCandidate |
Discards the temporary native candidate handle |
Upload/export live in NativeFaceTransferDispatcher. The package store also checks limits/lifetime. replaceNativeFaceResources is a Dart upload wrapper, not a separate Bridge IPC method.
| Method | Actual behavior |
|---|---|
syncSettings |
Adapter error UNIMPLEMENTED; use supported per-key setWatchSetting |
unpairWatch |
Adapter error UNIMPLEMENTED; its Dart description does not implement a wipe |
triggerCall |
Adapter error UNIMPLEMENTED; telephony codecs/topics do not establish real call relay |
callAction |
Adapter error UNIMPLEMENTED; Android answer/decline flow is not connected |
The native Companion whitelist rejects these before sending Binder commands. A direct unknown method to Bridge also yields UNIMPLEMENTED. finishSetup remains an explicitly rejected legacy policy method, but the adapter does not forward it.
Sources: BridgeCommands, CompanionFlutterBridge. _invokeConfirmed() accepts only APPLIED, so QUEUED does not become confirmed success.
The table follows IdsApplicationRoute. alloy.X expands to com.apple.private.alloy.X. All listed application routes use urgent priority. A valid route does not establish complete feature support.
| Topic suffix | Class | Phone codec / consumer | Watch-side purpose |
|---|---|---|---|
alloy.bluetoothregistry |
D | NanoRegistryPropertyCodec, Class-D setup |
Registry properties / lifecycle |
alloy.bluetoothregistryclassc |
C | Initial/property registry path | Class-C registry exchange |
alloy.pbbridge |
C | PbBridgeCodec, post-commit coordinator |
Activation / Buddy / Setup |
alloy.preferencessync.pairedsync |
C | PairedSyncCodec, paired mirror, sync coordinator |
Paired preferences / sync state |
alloy.preferencessync |
D | PreferencesSyncCodec, NPS consumers |
Ordinary preferences |
alloy.bulletindistributor |
C | Bulletin transport, notification listener | Notifications / actions |
alloy.bulletindistributor.settings |
D | Bulletin settings | Notification policy |
alloy.findmylocaldevice |
D | FindMyLocalDeviceCodec, OperationalPhoneFinder |
Play sound / Find Phone |
alloy.health.sync.classc |
C | OperationalHealthController, encrypted codecs |
Health synchronization |
alloy.telephony |
C | Telephony research codecs | Telephony route; full native acceptance unproven |
alloy.timesync |
D | Timesync messages | Time exchange |
alloy.timezonesync |
D | Timezone messages | Timezone exchange |
alloy.clockface.sync |
D | ClockFaceSyncClient/Receiver, delta session |
NanoTimeKit native collection |
alloy.systemsettings |
D | NanoSystemSettingsDiagnostics, reboot codec |
About / native diagnostic operations |
alloy.wifi.networksync |
C | WifiNetworkSyncCodec, workers |
Network archive V2 ADD |
alloy.sysdiagnose |
C | SysdiagnoseArchiveInventory/Collection |
Diagnostic archive operations |
alloy.idscredentials |
D | IdsDeviceInfoExchange, commands 11/12 |
Mutual device credentials, separate from login/activation |
pairedsync has its own Class-C route; inheriting Class-D from ordinary preferencessync is incorrect. Unknown topics are rejected. StateReplicator over NetworkRelay/QUIC is a separate path below.
refreshFaceCollection
→ REQUEST_FACE_COLLECTION
→ HalTransportSession.drainOutboundAppMessages()
→ ClockFaceSyncClient.reserveHeader()
→ ClockFaceSyncHeaderCodec.fullRequest()
→ NormalLinkIdsSessionBridge.sendClockFaceCollectionRequest()
→ Watch clockface.sync
→ ClockFaceSyncReceiver: session, parts, archive/configuration validation
→ native collection / package persistence
→ publishClockFaceObservation()
→ IPC snapshot → NativeFaceCollection → UI
Header sequence/generation is persisted separately. Incomplete multipart data is not published as a complete library. Recovery includes a known unfinished session: this controls native synchronization, not deletion of user faces.
sequenceDiagram
participant UI as NativeFaceController / editor
participant Bridge as CompanionBridgeService
participant Plan as ClockFaceDeltaCommand / Plan
participant Tx as ClockFaceDeltaSession
participant Watch as Watch clockface.sync
participant Rx as ClockFaceSyncReceiver
UI->>Bridge: Command + committed face UUID / config
Bridge->>Plan: Validate inventory, target, payload, baseline
Plan->>Tx: Pair / epoch / watchPeer / session
Tx->>Watch: START
Watch-->>Tx: Correlated START response
loop Change batches
Tx->>Watch: BATCH(index, changes)
Watch-->>Tx: Matching session/index response
end
Tx->>Watch: END
Watch-->>Tx: END accepted / rollback / error
Tx->>Watch: Request complete readback
Watch-->>Rx: Complete native collection
Rx->>Tx: Observe full correlated result
alt Expected result present
Tx-->>UI: APPLIED / NATIVE_FACE_APPLIED
else Timeout / disconnect / ambiguous partial result
Tx-->>UI: UNKNOWN or REJECTED
end
Stages are START_READY → START_WAIT → BATCH_READY → BATCH_WAIT → END_READY → END_WAIT → READBACK_WAIT → APPLIED, with terminal REJECTED / UNKNOWN. Readback timeout is 180 seconds. Replies must match topic, message identifier, session, Watch peer, and batch index.
A batch/end error can follow a partial write. The state machine does not automatically replay mutations; inspect a fresh observation first. Sources: delta session, plan, command codec.
.watchface, Photos, and resourcesNative archive → validate structure/configuration/resources → UI draft/review
→ explicit Add / resource replacement
→ inline for Add archive ≤ 128 KiB
→ otherwise beginNativeFaceImport(total, pair, epoch, optional baseline)
→ sequential appendNativeFaceImport(offset, chunk ≤ 64 KiB)
→ finishNativeFaceImport → sealed staged package
→ NATIVE_FACE_ADD_FILE / NATIVE_FACE_RESOURCES_FILE
→ native delta transaction → Watch readback
Archive limit is 16 MiB, and Bridge transfer TTL 30 minutes. Export returns chunks ≤ 64 KiB, stable total/sha256, and checked offset. Dart assembles bytes and verifies final SHA-256. A pair/epoch change stops transfer. Unfinished uploads are canceled or expire on Bridge.
Photos crop/resource editing uses actual resources and a baseline package hash. Local preview/draft changes do not themselves submit mutations. Installed-face autosave has a guarded controller; gallery creation requires explicit Add.
Companion includes catalogs/options, AppIntent parameters/identity, complication layout, Photos metadata, pigment/monogram editors, indexed/component preview codecs, and a decode queue. Research captures bind complete configuration identities. One face family's results do not automatically apply to others.
Stage 90 verifies Activity Analog swatch components against 530 independent native reference images, with maximum premultiplied channel difference 2/255 and alpha difference 0. This is offline research validation; production generic-option UI integration remains open. Unsupported fractions and native geometry are documented in stage 90.
| API enum | NPS domain | Native key | Notes |
|---|---|---|---|
RIGHT_WRIST |
com.apple.nano |
wornOnRightArm |
Two-way preference |
INVERT_SCREEN |
com.apple.nano |
invertUI |
Two-way preference |
TIME_24_HOUR |
.GlobalPreferences |
AppleICUForce24HourTime |
Not marked two-way by the codec |
setWatchSetting → SET_WATCH_SETTING:<enum>:<bool> → WatchSettingsCodec.encode → NPS → Watch preference observer. Missing or malformed values remain unknown, not false. Unix time differs from the Apple preference epoch (978307200 seconds).
NPS domain: com.apple.NanoTimeKit. Keys: SelectedPigmentList and AutoSelectedPigmentList. These are pair-scoped palette fullname sets, not individual face JSON fields.
Watch NPS type-0 update
→ NtkPreferenceEnvelope (shared parse)
→ PigmentPreferenceCodec
→ pair-scoped durable PigmentPreferenceMirror
→ Companion baseline: pair / epoch / both timestamps / complete sets
→ manual visibility change
→ setNativePigmentVisibility
→ PigmentPreferenceCommand validates the same baseline
→ guarded paired NPS write
→ new observed mirror confirms values
Automatic and manual sets have different lifecycles. The writer validates both baseline collections and preserves native manual opt-out semantics. Limits: 1023 names, 64 KiB value bytes. Scoped preferences are not replayed from an inappropriate global cache.
customMonogram uses domain com.apple.NanoTimeKit, with a separate mirror/revision and writer. The path is Foundation-compatible normalization/rules → current pair/epoch/revision baseline → setNativeMonogram → guarded NPS write → paired observation.
The face monogram switch and global text are distinct values. Editor state, delivery receipt, and observed text remain separate. Details: Foundation normalization, native writer.
flowchart LR
Apps[Android notification] --> Listener[AppleWatchNotificationListenerService]
Listener --> Mirror[Bulletin payload / stable identity / revision]
Mirror --> Op[OperationalWatchService]
Op --> IDS[IDS bulletindistributor: Class-C]
IDS --> Watch[Watch bulletin]
Watch --> Action[Watch action / reply / dismiss]
Action --> Claims[Durable claim + identity/revision validation]
Claims --> Effect[Android action / RemoteInput / cancellation]
Old actions must not affect newer revisions or another chat. Duplicate claims survive restart. Notification access, per-app policy, and available Android actions require separate checks. Test bulletin delivery does not establish every app, icon, attachment, Focus, or haptic policy.
Phone → Watch: pingWatch → PING_WATCH → FindMyLocalDeviceCodec → authenticated IDS → Watch sound request → correlated response.
Watch → Phone: native request → FindMyPhoneIpcCodec → OperationalPhoneFinder → main-thread Android sound/flash effects → durable duplicate claim → correlated result through root/IDS. stopPhonePing controls the local effect. Permission receipt OPENED does not establish camera/flash availability.
After activation/language/Normal and correlated PrepareInitialSync response, a separate root worker reads the current supported WPA2/CCMP network. A V2 ADD archive travels on alloy.wifi.networksync in the same setup session. The worker binds pair/generation, limits retries, and clears late results on stop/link reset.
Operational READY has its own automatic send; syncWifi remains a manual retry for the current network. Archive ACK establishes delivery, while actual Watch network join requires separate evidence. Credentials are excluded from public Flutter snapshots.
Watch encrypted Health message
→ live IDS Class-C lane
→ forwardEncryptedHealthData()
→ app encrypted inbox / replay
→ OperationalHealthController
→ peer identity + registry binding + native decode
→ durable Health session/defaults state
→ limited typed observations → Companion
Outbound encrypted requests use scoped HealthOutboundIpcCodec with UUID/peer/session correlation. Ciphertext delivery and object decode do not establish native changes acceptance, anchor commits, or complete workout statistics. Limits: feature plan, defaults mirror evidence.
This research path implements modern native snapshot exchange. It does not replace the verified clockface.sync delta transaction.
flowchart LR
Discovery[Authenticated NetworkRelay discovery] --> Pin[Endpoint + public-key pins]
Pin --> QUIC[watch-replicator-quic / pinned TLS 1.3]
QUIC --> Tunnel[Bounded datagrams through existing relay tunnel]
QUIC --> Stream[ReplicatorStreamReader / NetworkHeader]
Stream --> OPACK[OPACK envelope / encodedBody]
OPACK --> Session[ReplicatorReadSession: StateReplicator handshake]
Session --> Zones[library_snapshots / gallery_snapshots]
Zones -. unfinished .-> Files[Persist complete files / correlated ACK / publish snapshot]
The QUIC engine opens no OS socket. Its caller supplies datagrams/time and consumes outbound datagrams/stream events. TLS identity uses public-key pins from authenticated discovery and proof of key possession, rather than Web PKI for an arbitrary hostname.
Constants: ALPN application-service, maximum datagram 1200 bytes, maximum batch 4. ReplicatorReadSession uses envelope protocol 8, zone protocol 6, message type StateReplicator, client com.apple.nanotimekit.replicator.library.
Handshake and authenticated DATA do not establish complete snapshot records. Current non-DATA file persistence/ACK is unimplemented; snapshot delivery remains unconfirmed. Sources: Rust engine, ReplicatorReadSession, research report.
| Owner | Responsibility |
|---|---|
CompanionScope |
Shared Bridge and disposal |
WatchBridgeService |
Platform subscription, typed public streams, projections |
BridgeIpcClient |
Binder connection, pending requests/deadlines, binding death |
BridgeSetupService / Engine |
Setup foreground lifetime, owner input, safe stop |
OperationalWatchService |
Saved pair runtime, epoch/readiness, commands/reconnect |
RootBluetoothHalHost |
Exclusive HAL lease, startup/exit |
HalTransportSession |
Serial packet queue/state machine, transient SAs, cleanup |
| App Keystore/storage adapters | Sealed pair/bond/IDS records and persisted identity |
PairingSessionRecord |
Versioned plaintext model inside its owner; sealing required before disk |
PairedSyncPreferenceStore / mirrors |
Pair-scoped preferences and write baselines |
ClockFaceSyncClient / package store |
Header sequence/generation and validated native packages |
| Health stores | Peer identity, inbox/replay/session/defaults boundaries |
| Rust C ABI | Rust-owned JSON allocations; Dart releases with aw_core_free |
Key correlations: pair UUID identifies a saved pair; epoch the current transport session; requestId a command; message UUID one IDS message; generation/sequence/revision protocol freshness or a baseline.
Keys, PINs, optical payloads, and Wi-Fi passwords are not UI data. Public snapshots contain permitted projections. Pair/epoch changes, close, or timeout invalidate pending commands and late callbacks. Interrupted native mutations must not be replayed automatically.
Run each block on its stated host and stop if a command fails. Shell blocks use Bash/zsh; Windows blocks use PowerShell. Build desktop targets on their own OS. The current Android build scripts use the macOS/Linux NDK layout. The tested SDK combination is Flutter 3.47.4 / Dart 3.13.3, Rust 1.98.1, JDK 17. The commands below install tools; they do not upgrade the project's lockfiles.
Linux Mint 22 / Ubuntu 24.04, x86_64:
sudo apt-get update
sudo apt-get install -y git curl unzip xz-utils zip python3 build-essential \
clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev \
libglu1-mesa libusb-1.0-0-dev libbluetooth-dev bluez systemd libcap2-bin openjdk-17-jdk
sudo update-alternatives --config java
sudo update-alternatives --config javac
export JAVA_HOME="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")"
export PATH="$JAVA_HOME/bin:$PATH"
Choose JDK 17 in both alternatives prompts if several JDKs are installed. Other distributions need equivalent packages and their own hardware acceptance run. Package requirements follow Flutter's Linux setup, with the project's Java, USB and Bluetooth dependencies added.
macOS: install full Xcode and Homebrew first, then:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
sudo xcodebuild -license
brew install python git openjdk@17 cocoapods libusb
export JAVA_HOME="$(brew --prefix openjdk@17)/libexec/openjdk.jdk/Contents/Home"
export PATH="$JAVA_HOME/bin:$PATH"
Complete the license prompts. If Xcode is elsewhere, substitute its actual path. See Flutter's macOS setup.
Windows 10/11, x64: install App Installer/WinGet if winget is absent, then
run these commands in PowerShell. The Visual Studio installer may request UAC.
winget install --exact --id Git.Git
winget install --exact --id Python.Python.3.13
winget install --exact --id EclipseAdoptium.Temurin.17.JDK
winget install --exact --id Rustlang.Rustup
winget install --exact --id Microsoft.VisualStudio.2022.Community --override "--wait --passive --add Microsoft.VisualStudio.Workload.NativeDesktop --includeRecommended"
Start-Process 'ms-settings:developers'
Open a new PowerShell window after installation. Confirm the Visual Studio Desktop development with C++ workload and CMake tools are installed; VS Code alone does not provide them. Enable Developer Mode in the opened Windows Settings page to permit Flutter plugin symlinks. See Flutter's Windows setup and Microsoft's installer options.
For a new macOS/Linux environment, install Rust through its official installer and Flutter at the tested tag:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/watch-rustup-init.sh
sh /tmp/watch-rustup-init.sh
. "$HOME/.cargo/env"
mkdir -p "$HOME/develop"
git clone --branch 3.47.4 --depth 1 https://github.com/flutter/flutter.git "$HOME/develop/flutter"
export PATH="$HOME/develop/flutter/bin:$PATH"
git clone https://github.com/LordixDemon/apple_watch_connector.git
cd apple_watch_connector
rustup toolchain install 1.98.1 --profile minimal --component rustfmt --component clippy
rustup override set 1.98.1
For Windows, after the WinGet steps:
New-Item -ItemType Directory -Force "$env:USERPROFILE\develop" | Out-Null
git clone --branch 3.47.4 --depth 1 https://github.com/flutter/flutter.git "$env:USERPROFILE\develop\flutter"
$env:Path = "$env:USERPROFILE\develop\flutter\bin;$env:Path"
$env:JAVA_HOME = Split-Path (Split-Path (Get-Command javac.exe).Source -Parent) -Parent
$env:Path = "$env:JAVA_HOME\bin;$env:Path"
git clone https://github.com/LordixDemon/apple_watch_connector.git
cd apple_watch_connector
rustup toolchain install 1.98.1 --profile minimal --component rustfmt --component clippy
rustup override set 1.98.1
If an SDK or project directory already exists, use it instead of cloning over
it. Persist the Flutter/Cargo bin paths and JDK selection in your own shell
profile or user environment; the exports above apply to the current terminal.
The official Flutter installation guide
also provides SDK archives. Do not run flutter upgrade or pub upgrade as
part of reproducing the tested candidate.
On all hosts, from the project root:
git rev-parse --short HEAD
python3 --version
java -version
javac -version
rustc --version
flutter --version
flutter doctor -v
cd apple-watch-companion
flutter pub get --enforce-lockfile
cd ..
Use python instead of python3 on Windows. flutter doctor must show a
working toolchain for your chosen target; an unrelated Android/Xcode warning
does not block a Linux/Windows build. Expected SDK output is Rust 1.98.1,
Flutter 3.47.4 and Dart 3.13.3; Java and javac should use JDK 17.
Record the Git revision when reporting a test.
| Target | Next steps | Current outcome |
|---|---|---|
| Android | SDK/build, then APK/root installation | Tested on rooted OnePlus CPH2653; clean Companion build is blocked by missing optical inputs |
| Linux | Build, then broker installation and pairing | Fresh pairing, activation and saved-pair reconnect |
| Windows | Build, then controller launcher and pairing | Tested on Windows 10 / Realtek 0bda:b00e; latest refactor needs hardware retest |
| macOS | Build and launch | Discovery and BLE only; full activation/IDS is incomplete |
For a new pair, the Watch must display its pairing screen. A Watch already paired to another host cannot join a new pair using that host's old PIN. For an existing pair saved by this installation, use Connect, preserving its records. Keep the Watch nearby and charged. The selected Bluetooth controller is reserved during a Linux/Windows session.
Successful Linux/Windows setup requires a visible Watch face, owner confirmation
when requested, and OPERATIONAL / watchReady=true. Android likewise requires
observed setup completion and operational readiness. A Bluetooth connection,
activation reply or delivery ACK alone does not establish completed setup.
Finally disconnect and reconnect: an eligible saved pair should resume without
another PIN. See troubleshooting if a stage stalls.
| Target | Current project requirements |
|---|---|
| Java core tests | JDK 17, existing Gradle wrapper |
| Android Bridge | SDK 36, Build Tools 36.1.0, minSdk 33, targetSdk 36 |
| Native Android QUIC | NDK 28.2.13676358, Rust target aarch64-linux-android; API33 clang in Gradle |
| Companion | Flutter compatible with Dart ^3.10.3, existing pubspec.lock; reports used Flutter 3.47.4 / Dart 3.13.3 |
| Rust | Edition 2024, Cargo.lock; workspace compiler checked as 1.98.1 |
| macOS | macOS/Xcode/CoreBluetooth; arm64 verified host, Intel build provided but physically unverified |
| Linux | Flutter Linux toolchain, Rust, Python 3, JDK 17, CMake/Ninja/clang, GTK3 development files, BlueZ/busctl, libusb/libbluetooth development files and setcap for the HCI broker |
| Windows | Windows 10 version 2004+ / 11, Bluetooth controller, Rust MSVC, Python 3, JDK 17–22, Visual Studio Desktop development with C++ and CMake tools; Flutter Developer Mode |
| Camera/native assets | Required local hash-pinned research inputs for native build scripts; README commands do not download them |
| Android physical runtime | Supported root/HAL handoff and compatible APK signatures; ordinary BLE API is insufficient for this backend |
macOS deployment also depends on Xcode configuration; see macOS findings. Linux requires installation of the HCI broker, and Windows requires controller access through WinUSB or the supported UsbDk driver. See the platform connection sections below.
Install Android Studio and SDK Command-Line Tools first. In Bash/zsh on macOS/Linux, after selecting JDK 17:
if [ "$(uname -s)" = Darwin ]; then
export ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
else
export ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}"
fi
export PATH="$ANDROID_HOME/platform-tools:$PATH"
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" --licenses
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" \
"platform-tools" "platforms;android-36" "build-tools;36.1.0" \
"ndk;28.2.13676358" "cmake;3.22.1"
flutter config --android-sdk "$ANDROID_HOME" --jdk-dir "$JAVA_HOME"
rustup target add aarch64-linux-android
flutter doctor -v
Use the installed SDK path if it differs, and adjust cmdline-tools/latest
to its installed directory. Review the license prompts. Package installation
uses the SDK Manager CLI.
Clean Android Companion builds are currently blocked in this public checkout.
prepareOpticalAsset requires four local inputs which are not distributed here.
From the project root, check them before attempting tools/build.py android:
test -f firmware/ios-26.6-23G71/extracted/VisualPairing
test -f research-tools/prepare_optical_android_asset.py
test -f research-tools/visual_pairing_reader_oracle.py
test -x research-tools/venv-dis/bin/python
If any input is missing, stop the Companion package build. The pinned asset
generation step validates its source; arbitrary substitute files are not a
working decoder. These instructions do not supply the private inputs.
-x prepareOpticalAsset is only a source-compilation check and cannot validate
camera pairing. See the Android release gate.
Bridge can still be built separately with
(cd apple-watch-bridge && ./gradlew :app:assembleRelease) after its SDK/NDK setup.
# Workspace Rust tests and Clippy
python3 tools/build.py core
# Bridge and Companion APKs; requires the optical inputs above; no installation
python3 tools/build.py android
# macOS app with embedded Rust dylib and signature verification
python3 tools/build.py macos
# Prepare shared Java dependencies, then build Linux on Linux
(cd apple-watch-bridge && ./gradlew :core:desktopRuntime)
python3 tools/build.py linux
# Windows app with Rust HCI broker, protocol JARs and Java runtime
python tools/build.py windows
Bridge preBuild invokes buildReplicatorQuic. If needed, install its Rust target:
rustup target add aarch64-linux-android
SDK location comes from apple-watch-bridge/local.properties / local environment; do not copy another developer's absolute paths. Bridge release currently uses debug signing configuration as a laboratory artifact. Compatible signatures are required for IPC.
# Root: host-appropriate workspace tests and Clippy
python3 tools/build.py core
cargo fmt --all --check
# In apple-watch-bridge/
./gradlew :core:test
./gradlew :protocol-runtime:test
./gradlew :app:lintDebug :hal:lintDebug
./gradlew :app:assembleDebug :app:assembleRelease
./gradlew :app:assembleDebugAndroidTest
# In apple-watch-companion/; run flutter pub get for first-time preparation
flutter analyze --no-pub
flutter test --no-pub
flutter build apk --release --no-pub
Native captures/builders have tools/test_native_*.py checks and documented fixtures. Instrumentation APK build, installation, and execution are separate actions. Offline tests do not establish physical pairing, visual output, sound, or Watch-local application.
| Artifact | Path |
|---|---|
| Bridge debug | apple-watch-bridge/app/build/outputs/apk/debug/app-debug.apk |
| Bridge release | apple-watch-bridge/app/build/outputs/apk/release/app-release.apk |
| Companion release | apple-watch-companion/build/app/outputs/flutter-apk/app-release.apk |
| macOS app | apple-watch-companion/build/macos/Build/Products/Release/apple_watch_companion.app |
| Linux bundle (x64) | apple-watch-companion/build/linux/x64/release/bundle/ |
| Windows bundle (x64) | apple-watch-companion/build/windows/x64/runner/Release/ |
| Java XML / report | apple-watch-bridge/core/build/test-results/test/, core/build/reports/tests/test/ |
The root HAL implementation currently accepts OnePlus CPH2653 only and
requires an already rooted, supported phone. This guide does not root the phone.
Enable USB debugging, unlock it, and approve the computer's ADB key. From the
project root, select the phone using its serial from adb devices:
adb devices -l
WATCH_ADB_SERIAL='REPLACE_WITH_ADB_DEVICE_SERIAL'
adb -s "$WATCH_ADB_SERIAL" get-state
adb -s "$WATCH_ADB_SERIAL" shell getprop ro.product.model
adb -s "$WATCH_ADB_SERIAL" shell su -c id
Expected: device, model CPH2653, and root uid=0 after the Magisk prompt.
unauthorized requires approval on the phone; an emulator does not test the
physical HAL. For multiple devices always retain the explicit -s selection.
For initial deployment using the checked-in Magisk module:
You can use the published experimental ZIP instead of building it. It contains Bridge 0.2.427 / code 627, the whitelist and SELinux rules; Companion is installed separately with a matching signing certificate. Download and checksums are in the release notes. The tested phone remains rooted OnePlus CPH2653.
To build your own ZIP from the project root:
bash apple-watch-bridge/tool/build_magisk_module.sh
adb -s "$WATCH_ADB_SERIAL" push apple-watch-bridge/build/apple-watch-bridge-magisk.zip /sdcard/Download/
In Magisk → Modules → Install from storage, choose that ZIP and reboot the phone to apply the module. It supplies the Bridge priv-app, hidden-API whitelist and SELinux rules. Root access alone does not supply this configuration. The ZIP embeds the built Bridge APK; rebuild it for your intended version.
Once both APKs are built or supplied as a tested matching pair, verify their signatures, then install them:
"$ANDROID_HOME/build-tools/36.1.0/apksigner" verify --print-certs apple-watch-bridge/app/build/outputs/apk/release/app-release.apk
"$ANDROID_HOME/build-tools/36.1.0/apksigner" verify --print-certs apple-watch-companion/build/app/outputs/flutter-apk/app-release.apk
adb -s "$WATCH_ADB_SERIAL" install -r apple-watch-bridge/app/build/outputs/apk/release/app-release.apk
adb -s "$WATCH_ADB_SERIAL" install -r apple-watch-companion/build/app/outputs/flutter-apk/app-release.apk
adb -s "$WATCH_ADB_SERIAL" shell am start -n dev.applewatchandroid.companion.apple_watch_companion/.MainActivity
Both signer SHA-256 digests must match for signature-protected IPC. Expected
install output is Success for each APK. Ordinary local builds use the debug
key; distribution signing is a separate release procedure.
For an update, first follow session shutdown and backup.
adb install -r preserves app data; do not erase pair storage to update.QUEUED/ACK cannot substitute for them.Desktop platforms share the Flutter interface through Rust and use code/PIN pairing. Linux and Windows have their own HCI transports; macOS discovery/BLE does not establish complete new pairing capability. Capabilities are explicit, and there is no desktop fallback to Android channels.
After host/SDK preparation, from the project root:
rustup target add aarch64-apple-darwin
python3 tools/build.py macos
codesign --verify --deep --strict apple-watch-companion/build/macos/Build/Products/Release/apple_watch_companion.app
open apple-watch-companion/build/macos/Build/Products/Release/apple_watch_companion.app
On an Intel Mac use x86_64-apple-darwin instead. Allow Bluetooth access when
macOS requests it; select Scan / Find a Watch in Companion. Expected result
is an observed BLE device list. A missing permission can be corrected in
System Settings → Privacy & Security → Bluetooth. codesign verifies the
local app signature; it does not establish Developer ID notarization.
Full Watch pairing, activation and operational IDS remain unavailable on macOS.
Build on Linux with the commands above, then install the broker and desktop launcher as the desktop user:
python3 tools/install_linux.py apple-watch-companion/build/linux/x64/release/bundle
getcap /usr/local/libexec/watch-companion/watch-linux-hci
./apple-watch-companion/build/linux/x64/release/bundle/apple_watch_companion
The example uses the x64 bundle path; use the corresponding arm64 output for
an ARM64 build. The installer uses sudo for the root-owned HCI broker and grants
cap_net_admin,cap_net_raw; Flutter and the Java worker run as the desktop user.
Open Watch Companion, choose All Watches → Bluetooth Adapter, then use
Find a Watch, select the observed Watch and enter its six-digit code.
Adapter selection is saved by Bluetooth address and survives HCI index changes. If the saved controller is missing, choose an available controller explicitly. During the session, the selected adapter belongs to the HCI broker, so its normal BlueZ peripherals disconnect. Other adapters remain available.
Pairing, activation and encrypted reconnect were physically verified on Mint
22.3. State is stored under $XDG_STATE_HOME/watch-companion or
~/.local/state/watch-companion; private records use AES-GCM and restricted
filesystem permissions. An eligible saved pair resumes once on startup.
Desktop Wi-Fi provisioning, watch-face management, notifications and Health
are not yet fully exposed through the desktop command contract. See
Linux hardware verification and
adapter selection.
Before connection, inspect adapters without opening an HCI session:
systemctl is-active bluetooth
bluetoothctl list
busctl --system --json=short call org.bluez / org.freedesktop.DBus.ObjectManager GetManagedObjects
Expected: Bluetooth service active, your controller in the inventory, and
cap_net_admin,cap_net_raw=ep on the installed broker. Run the bundle as your
desktop user. If BlueZ is stopped, use sudo systemctl start bluetooth and
refresh adapters in Companion. While connected the leased adapter can disappear
from BlueZ; inspect it after Disconnect. Do not start the raw HCI broker
manually or run Flutter with sudo.
Build with python tools/build.py windows, or run from Flutter with
flutter run -d windows -t lib/main_desktop.dart. CMake builds and copies
watch_core_ffi.dll beside the executable automatically. ARM64 builds require
the Rust target aarch64-pc-windows-msvc.
The Windows full-protocol backend uses a native Rust USB HCI broker and the
same portable Java pairing/activation/IDS engine as Linux. The bundle contains
the broker, protocol JARs and a minimal Java runtime; building requires a JDK
17–22 (JAVA_HOME). The USB identity identifies the internal Bluetooth
controller; the Watch connects wirelessly and needs no USB cable.
For the tested Realtek 0bda:b00e, start the built interface with
.\tools\run_windows_companion.ps1. Its elevated supervisor temporarily binds
only the selected controller to the existing signed Microsoft WinUSB driver;
the interface itself runs as the normal user. Closing the interface restores
the original Bluetooth driver. The launcher discovers compatible USB Bluetooth
controllers: it uses a single available controller, or asks you to choose when
several are present. You can also specify an exact -InstanceId;
-NonInteractive requires that choice when there are several controllers.
Use -Check for a read-only controller inventory and preflight. Recovery information and the supervisor
log are under %LOCALAPPDATA%/watch-companion/controller-leases.
No generated driver INF, publisher certificate or signing override is needed.
The native build stages DLL, HCI broker, protocol and JRE together before
replacing the previous complete bundle. Controller discovery refreshes in the
background; opening uses the selected USB identity and its backend.
Rust detects the bound WinUSB controller automatically and restores the
installed OEM RTL8821C RAM firmware after power transitions, checking the chip
cut, firmware project and controller version readback.
Choose the controller in All Watches → Bluetooth Adapter, use Find a Watch, select the Watch and enter its observed PIN. Windows Bluetooth devices using that controller disconnect while the interface reserves it; a dedicated adapter avoids interrupting other Bluetooth devices. Enumeration sends no HCI commands and never redirects a controller.
The alternative UsbDk path requires signed
UsbDk 1.0.21, installed
with tools/install_windows_controller.ps1 -Install. It produced transfer
errors on the tested Realtek; use the WinUSB launcher for that controller.
UsbDk 1.0.22 is blocked before capture because of its documented Bluetooth
power-management WDF_VIOLATION regression.
The installer replaces an existing 1.0.22 with the verified signed 1.0.21 package.
Private pairing, bond and installation records are protected using Windows
DPAPI under the current user, with restricted file ACLs. Persistent records and
the filtered protocol.log are in %LOCALAPPDATA%/watch-companion.
An observed PIN, a BLE link or a queued command never sets watchReady;
readiness requires the protocol's activation and operational evidence.
Physical validation on Windows 10 / Realtek 0bda:b00e with Watch7,5 / watchOS
26.2.0 passed PIN authentication, activation, IDS exchange and reconnection with
the stored LTK and IRK. After the owner confirmed the Watch displayed its face,
the same activated pair reached OPERATIONAL with watchReady=true without
another PIN. This verifies the tested controller and Watch; other controllers
and Watch versions have not been physically tested.
The earlier WinRT discovery/BLE transport remains in
crates/watch-transport-windows for diagnostics; it is not the full-pairing
backend used by the Windows FFI.
From the project root in PowerShell, after building:
.\tools\run_windows_companion.ps1 -Check
.\tools\run_windows_companion.ps1
The first command reports controller IDs/services and driver signature; the
second starts the controller-selection/UAC flow. For a specific controller,
copy its full ID from -Check into the prompt:
$watchControllerId = Read-Host 'Controller InstanceId from -Check'
.\tools\run_windows_companion.ps1 -InstanceId $watchControllerId -NonInteractive
If scripts are blocked, launch only this invocation with a process-local policy:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\tools\run_windows_companion.ps1
After closing Companion, wait for the supervisor to restore the controller:
use the $watchControllerId captured above (set it from -Check if you used
automatic selection).
Get-PnpDevice -PresentOnly -InstanceId $watchControllerId
Get-PnpDeviceProperty -InstanceId $watchControllerId -KeyName DEVPKEY_Device_Service
Get-ChildItem "$env:LOCALAPPDATA\watch-companion\controller-leases" -Filter restored -Recurse
For a controller originally using the Windows Bluetooth stack, its service
should return to BTHUSB; inspect the restored marker in the matching lease
directory. If driver restoration was interrupted, retain that lease's
original.json and start a recovery session with the same controller:
$watchRecoveryRecord = Read-Host 'Full path to the matching lease original.json'
.\tools\run_windows_companion.ps1 -InstanceId $watchControllerId -RecoveryRecord $watchRecoveryRecord
This starts Companion with the saved restoration information. Close the window and wait for restoration, then repeat the service check. The record must belong to that exact controller. This launcher has no restore-only switch.
First use Disconnect / stop setup in Companion, wait for HAL/HCI worker shutdown, and close the app. On Windows also wait for driver restoration before replacing the bundle. Keep pairing records; updating an app does not require resetting the Watch or choosing Unpair.
From the existing project's root, after stopping the app:
watch_state="${XDG_STATE_HOME:-$HOME/.local/state}/watch-companion"
watch_backup="$HOME/watch-companion-backups/$(date +%Y%m%d-%H%M%S)"
install -d -m 700 "$watch_backup"
cp -a "$watch_state" "$watch_backup/state"
cp -a apple-watch-companion/build/linux/x64/release/bundle "$watch_backup/bundle"
git status --short
git pull --ff-only
(cd apple-watch-companion && flutter pub get --enforce-lockfile)
(cd apple-watch-bridge && ./gradlew :core:desktopRuntime)
python3 tools/build.py linux
python3 tools/install_linux.py apple-watch-companion/build/linux/x64/release/bundle
./apple-watch-companion/build/linux/x64/release/bundle/apple_watch_companion
These backup steps assume a prior successful installation. Stop before pulling
if git status lists local changes. Adjust the bundle path for ARM64.
Expected: the saved pair is available and resumes without another PIN.
For rollback, stop the app again and install/run the preserved whole bundle:
python3 tools/install_linux.py "$watch_backup/bundle"
"$watch_backup/bundle/apple_watch_companion"
If the new version changed state incompatibly, with all workers stopped preserve that state and restore the matching backup first:
mv "$watch_state" "$watch_backup/state-after-update"
cp -a "$watch_backup/state" "$watch_state"
In PowerShell, after closing the app and restoring the driver:
$watchBackup = Join-Path $env:USERPROFILE ('watch-companion-backups\' + (Get-Date -Format yyyyMMdd-HHmmss))
$watchState = Join-Path $env:LOCALAPPDATA 'watch-companion'
New-Item -ItemType Directory -Path $watchBackup | Out-Null
Copy-Item -LiteralPath $watchState -Destination (Join-Path $watchBackup 'state') -Recurse
Copy-Item -LiteralPath 'apple-watch-companion\build\windows\x64\runner\Release' -Destination (Join-Path $watchBackup 'bundle') -Recurse
git status --short
git pull --ff-only
Push-Location apple-watch-companion
flutter pub get --enforce-lockfile
Pop-Location
python tools/build.py windows
.\tools\run_windows_companion.ps1
Keep the backup private, on this Windows installation and under the same account; DPAPI records are not a portable pairing export. To run the previous version:
.\tools\run_windows_companion.ps1 -Bundle (Join-Path $watchBackup 'bundle')
If state rollback is required, first close the app and wait for the supervisor, then preserve the new state and restore the matching backup:
Move-Item -LiteralPath $watchState -Destination (Join-Path $watchBackup 'state-after-update')
Copy-Item -LiteralPath (Join-Path $watchBackup 'state') -Destination $watchState -Recurse
Android: stop the session, build a matching-signature pair of APKs, and repeat
the two adb install -r commands. Keep the installed
signing key and package IDs. allowBackup=false means ordinary adb backup
does not provide a pair backup. A lower release versionCode can be rejected;
uninstalling/clearing app data to force a downgrade destroys the saved pair.
There is no documented general Android pair-export/restore procedure yet.
Update the Magisk module's embedded APK too when changing its base version.
macOS: quit the app before rebuilding. Keep a copy of the whole old .app, then
rebuild/verify/open using the macOS commands. This build does not
provide an activated pair to migrate. For example, from the project root:
watch_mac_app=apple-watch-companion/build/macos/Build/Products/Release/apple_watch_companion.app
watch_mac_backup="$HOME/watch-companion-backups/$(date +%Y%m%d-%H%M%S)/apple_watch_companion.app"
mkdir -p "$(dirname "$watch_mac_backup")"
ditto "$watch_mac_app" "$watch_mac_backup"
python3 tools/build.py macos
open "$watch_mac_app"
To run the old build, quit the new app and use open "$watch_mac_backup".
On any platform a saved backup cannot
restore a pair after the Watch itself has been reset or paired elsewhere.
| Symptom | Check |
|---|---|
| Bridge IPC unavailable | APK package/signatures, signature permission, Binder binding/death |
PERMISSION_REQUIRED |
Bridge Bluetooth permission; camera/flash have separate gates |
BUSY |
Setup/operational owner remains active or stopping |
| Discovery token rejected | Stale advertisement, different candidate, changed session |
| BLE connected but Watch-ready false | BT_CL/security/bond/normal link/IDS not complete |
| Control ready but data unavailable | Class-C/D, route, NWSC, service connector, authenticated Hello |
| Setup requires language/locale | Missing authenticated Watch observation; phone locale cannot replace it |
QUEUED without a visible effect |
HAL/IDS statuses, correlated response, fresh native observation |
Face UNKNOWN |
Timeout/disconnect/ambiguous apply; read full inventory before deciding to retry |
| No records after Replicator handshake | Open file persistence/ACK/application-stage problem |
| Old battery/settings | Timestamp, pair/epoch, connected state; saved snapshot is not a fresh reply |
| Incorrect native preview | Full configuration identity, family/options/style, assets, unsupported shades |
| macOS pairing unavailable | Authorized bootstrap packet transport remains unfinished |
Read logs separately from Watch commands:
adb -s "$WATCH_ADB_SERIAL" logcat -d -s WatchBridgeIpc WatchBridge WatchNotification
For Linux, after the protocol worker has run:
watch_state="${XDG_STATE_HOME:-$HOME/.local/state}/watch-companion"
tail -n 100 "$watch_state/protocol.log"
getcap /usr/local/libexec/watch-companion/watch-linux-hci
For Windows:
Get-Content "$env:LOCALAPPDATA\watch-companion\protocol.log" -Tail 100
.\tools\run_windows_companion.ps1 -Check
| Problem | Action and expected check |
|---|---|
Missing flutter / cargo / javac |
Reopen the terminal after installation; check PATH, JAVA_HOME and version commands in Start here |
Linux Missing shared dependencies |
Run (cd apple-watch-bridge && ./gradlew :core:desktopRuntime) before building; do not add arbitrary JARs |
Linux LINUX_BACKEND_NOT_INSTALLED / permission error |
Re-run tools/install_linux.py on the correct bundle; check broker ownership/capabilities and adapter inventory |
| Windows no raw HCI / controller remains unavailable | Use the WinUSB launcher and its matching UAC flow; check -Check, the lease log and recovery |
Android prepareOpticalAsset missing input |
The public source cannot finish that APK; follow the documented build prerequisite rather than bypassing it |
| Android signature / IPC denied | Compare both APK signer digests and the installed signing key; use matching builds |
| Wrong/stale PIN or Watch awaiting a new pair | Select a fresh discovery candidate and use the current on-screen PIN; an old PIN/session cannot resume a reset Watch |
| Setup stalls after an ACK | Inspect current phase/journal and actual Watch screen; do not force a success state or repeatedly reset the Watch |
| macOS BLE list empty | Check Bluetooth permission, proximity and the Watch's discoverable pairing state; activation is not supported on this backend |
Log files are created after the worker starts. Review/redact identifiers before sharing excerpts; never upload pairing stores, keys, account inputs or Wi-Fi secrets.
Journal/projections and retained reports provide other stages. Ordinary diagnosis does not require exposing secrets from encrypted pair records.
OperationalCommandPolicy additionally allows REQUEST_REGISTRY, REBOOT_WATCH, REQUEST_DIAGNOSTIC_ARCHIVES, COLLECT_WATCH_DIAGNOSTIC, REMOVE_BULLETIN:, and native staged-resource commands. Reboot/collection are separate diagnostic actions; a whitelist entry does not imply a Companion method or button.
Parser: HalCommandReader.java. It is broader than production Companion; research commands are not a normal Connect flow.
| Group | Names / purpose |
|---|---|
| Lifetime | STOP; EOF closes the command owner and requires root stop |
| Discovery | COMPANION_DISCOVERY_V1, SELECT_DISCOVERED_WATCH_V1: |
| Inputs | OPTICAL_PAIRING_CODE_V1:, PIN:, local identity/local IDS/restored session prefixes |
| Persistence | BOND-STORED, BOND-STORE-FAILED, PAIRING-SESSION-STORED, PAIRING-SESSION-STORE-FAILED |
| Typed operational | BridgeCommandCodec.REQUEST_PREFIX, Health outbound prefix, FindMyPhone result prefix |
| Native direct input | PING_WATCH, SET_WATCH_SETTING:, SEND_BULLETIN: |
| Generic research send | SEND_APP_DATA:, SEND_APP_PROTOBUF:; route/payload validation still applies |
| Link / IDS diagnostics | CLOSE_IKE_SESSION, PROBE_IDS_KEYS, OPEN_IDS_CONTROL, SEED_STALE_FLOW: |
| Snapshots | DISCOVER_NATIVE_SNAPSHOTS; discovery does not prove snapshot reception |
| Activation input | Credential/retry/cancel prefixes in ActivationChallengeManager |
| Setup research | SYNC_PROGRESS:, PUBLISH_PAIRED_SYNC, FORCE_ACTIVATION_CONFIRMED, REDRIVE_ACTIVATION, RETRY_ACTIVATION, FORCE_SETUP_OBSERVED |
| Explicit rejection | FINISH_SETUP, legacy SET_ACTIVE_FACE: |
Force/replay branches are historical research mechanisms. They do not generate physical evidence or replace normal activation/Setup policy. Reproduce experiments from dated reports with their recorded preconditions and outcomes.
| Question | Primary source |
|---|---|
| Which Companion methods are accepted? | CompanionBridgeService |
| How are setup/connect/replacement admitted? | CompanionConnectionPolicy |
| Where are UI lifetime and backend selection? | CompanionScope, backend factory |
| How does Dart form commands? | BridgeCommands |
| Who owns the operational pair? | OperationalWatchService |
| Who owns root HAL? | RootBluetoothHalHost |
| Where are handshake and packet events? | HalTransportSession |
| Where is the normal-link/IDS boundary? | NormalLinkIdsSessionBridge |
| How does a topic choose its lane? | IdsApplicationRoute |
| Which statuses/ACKs exist? | BridgeCommandCodec |
| Where is durable pairing evidence? | PairingSessionRecord |
| How does initial sync complete? | AppleWatchPostCommitCoordinator |
| Who receives native faces? | ClockFaceSyncReceiver |
| Who confirms face mutation? | ClockFaceDeltaSession |
| Where are sealed uploads/export? | NativeFaceTransferDispatcher |
| Where are Monogram/Pigment codecs? | MonogramPreferenceCodec, PigmentPreferenceCodec |
| Where are Health/Find Phone effects? | OperationalHealthController, OperationalPhoneFinder |
| Where is portable Rust state? | watch-core session, watch-pairing |
| Where is desktop ABI? | watch-core-ffi |
| Where is QUIC? | watch-replicator-quic |
| Topic | Document |
|---|---|
| Feature groups / acceptance | registry, coverage plan |
| Module boundaries / lifecycle | Bridge, Companion |
| Camera PSK → activation → visible face | optical result |
| Setup UI versus native evidence | setup chain, completion 388 |
| Battery | physical report |
| Notification action claims | notification 345 |
| Phone finder | phone ping 350 |
| Native preview coverage | coverage 51 |
| Native option rows / swatches | sections 89, swatches 90 |
| Modern replication | Replicator 39 |
| macOS limitations / evidence | Rust core, transport findings |
| Linux pairing, activation and installation | Linux Companion 91 |
| Shared-runtime refactor and release candidate | Stage 92, release guide |
| Localization | LOCALIZATION.md |
When adding a feature, update its API row, route, completion rule, and evidence together. Captures should record build/model, preconditions, input identity/hash, sent/received data, and inference limits. Never reuse ARM addresses or firmware results across builds without verification.
| Term | Meaning in this project |
|---|---|
| HCI / ACL | Controller interface and Bluetooth data packets |
| L2CAP / CID | Bluetooth channels and negotiated channel identifiers |
| BT_CL | Apple control/service negotiation before dynamic pairing/normal pipes |
| ERTM | L2CAP enhanced retransmission, ordering, and windows |
| uIKE / IKE SA | Pairing/relay security association and authenticated key exchange |
| SMP / OOB | LE Secure Connections and out-of-band authentication material |
| NetworkRelay | Normal-link transport and authenticated relay endpoint negotiation |
| ESP / IPsec | Protection of inner IP packets after SA establishment |
| IDS / Alloy | Apple identity/service delivery and application-topic messaging |
| NWSC | Network.framework service connector routing |
| NanoRegistry / NR | Pair/device registry and lifecycle properties |
| PBBridge | Phone/Watch Setup, activation, and Buddy messages |
| NPS / PairedSync / PSY | Preference transport and synchronization lifecycle/observers |
| NanoTimeKit / NTK | Native watch-face models, collection, and presentation |
| OPACK | Native object serialization for Replicator envelopes/bodies |
| Readback | Fresh Watch observation confirming an expected result |
| Pair / epoch / revision | Pair identity / current session / observed baseline version |
| Receipt | Acceptance/delivery acknowledgment at a particular layer |
Third-party tree/binary licenses and provenance remain in their own LICENSE/NOTICE files and reports. Bridge NOTICE covers its relevant components. This README does not assign a new blanket license to those materials.
Magisk-модуль Android: Скачать ZIP Bridge 0.2.427 · Установка
Начать здесь · Карта вызовов · Сопряжение · API · Сборка · Доказательства
Подключение Apple Watch к Android, Linux и Windows через нативные Bluetooth/IDS протоколы и общий Flutter Companion. macOS использует тот же интерфейс с ограниченным Bluetooth-бэкендом.
Проект воспроизводит путь от обнаружения и сопряжения до активации, повторного подключения, обмена настройками и управления нативными циферблатами. Companion показывает наблюдаемое состояние часов и отправляет команды на всех четырёх платформах. Android Bridge владеет соединением и ключами на телефоне. Linux и Windows используют Rust-бэкенд, нативные HCI-брокеры и общий Java-протокол; macOS — Rust и CoreBluetooth. На Linux и Windows сопряжение выполняется по шестизначному коду часов; камера доступна на Android.
Подготовка релиза: общий Java-модуль protocol-runtime, отдельные research/debug
probes, аудит Linux-пакета и единая подпись Android-приложений. См.
release guide. Сопряжение, активация и шифрованный
реконнект с Linux и Windows проверены физически на тестовом оборудовании;
поддержка прикладных desktop-функций остаётся частичной.
Rust-бэкенд Linux разделён на состояние, ввод/завершение воркера и команды. Поздние события не возвращают готовность после отключения или ошибки. Java-модули используют строгие lockfiles; Linux-архив проверяется по manifest schema 2 и получает воспроизводимые метаданные. См. этап 93.
Срез документации: 13 октября 2026. Проверяемый основной стенд — Apple Watch Ultra 2,
Watch7,5, watchOS26.2 / 23S303. Версии в исходниках: Bridge 0.2.429 / code 629, Companion 1.0.86+87, Rust workspace 0.1.1. Версия исходников не доказывает установленную на устройстве версию. Camera pairing на Android, сопряжение на Linux Mint 22.3 и Windows 10 / Realtek0bda:b00eимеют отдельные физические проверки.
Проект находится в разработке. Физическое сопряжение через камеру, активация и видимый циферблат подтверждены в журнале стенда; полная замена Watch.app, всех функций iPhone и всех моделей Apple Watch ещё не достигнута. Для macOS обнаружение реализовано, но полный физический путь pairing → activation → IDS остаётся незавершённым.
В таблице разделены реализация в исходниках и факты из сохранённых проверок. Подробности приведены в реестре функций и плане приёмки. Старые записи в этих документах могут описывать уже пройденные этапы.
Статус выпуска: experimental RC. Таблица платформ и проверенные версии, список изменений. Перечисленные ниже сервисы относятся к Android, если не указано иначе. Linux/Windows пока предоставляют управление подключением и настройкой пары; macOS — обнаружение и BLE-соединение. Панель Available Features показывает заявленные возможности backend отдельно от готовности соединения.
| Область | Реализованный путь | Подтверждение и границы |
|---|---|---|
| Android discovery / pairing | Реальное обнаружение, выбор часов, PIN и optical PSK ветки | Полный camera pairing с активацией и видимым циферблатом зафиксирован в optical report. Это проверка конкретного стенда |
| Активация / Setup | PBBridge, начальные свойства, язык часов, PairedSync, политика завершения | Активация, завершение интерфейса и operational-ready — отдельные проверки; completion report различает нативное доказательство и подтверждение владельца |
| Operational reconnect | Сохранённая активированная пара, шифрованный link, IDS, отдельный foreground service | Сопряжение и Setup не повторяются при обычном Connect; свежая батарея и Connected проверялись физически |
| Батарея / About | Нативные наблюдения, запрос SystemSettings | Battery evidence; отсутствующее значение остаётся неизвестным |
| Wi-Fi | Текущая сеть телефона → native V2 ADD | В стендовом отчёте подтверждены получение сети и подключение часов. Покрытие ограничено поддерживаемой WPA2-PSK/CCMP сетью |
| Native faces | Inventory, select, duplicate, remove, reorder, update, add, ресурсы, export | Нативная библиотека отделена от локальных дизайнов. Успех изменения требует readback; покрытие семейств/превью развивается |
| Watch.app UI / previews | Нативные каталоги, параметры, palette/shades, Photos, monogram, AppIntent metadata | Полное визуальное соответствие и оптимизация не закончены. Stage 90 — исследовательские swatch-компоненты, ещё не production UI |
| Настройки | RIGHT_WRIST, INVERT_SCREEN, TIME_24_HOUR |
Разрешён точный набор NPS ключей; наличие остальных экранов настроек не означает поддержку записи |
| Notifications | Bulletin transport, Android listener, привязка actions к revision | Доставка и обработчики реализованы; все приложения, вложения, звук и реальные actions требуют отдельных проверок |
| Find devices | Phone → Watch sound и Watch → Phone effects/result | Реализация различает receipt и фактический эффект; звук/flash зависят от Android permissions и состояния телефона |
| Health | Шифрованный ingress, replay, peer/registry binding, decode, defaults mirror | Это не доказательство полного импорта Health/Activity, корректных anchors и всех типов данных |
| Calls / SMS / contacts | Исследовательские codecs и отдельные Flutter методы | triggerCall и callAction текущим Companion IPC не реализованы. Полноценный звонок и аудиоканал не подтверждены |
| StateReplicator | Authenticated discovery, pinned QUIC, native framing, handshake | Получение и надёжное сохранение полного native snapshot/file потока ещё не завершены |
| macOS | Общий Flutter UI, Rust C ABI, CoreBluetooth discovery/BLE, portable pairing codecs | Полное физическое pairing/IDS блокируется незавершённым авторизованным bootstrap transport |
| Linux | Rust HCI broker + общий Java-протокол + Flutter UI | Сопряжение, активация и реконнект проверены на Mint 22.3; Wi-Fi credentials и application services ещё не завершены |
| Windows | Rust USB HCI + общий Java protocol + Flutter UI; Windows DPAPI | PIN, активация и encrypted reconnect физически проверены на Windows 10 / Realtek 0bda:b00e / Watch7,5; остальные модели и полное покрытие сервисов не подтверждены |
.
├── apple-watch-companion/ Общий Flutter UI; Android IPC и desktop Rust FFI
├── apple-watch-bridge/
│ ├── core/ Java wire codecs, crypto, policies, mirrors, JVM tests
│ ├── protocol-runtime/ Общий controller/session/stdin runtime; без Android SDK
│ ├── hal/ Android root entry, Bluetooth HAL и адаптеры файлов
│ └── app/ Android services, Keystore/SQLite, permissions, Companion IPC
├── crates/
│ ├── watch-protocol/ Rust framing, FE25 advertisements, BT_CL normal handoff
│ ├── watch-pairing/ Portable uIKE control/PIN security; no OS I/O
│ ├── watch-core/ Sessions, observations, transport interface
│ ├── watch-transport-macos/ CoreBluetooth ownership and callbacks
│ ├── watch-transport-linux/ Desktop worker, Linux HCI и Windows USB HCI brokers
│ ├── watch-transport-windows/ WinRT discovery/BLE diagnostic transport
│ ├── watch-core-ffi/ Versioned C ABI for Flutter desktop
│ └── watch-replicator-quic/ Socket-free pinned QUIC engine and Android JNI
├── src/ Compatibility facade and USB framing utilities
├── tools/ Builds, native captures, asset builders, verification
├── linux-watch-host/ Linux protocol host
├── windows-watch-host/ Windows protocol host и DPAPI storage
└── docs/ Architecture, protocol documentation and verification
Основные сборочные границы заданы в Gradle settings и Cargo workspace. artifacts/, target/, build/ и исторические source-before/ содержат много файлов, которые не являются текущими исходниками приложения.
flowchart LR
User[Пользователь] --> UI[Flutter Companion]
UI --> Facade[WatchBridgeService / BridgeTransport]
Facade --> Android[PlatformBridgeTransport]
Android --> Channels[CompanionFlutterBridge]
Channels --> Binder[BridgeIpcClient / Messenger]
Binder --> IPC[CompanionBridgeService]
IPC --> Setup[BridgeSetupService / Engine]
IPC --> Op[OperationalWatchService]
Setup --> Root[RootBluetoothHalHost]
Op --> Root
Root --> HAL[HalTransportSession]
HAL --> Core[Java core: codecs / coordinators]
HAL <--> Watch[Apple Watch]
Facade --> DesktopUI[RustBridgeTransport]
DesktopUI --> ABI[watch-core-ffi]
ABI --> Rust[watch-core / watch-pairing / watch-protocol]
ABI --> Desktop[watch-transport-linux / desktop backend]
Desktop --> Worker[LinuxProtocolHost / WindowsProtocolHost]
Worker --> HAL
HAL <--> Broker[Linux HCI USER / Windows USB HCI]
Broker <--> Watch
Rust --> CB[watch-transport-macos / CoreBluetooth]
CB -. незавершённый pairing bootstrap .-> Watch
HAL --> JNI[watch-replicator-quic: JNI]
В Android одна сборка Bridge APK содержит app, hal, protocol-runtime, core и QUIC .so. Companion — отдельный APK. Java package dev.applewatchandroid.bridge сохранён между модулями; зависимости направлены app → hal → protocol-runtime → core и app → core. Linux и Windows workers используют те же protocol-runtime и core с платформенными адаптерами контроллера и хранилища. Rust управляет временем жизни desktop worker и публичным состоянием; Flutter обращается к нему через C ABI.
Основной пользовательский flow находится во Flutter. Bridge launcher переадресует в Companion; закрытие экрана не прекращает operational foreground service. HAL принадлежит одному владельцу, а последовательность пакетов обрабатывает одна транспортная state machine.
Подробности: Bridge architecture, Companion architecture, macOS Rust core.
Это карта реализованных межслойных переходов и значимых вызовов. Стрелки в схемах показывают направление работы; некоторые переходы асинхронны и проходят через очередь. Для решения, выполняется ли конкретная ветка, нужны условия в исходниках и runtime evidence.
sequenceDiagram
actor Owner as Пользователь
participant UI as Flutter screen / controller
participant Dart as BridgeCommands
participant Channel as PlatformBridgeTransport
participant Native as CompanionFlutterBridge / BridgeIpcClient
participant IPC as CompanionBridgeService
participant Op as OperationalWatchService
participant HAL as HalCommandReader / HalTransportSession
participant IDS as NormalLinkIdsSessionBridge
participant Watch as Watch service
Owner->>UI: Выбрать действие
UI->>Dart: Typed method + baseline/target
Dart->>Channel: invoke(method, args)
Channel->>Native: MethodChannel
Native->>IPC: Messenger COMMAND=3, version=1, requestId
IPC->>IPC: signature, payload, pair/epoch, policy
IPC->>Op: sendCommand(command, requestId)
Op-->>IPC: Принято в локальную очередь?
IPC-->>UI: QUEUED / REJECTED / UNAVAILABLE
Op->>HAL: BRIDGE_COMMAND_V1 + encoded request
HAL->>IDS: sendApplicationProtobuf / sendApplicationData
IDS->>Watch: Authenticated application message
Watch-->>IDS: ACK / response / subsequent observation
IDS-->>HAL: SessionEvent
HAL-->>Op: Typed status / native observation
Op-->>IPC: BridgeIpcDispatcher + public state
IPC-->>Native: STATE=102
Native-->>UI: EventChannel → decoded observation
Для pairing/activation используется BridgeSetupService, а для больших .watchface пакетов перед командой работает NativeFaceTransferDispatcher. Привязка Companion к IPC сама по себе не запускает HAL и не создаёт пару.
Watch packet
→ Bluetooth HAL callback / HCI ACL
→ HalTransportSession
→ NormalLinkIdsSessionBridge.accept…
→ normal link: ERTM / NetworkRelay / authenticated ESP
→ IDS: IPv6 / TCP / NWSC / control or application stream
→ IdsModernSessionCoordinator.SessionEvent
→ handleIncomingProtobufEvent / feature receiver / typed observation
→ root stdout IPC → OperationalWatchService
→ BridgeIpcDispatcher + CompanionSessionState
→ CompanionBridgeService.state() / STATE=102
→ BridgeIpcClient → BridgeStateProjection
→ EventChannel → WatchBridgeService / BridgeObservation
→ WatchObservationController / WatchConnectionProvider / feature controller
→ Provider → виджеты
Публикация сначала декодируется целиком, затем раздаётся проекциям. При потере transport или смене пары connected/native state инвалидируется; поздний ответ прежней epoch не должен оживлять старое состояние.
| Значение / событие | Что оно доказывает |
|---|---|
QUEUED |
Bridge принял запрос в локальную очередь |
HAL_ACCEPTED |
Root transport принял структурированный запрос |
IDS_QUEUED |
Сообщение поставлено в IDS с привязанным message UUID |
APP_ACK_RECEIVED |
Получено соответствующее application ACK |
APP_RESPONSE_RECEIVED |
Получен соответствующий ответ приложения; его смысл зависит от протокола |
NATIVE_FACE_APPLIED |
Полный коррелированный Watch readback подтвердил результат face transaction |
APPLIED |
Значение, которое Flutter confirmed-wrapper принимает за подтверждение; нельзя синтезировать из очереди/ACK |
EXPIRED, FAILED, REJECTED, UNKNOWN |
Разные причины незавершённого запроса; UNKNOWN не доказывает отсутствие частичного эффекта |
OBSERVED |
Ответ на чтение публичного state; свежесть отдельных полей нужно проверять |
Stage enum определён в BridgeCommandCodec. Это не универсальная линейная цепочка: не каждый запрос имеет все стадии, а snapshot на диске может быть старым.
BLE advertisement / FE25 setup metadata
→ выбранный свежий Watch candidate
→ HCI LE connection
→ L2CAP
→ BT_CL fixed signaling CID 0x003a
→ VERSION / services / CREATE / ACCEPT
→ dynamic pairing pipe
→ uIKE security + PIN/optical authentication
→ OOB / LE Secure Connections SMP / durable Bluetooth bond
→ encrypted normal-link registration
Application protobuf / binary plist / resource / native face payload
↕ IDS application stream + ServiceMap topic
↕ NWSC service connector + TCP + inner IPv6
↕ authenticated ESP / Class-C or Class-D IPsec
↕ NetworkRelay
↕ L2CAP ERTM on negotiated dynamic channels
↕ HCI ACL
↕ Android Bluetooth HAL / controller
↕ BLE link
Ключевая граница в NormalLinkIdsSessionBridge: normal-link слой владеет IKE/ESP/NetworkRelay/ERTM, IDS слой — расшифрованными IPv6/TCP, NWSC и control/data streams. IKE обменивается сообщениями при установлении SAs; обычный application payload идёт через ESP.
Topic не равен отдельному TCP соединению. Services с одинаковыми priority и protection class используют общий канонический connector. Class-C/D — классы защищённого маршрута; это не два отдельных приложения или радиоканала.
Companion запрашивает beginPairing либо beginOpticalPairing. CompanionConnectionPolicy проверяет Bluetooth permission, известность saved identity и отсутствие другого владельца. Обычный begin не заменяет существующую пару. Явный replacePairOptically требует совпадающий UUID сохранённой пары и проверенный encrypted replacement backup.
BridgeSetupService / BridgeSetupEngine → RootBluetoothHalHost → HalTransportSession.initialize() → runTransportHandshake().
Root host проверяет invocation и exclusive HAL lease. Transport настраивает controller, получает устойчивую локальную identity из приложения, ACL buffer/credits и event masks. MainActivity не владеет этим процессом.
Сканируются advertisements. Валидные setup metadata дают product/watchOS/candidate information. WatchDiscoverySelection публикует ограниченный набор временных tokens; Companion отправляет selectDiscoveredWatch. Свежесть advertisement проверяется снова перед использованием. Manufacturer data само по себе не доказывает, что устройство можно сопрягать.
После LE connection выполняются BT_CL version/service/CREATE/ACCEPT переходы. Dynamic CID получается из переговоров, а не подставляется как константа. В HAL это приводит к probeControlSaInit().
Карта вызовов в HalTransportSession:
runTransportHandshake()
→ probeControlSaInit()
→ probeAdditionalKeyExchange()
→ probeControlIkeAuth()
→ выбранная ветка аутентификации
Эти функции выполняют protocol negotiation и проверку authenticated payloads. Здесь ещё нет доказательства durable bond, IDS-ready или активации.
flowchart TD
Control[Authenticated control transport] --> Choice{Способ pairing}
Choice --> PIN[probePinAuthMethod / awaitWatchPin]
PIN --> PPK[SPAKE2+ / PPK pairing path]
PPK --> Pair[probePairingSession]
Choice --> Camera[Camera → native optical reader]
Camera --> Handle[Одноразовый opaque handle в native Companion]
Handle --> Code[110-byte OpticalPairingCode]
Code --> Bind[Связать со свежим setup advertisement]
Bind --> PSK[probeOpticalPairing: PSK/1, authentication at MID2]
PSK --> Notify[exchangePairingPrivateNotifies]
Pair --> Notify
Notify --> SMP[completeLeSecureConnectionsSmp]
SMP --> Store[Encrypted durable pair / bond]
Store --> Normal[completeModernNormalRegistration]
Normal --> IDS[attachModernIdsSession]
PIN и optical используют разные authentication paths и message counters. Optical code должен соответствовать свежему setup advertisement; декодированное изображение ещё не означает authenticated pair.
На Android camera reader работает в отдельном native :optical процессе. Dart получает публичное имя и временный handle, а explicit Pair/Replace передаёт native payload через signature-protected IPC. Handle одноразовый; documented lifetime — 120 секунд, request deadline — 10 секунд.
Текущий spatial reader использует локальный hash-pinned transitional firmware backend 23G71, генерируемый при сборке. Полностью независимый portable spatial detector ещё не готов. Arm64 поддерживается; другие native ABIs явно отвергают инициализацию. Эти входные бинарники не заменяются произвольными файлами.
exchangePairingPrivateNotifies() приводит к OOB/SMP и completeLeSecureConnectionsSmp(). Приложение запечатывает records, root получает результат persistence. Переходы с BOND-STORED / PAIRING-SESSION-STORED отличаются от отправки запроса сохранения; отказ нельзя считать успешным commit.
completeModernNormalRegistration() → attachModernIdsSession() поднимают normal pipe и защищённые маршруты. После NetworkRelay/IKE Class-D и Class-C появляются IDS control Hello, service routing и data lanes. Следующий этап — обмен device info и NanoRegistry properties.
Источники: optical result, typed setup state chain, PairingSessionRecord.
| Шаг | Владелец / данные | Условие перехода |
|---|---|---|
| 1. IDS bootstrap | IdsModernSessionCoordinator, IdsBootstrapState, device-info exchange |
Authenticated identity и живые control/data lanes |
| 2. Initial registry | AppleWatchInitialSetupIdsAdapter, NanoRegistry properties |
Получены обязательные свойства и согласована identity |
| 3. Language / locale | WatchLocaleSnapshot, locale preferences archive |
Только язык/locale из authenticated Watch observation; fallback на язык Android отсутствует |
4. Commit isPaired |
Initial setup coordinator + durable record | Валидны обязательные предыдущие checkpoint/barrier |
| 5. Activation | HalPostCommitOrchestrator, ActivationProxyWorker, PBBridge |
Реальный результат активации; требуемый owner input передаётся отдельно |
| 6. Normal / initial sync prep | PBBridge 36, затем PBBridge 21 / response 18 | Correlated response; он ещё не означает окончание Setup |
| 7. Wi-Fi | InitialWifiSyncWorker |
Поддерживаемая сеть, та же pair/generation и live setup Class-C lane |
| 8. PairedSync | NPS domain, active/completed sync state | Нативные observer/saga условия, а не только progress wording |
| 9. Exit Setup | Buddy / Carousel и completion policy | Отдельное доказательство или явное подтверждение владельца видимого циферблата |
| 10. Normal reconnect | Operational policy | Сохранена подходящая та же пара; старый setup transport безопасно остановлен |
flowchart TD
Activation[Активация] --> PB36[PBBridge 36: Normal / notifyActivationCompleted]
PB36 --> PB21[PBBridge 21: PrepareInitialSync]
PB21 --> PB18[PBBridge 18: correlated response]
Scheduler[Phone PairedSync scheduler] --> NPS[NPS type 0: com.apple.pairedsync]
NPS --> Prefs[CFPreferences / synchronize]
Prefs --> Note[PSYWatchSyncStateChangedNotification]
Note --> UIObserver[PSYSyncStateObserver]
Note --> SagaObserver[PSYSyncClientStateObserver]
UIObserver --> Finished[BuddyFinished.syncDidComplete]
SagaObserver --> Saga[NanoRegistry PairedSync saga]
Finished --> Exit[exitCriteria / haptic / Start or Crown]
Exit --> Dismiss[completeAndDismiss / suspendApp / buddyDone]
Dismiss --> Clock[Carousel / циферблат]
Saga --> IDSComplete[Local IDS sync completion]
Схема показывает две независимые observer-ветви. PBBridge 4 об активации, response 18, AppACK и отправленный progress сами по себе не доказывают видимый Clock. Точные native handlers и различия сборок 23S303, 23U67, 23G71 сохранены в SETUP_CHAIN.md.
SetupProgressState.Phase содержит:
IDLE, STARTING, DISCOVERING, CONNECTING, PIN_REQUIRED, SECURITY, IDS,
REGISTRY, CONFIGURING, ACTIVATING, ACTIVATION_INPUT, ACTIVATED, SYNCING,
WAITING_FOR_WATCH, VERIFYING_RECONNECT, VERIFIED, FAILED, STOPPING, STOPPED
Это vocabulary проекций, а не обязательный единственный порядок всех сценариев. BridgeSetupEngine запрещает регрессии и очищает поздние owner-input fields.
Durable states PairingSessionRecord сохраняют wire numbering:
| Значение | Checkpoint | Значение | Checkpoint |
|---|---|---|---|
| 1 | PAIRING_MATERIAL_PERSISTED |
10 | INITIAL_PROPERTIES_RECEIVED |
| 2 | SMP_BONDED_RAW |
11 | READY_TO_COMMIT_IS_PAIRED |
| 3 | STOCK_BOND_IMPORTED |
12 | IS_PAIRED_COMMITTED |
| 4 | STOCK_ENCRYPTED_RECONNECT |
13 | ACTIVATION_CONFIRMED |
| 5 | NETWORK_RELAY_PRELUDE_NEGOTIATED |
14 | IS_SETUP_CONFIRMED |
| 6 | CLASS_D_ESTABLISHED |
15 | PAIRED_SYNC_COMPLETE |
| 7 | CLASS_C_ESTABLISHED |
16 | SETUP_COMPLETE |
| 8 | IDS_CONTROL_READY |
17 | CLOCK_VISIBLE_CONFIRMED |
| 9 | IDS_DATA_READY |
18 | OPERATIONAL_HEALTH_CONFIRMED |
Проверяются также flags/evidence и identity. Одного большого ordinal недостаточно для допуска к operational mode. Подтверждение владельца хранится отдельно от native evidence; legacy не наблюдавшиеся terminal records не получают автоматическое право на нормальный reconnect.
sequenceDiagram
participant UI as Companion
participant Policy as CompanionConnectionPolicy
participant Op as OperationalWatchService
participant Store as Encrypted pair / bond store
participant HAL as Root host / HalTransportSession
participant Watch as Watch
UI->>Policy: connectWatch(pairId)
Policy->>Policy: saved pair, operationalEligible, matchingPair, idle owner
Policy->>Op: START_OPERATIONAL
Op->>Store: Load and validate existing records
Op->>HAL: Start with operational pairing identity
HAL->>HAL: requireMatchingActivatedPair
HAL->>Watch: Reconnect + link encryption + normal registration
HAL->>Watch: NetworkRelay / Class-C,D / IDS bootstrap
Watch-->>HAL: Authenticated Hello / state
HAL-->>Op: Ready epoch + observations
Op-->>UI: Connected + current native values
Note over Op,Watch: Setup / Albert / Buddy replay disabled
OperationalWatchService владеет root lifetime, readiness epoch, ограниченной очередью и reconnect. Feature controllers получают OperationalSessionAccess; они не запускают новый root owner и не заменяют пару.
При разрешённом retry policy задержка зависит от failure count: 15 → 30 → 60 секунд. IDS policy rejection и незавершённый paired commit могут запретить reconnect; таймер сам по себе не даёт права игнорировать эти ограничения.
Disconnect выполняет cleanup и завершает владельца. Пока он STOPPING, второй flow не получает HAL. После нового reconnect создаётся новая transport epoch; запросы старой epoch больше не действуют.
Ниже перечислены обработчики текущего Android IPC service и отдельные локальные adapter methods. Аргументы сокращены до значимых; точные validation/size rules находятся в связанной реализации. Это внутренний API двух подписанных приложений.
| Уровень | Контракт |
|---|---|
| Flutter commands | MethodChannel dev.applewatchandroid.companion/bridge |
| Flutter observations | EventChannel dev.applewatchandroid.companion/events |
| Native service | dev.applewatchandroid.bridge.CompanionBridgeService |
| Permission | dev.applewatchandroid.bridge.permission.COMPANION_IPC, protection level signature |
| Messenger request | 1 = SUBSCRIBE, 2 = GET_STATE, 3 = COMMAND |
| Messenger reply | 101 = RESPONSE, 102 = STATE |
| Envelope | version=1, canonical requestId UUID; command включает method и typed Bundle args |
| Admission | Разрешён Companion package и совпадающая подпись; максимум 4 subscribed clients |
| Companion client | Максимум 32 pending requests; deadline 5 секунд; binding-death recovery |
| Root command | BRIDGE_COMMAND_V1: + Base64 binary request {id, epoch, deadlineMs, command} |
| Root status | BRIDGE_COMMAND_STATUS_V1: + Base64 typed status |
Нативный text/file adapter использует отдельные channels …/text и …/face_files; эти операции не являются IDS командами часов.
| Method | Путь / условие |
|---|---|
connectWatch |
START_OPERATIONAL: та же eligible saved pair |
disconnectWatch |
STOP_SESSION: setup или operational owner |
beginPairing |
START_SETUP: сохранённой пары нет |
beginOpticalPairing |
START_SETUP: native optical payload, сохранённой пары нет |
replacePairOptically |
Явный replacement: matching saved pair, optical code, verified backup |
resumeSetup |
Сохранённая matching pair ещё не operationalEligible |
submitPin |
Forward в действующий setup owner |
selectDiscoveredWatch |
Forward временного discovery token в setup |
activationResponse |
Forward challenge response в setup |
confirmSetup |
Activated matching pair; безопасное owner-confirmed завершение |
finishSetup |
Explicit REJECTED; legacy force-finish отключён |
auditStockBond |
Отдельный setup diagnostic flow для matching pair |
importStockBond |
Guarded stock bond import flow |
alignStockIdentity |
Guarded stock identity diagnostic flow |
probeStockReconnect |
Guarded stock reconnect probe |
Sources: connection policy, IPC service.
| Method | Аргументы / command | Что подтверждает результат |
|---|---|---|
pingWatch |
PING_WATCH → FindMyLocalDevice |
Correlated response и отдельная физическая проверка звука |
refreshWatchState |
REQUEST_DEVICE_ABOUT |
SystemSettings About response / telemetry observation |
refreshFaceCollection |
REQUEST_FACE_COLLECTION |
Полный принятый native collection snapshot |
syncWifi |
SYNC_CURRENT_WIFI |
V2 archive delivery; join сети проверяется отдельно |
setActiveFace |
faceId → NATIVE_FACE_SELECT: |
Новое подтверждённое selected UUID |
duplicateNativeFace |
sourceFaceId; новый UUID из requestId |
Новый face/archive в readback |
removeNativeFace |
faceId |
Readback без UUID; последний face удалить нельзя |
reorderNativeFaces |
faceIds, точная перестановка inventory |
Полный matching порядок в readback |
updateNativeFace |
faceId, binary configuration |
Matching config в readback |
addNativeFace |
.watchface archive, до 128 KiB inline |
Новый target UUID и архив в readback |
setNativeMonogram |
pairId, epoch, revision, text |
Новое paired monogram observation |
setNativePigmentVisibility |
Pair/epoch, timestamps, полные baseline sets, changes |
Paired pigment mirror; очередь/ACK недостаточны |
setWatchSetting |
setting, boolean value |
Native NPS observation точного ключа |
sendNotification |
title, message, sectionId, sectionDisplayName |
Native bulletin path; действие/карточка проверяются отдельно |
stopPhonePing |
Локальный stop Android effect | STOPPED при успешном локальном stop |
Operational service проверяет readiness и policy, root повторно проверяет envelope. Face commands дополнительно требуют известный актуальный inventory. Legacy строка SET_ACTIVE_FACE: в root reader отвергается: используется native collection protocol.
| Method | Поведение |
|---|---|
beginNativeFaceImport |
Создаёт upload для pair/epoch, объявленного total; optional faceId/baselineHash для ресурсов |
appendNativeFaceImport |
Последовательные chunks, проверка offset |
finishNativeFaceImport |
Проверяет/seals пакет и ставит native add/resources command в очередь |
cancelNativeFaceImport |
Отменяет незавершённый upload |
exportNativeFace |
Chunked export сохранённого native package с offset/total/SHA-256 |
getState |
Локальный adapter возвращает последний client.observedState; это не fresh Watch request |
requestConnectionPermissions |
Локальный adapter открывает Bridge permission screen для connection scope |
requestPhoneFlashPermission |
Локальный adapter открывает permission screen; OPENED не означает granted |
startOpticalDecoder |
Запускает native optical worker для объявленных width/height |
processOpticalFrame |
Передаёт bounded packed UV frame native reader; не IDS packet |
stopOpticalDecoder |
Закрывает reader/worker resources |
discardOpticalCandidate |
Удаляет временный native candidate handle |
Методы upload/export находятся в NativeFaceTransferDispatcher; staging/export limits и lifetime проверяются также в package store. replaceNativeFaceResources — Dart wrapper над upload API, а не отдельный Bridge IPC method.
| Method | Реальное поведение текущего Bridge |
|---|---|
syncSettings |
Adapter error UNIMPLEMENTED; используйте поддерживаемый setWatchSetting для конкретных ключей |
unpairWatch |
Adapter error UNIMPLEMENTED; описание Dart метода не означает реализованный wipe |
triggerCall |
Adapter error UNIMPLEMENTED; telephony codec/topic недостаточен для real call relay |
callAction |
Adapter error UNIMPLEMENTED; answer/decline Android call flow ещё не подключён |
Эти вызовы отвергаются whitelist нативного Companion adapter ещё до отправки Binder command. При прямом вызове неизвестного method Android Bridge также возвращает UNIMPLEMENTED. finishSetup отдельно существует в policy как отвергаемый legacy method, но adapter его не пропускает.
Sources: BridgeCommands, CompanionFlutterBridge. _invokeConfirmed() принимает только receipt APPLIED, поэтому QUEUED не возвращается как подтверждённый успех.
Таблица соответствует whitelist в IdsApplicationRoute. Сокращение alloy.X означает com.apple.private.alloy.X. Все перечисленные application routes используют urgent priority. Наличие route означает возможность маршрутизации, а не полную готовность соответствующей функции.
| Topic suffix | Class | Телефон: codec / consumer | Смысл на стороне часов |
|---|---|---|---|
alloy.bluetoothregistry |
D | NanoRegistryPropertyCodec, Class-D setup |
Registry properties / lifecycle |
alloy.bluetoothregistryclassc |
C | NanoRegistry initial/property path | Class-C registry exchange |
alloy.pbbridge |
C | PbBridgeCodec, post-commit coordinator |
Activation / Buddy / Setup |
alloy.preferencessync.pairedsync |
C | PairedSyncCodec, paired mirror, sync coordinator |
Paired preferences и sync state |
alloy.preferencessync |
D | PreferencesSyncCodec, NPS consumers |
Ordinary preference route |
alloy.bulletindistributor |
C | Bulletin transport, notification listener | Notifications / actions |
alloy.bulletindistributor.settings |
D | Bulletin settings | Notification policy |
alloy.findmylocaldevice |
D | FindMyLocalDeviceCodec, OperationalPhoneFinder |
Play sound / Find Phone |
alloy.health.sync.classc |
C | OperationalHealthController, encrypted Health codecs |
Health synchronization |
alloy.telephony |
C | Telephony research codecs | Telephony route; full native acceptance не доказана |
alloy.timesync |
D | Timesync messages | Time exchange |
alloy.timezonesync |
D | Timezone messages | Timezone exchange |
alloy.clockface.sync |
D | ClockFaceSyncClient/Receiver, delta session |
NanoTimeKit native face collection |
alloy.systemsettings |
D | NanoSystemSettingsDiagnostics, reboot codec |
About / native system diagnostic operations |
alloy.wifi.networksync |
C | WifiNetworkSyncCodec, Wi-Fi workers |
Network archive V2 ADD |
alloy.sysdiagnose |
C | SysdiagnoseArchiveInventory/Collection |
Diagnostic archive operations |
alloy.idscredentials |
D | IdsDeviceInfoExchange, commands 11/12 |
Mutual credentials/device info; отдельны от account login / activation |
pairedsync имеет собственный Class-C route: наследовать Class-D от обычного preferencessync неправильно. Unknown topic whitelist отвергает. StateReplicator по NetworkRelay/QUIC — отдельный путь, описанный ниже.
refreshFaceCollection
→ REQUEST_FACE_COLLECTION
→ HalTransportSession.drainOutboundAppMessages()
→ ClockFaceSyncClient.reserveHeader()
→ ClockFaceSyncHeaderCodec.fullRequest()
→ NormalLinkIdsSessionBridge.sendClockFaceCollectionRequest()
→ Watch clockface.sync
→ ClockFaceSyncReceiver: session, parts, archive/configuration validation
→ native collection / package persistence
→ publishClockFaceObservation()
→ IPC public snapshot → NativeFaceCollection → UI
Header sequence/generation хранятся отдельно. Неполный multipart пакет не публикуется как готовая библиотека. При recovery передаётся known unfinished session; это управление native synchronization session, а не удаление пользовательских faces.
sequenceDiagram
participant UI as NativeFaceController / editor
participant Bridge as CompanionBridgeService
participant Plan as ClockFaceDeltaCommand / Plan
participant Tx as ClockFaceDeltaSession
participant Watch as Watch clockface.sync
participant Rx as ClockFaceSyncReceiver
UI->>Bridge: Command + committed face UUID / config
Bridge->>Plan: Validate inventory, target, payload, baseline
Plan->>Tx: Pair / epoch / watchPeer / session
Tx->>Watch: START
Watch-->>Tx: Correlated START response
loop Change batches
Tx->>Watch: BATCH(index, changes)
Watch-->>Tx: Matching session/index response
end
Tx->>Watch: END
Watch-->>Tx: END accepted / rollback / error
Tx->>Watch: Request complete readback
Watch-->>Rx: Complete native collection
Rx->>Tx: Observe full correlated result
alt Expected result present
Tx-->>UI: APPLIED / NATIVE_FACE_APPLIED
else Timeout / disconnect / ambiguous partial result
Tx-->>UI: UNKNOWN or REJECTED
end
Реальные stages: START_READY → START_WAIT → BATCH_READY → BATCH_WAIT → END_READY → END_WAIT → READBACK_WAIT → APPLIED, с terminal REJECTED / UNKNOWN. Readback timeout — 180 секунд. Ответ обязан совпадать по topic, message identifier, session, Watch peer и batch index.
Batch/end error может следовать за частично применённой записью. Автоматический повтор mutation запрещён этой state machine; сначала нужно разобраться с новым наблюдением. Файлы: delta session, delta plan, command codec.
.watchface и Photos/resourcesNative archive → validate structure/configuration/resources → UI draft/review
→ явный Add / resource replacement
→ inline, если Add archive ≤ 128 KiB
→ иначе beginNativeFaceImport(total, pair, epoch, optional baseline)
→ appendNativeFaceImport(offset, chunk ≤ 64 KiB), последовательно
→ finishNativeFaceImport → sealed staged package
→ NATIVE_FACE_ADD_FILE / NATIVE_FACE_RESOURCES_FILE
→ обычная native delta transaction → Watch readback
Общий archive limit — 16 MiB, Bridge transfer TTL — 30 минут. Export возвращает chunks ≤ 64 KiB, постоянные total/sha256 и проверяемый offset; Dart собирает bytes и проверяет итоговый SHA-256. Смена pair/epoch прекращает transfer. Незавершённый upload отменяется либо истекает на Bridge стороне.
Photos crop/resource editing работает с настоящими resources и baseline package hash. Локальная правка preview/draft сама по себе не отправляет изменение. Installed-face autosave имеет собственный guarded controller; gallery creation требует явного Add.
Companion содержит catalogs/options, AppIntent parameters/identity, complication layout, Photos metadata, pigment/monogram editor, indexed/component preview codecs и decode queue. Нативные research captures привязаны к полным configuration identities; перенос результата одной face family на остальные не предполагается.
Текущий stage 90 доказал Activity Analog swatch-компоненты: 530 независимых native reference images, max premultiplied channel delta 2/255, alpha delta 0. Это offline validation исследовательских программ; production generic-option UI ещё требует интеграции. Границы, unsupported shade fractions и native geometry подробно описаны в stage 90.
| API enum | NPS domain | Native key | Особенность |
|---|---|---|---|
RIGHT_WRIST |
com.apple.nano |
wornOnRightArm |
Two-way native preference |
INVERT_SCREEN |
com.apple.nano |
invertUI |
Two-way native preference |
TIME_24_HOUR |
.GlobalPreferences |
AppleICUForce24HourTime |
Codec не помечает как two-way |
setWatchSetting → SET_WATCH_SETTING:<enum>:<bool> → WatchSettingsCodec.encode → NPS → Watch preference observer. Чтение преобразует отсутствующий или malformed value в unknown, а не false. Unix time и Apple preference epoch (978307200 секунд) различаются.
NPS domain — com.apple.NanoTimeKit, keys — SelectedPigmentList и AutoSelectedPigmentList. Это fullname sets глобальной для пары палитры, а не поля конкретного face JSON.
Watch NPS type-0 update
→ NtkPreferenceEnvelope (общий parse)
→ PigmentPreferenceCodec
→ pair-scoped durable PigmentPreferenceMirror
→ Companion baseline: pair / epoch / both timestamps / complete sets
→ ручное изменение visibility
→ setNativePigmentVisibility
→ PigmentPreferenceCommand validates same baseline
→ guarded paired NPS write
→ новое observed mirror подтверждает значения
Автоматические и ручные sets имеют разный lifecycle. Writer проверяет обе baseline collections и сохраняет native manual opt-out semantics. Предел codec — 1023 имени, value bytes — 64 KiB. Scoped data не воспроизводится из неподходящего global cache.
customMonogram находится в том же domain com.apple.NanoTimeKit, но имеет отдельный mirror/revision и writer. Путь: Foundation-compatible text normalization/rules → current pair/epoch/revision baseline → setNativeMonogram → guarded NPS write → paired observation.
Monogram face switch и глобальный текст — разные значения. UI editor, delivery receipt и реальное observed text не смешиваются. Документы: Foundation normalization, native writer.
flowchart LR
Apps[Android notification] --> Listener[AppleWatchNotificationListenerService]
Listener --> Mirror[Bulletin payload / stable identity / revision]
Mirror --> Op[OperationalWatchService]
Op --> IDS[IDS bulletindistributor: Class-C]
IDS --> Watch[Watch bulletin]
Watch --> Action[Watch action / reply / dismiss]
Action --> Claims[Durable claim + identity/revision validation]
Claims --> Effect[Android action / RemoteInput / cancellation]
Старый action не должен применяться к новому revision или соседнему чату. Duplicate claims переживают restart. Notification access, per-app policy и доступные Android actions проверяются отдельно. Test bulletin sender не доказывает все реальные приложения, icons, attachments, Focus и haptic policy.
Phone → Watch: pingWatch → PING_WATCH → FindMyLocalDeviceCodec → authenticated IDS → Watch sound request → correlated response.
Watch → Phone: native request → FindMyPhoneIpcCodec → OperationalPhoneFinder → main-thread Android sound/flash effects → durable duplicate claim → correlated result обратно через root/IDS. stopPhonePing управляет локальным эффектом. Permission-screen receipt OPENED не означает, что камера/flash доступна.
После activation/language/Normal и correlated PrepareInitialSync response отдельный root worker получает текущую валидную WPA2/CCMP сеть. V2 ADD archive отправляется на alloy.wifi.networksync в той же setup session. Worker связан с pair/generation, ограничивает retries и очищает late results при stop/link reset.
При operational READY есть отдельная автоматическая отправка; syncWifi остаётся ручным retry для текущей сети. Archive ACK означает доставку архива; Watch join сети — отдельное физическое доказательство. Credentials не передаются в публичный Flutter snapshot.
Watch encrypted Health message
→ live IDS class-C lane
→ forwardEncryptedHealthData()
→ app encrypted inbox / replay
→ OperationalHealthController
→ peer identity + registry binding + native decode
→ durable Health session/defaults state
→ limited typed observations → Companion
Встречный encrypted request использует scoped HealthOutboundIpcCodec; UUID/peer/session correlation сохраняется. Доставка ciphertext и decode объекта не равны native changes acceptance, успешному commit anchors или полноценной статистике тренировок. Подробные границы — в Health feature plan и defaults mirror evidence.
Это отдельная исследовательская ветвь современного native snapshot обмена, а не замена подтверждённой clockface.sync delta transaction.
flowchart LR
Discovery[Authenticated NetworkRelay discovery] --> Pin[Endpoint + public-key pins]
Pin --> QUIC[watch-replicator-quic / pinned TLS 1.3]
QUIC --> Tunnel[Bounded datagrams through existing relay tunnel]
QUIC --> Stream[ReplicatorStreamReader / NetworkHeader]
Stream --> OPACK[OPACK envelope / encodedBody]
OPACK --> Session[ReplicatorReadSession: StateReplicator handshake]
Session --> Zones[library_snapshots / gallery_snapshots]
Zones -. остаётся завершить .-> Files[Persist complete files / correlated ACK / publish snapshot]
QUIC engine не создаёт ОС socket: caller передаёт datagrams и время, получает outbound datagrams/stream events. TLS peer проверяется по public keys из authenticated discovery, с проверкой владения ключом; это не Web PKI сертификат по произвольному hostname.
Текущие constants: ALPN application-service, max datagram 1200 bytes, max batch 4. Handshake ReplicatorReadSession использует envelope protocol 8, zone protocol 6, message type StateReplicator, client com.apple.nanotimekit.replicator.library.
Handshake и authenticated DATA не доказывают, что complete snapshot records получены. В текущем reader non-DATA file persistence/ACK не реализованы; отчёт о snapshot delivery остаётся неподтверждённым. Sources: Rust engine, ReplicatorReadSession, native research.
| Owner | Что хранит / чем владеет |
|---|---|
CompanionScope |
Общий Bridge и его dispose |
WatchBridgeService |
Platform subscription, typed public streams, projections |
BridgeIpcClient |
Binder connection, pending requests/deadlines, binding death |
BridgeSetupService / Engine |
Setup foreground lifetime, owner input, safe stop |
OperationalWatchService |
Existing pair runtime, epoch/readiness, commands/reconnect |
RootBluetoothHalHost |
Exclusive HAL lease, startup/exit |
HalTransportSession |
Serial packet queue/state machine, transient SAs, idempotent cleanup |
| App Keystore/storage adapters | Запечатанные pair/bond/IDS records и persisted identity |
PairingSessionRecord |
Versioned plaintext model внутри владельца; перед диском обязательно sealing |
PairedSyncPreferenceStore / mirrors |
Pair-scoped preference state и write baselines |
ClockFaceSyncClient / package store |
Header sequence/generation, validated native packages |
| Health stores | Peer identity, inbox/replay/session/defaults boundaries |
| Rust C ABI | Rust-owned JSON allocations; Dart освобождает через aw_core_free |
Основные привязки: pair UUID обозначает сохранённую пару, epoch — текущую transport session, requestId — команду, message UUID — конкретное IDS сообщение, generation/sequence/revision — protocol freshness/baseline.
Ключи, PIN, optical payload и Wi-Fi passwords не являются UI data. Public snapshots содержат только разрешённые проекции. Смена пары, epoch, close или timeout инвалидирует pending commands/late callbacks; interrupted native mutation не должна автоматически переигрываться.
Выполняйте каждый блок на указанной ОС и останавливайтесь при ошибке команды. Shell-блоки рассчитаны на Bash/zsh, Windows-блоки — на PowerShell. Desktop-цели собираются на своей ОС. Текущая Android-сборка использует NDK для macOS/Linux. Проверенный набор SDK: Flutter 3.47.4 / Dart 3.13.3, Rust 1.98.1, JDK 17. Команды устанавливают инструменты и сохраняют зависимости из lockfiles.
Linux Mint 22 / Ubuntu 24.04, x86_64:
sudo apt-get update
sudo apt-get install -y git curl unzip xz-utils zip python3 build-essential \
clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev \
libglu1-mesa libusb-1.0-0-dev libbluetooth-dev bluez systemd libcap2-bin openjdk-17-jdk
sudo update-alternatives --config java
sudo update-alternatives --config javac
export JAVA_HOME="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")"
export PATH="$JAVA_HOME/bin:$PATH"
Если установлено несколько JDK, в обоих запросах alternatives выберите JDK 17. Для других дистрибутивов нужны эквивалентные пакеты и отдельная проверка оборудования. За основу взяты требования Flutter для Linux, добавлены Java, USB и Bluetooth-зависимости проекта.
macOS: сначала установите полный Xcode и Homebrew, затем:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
sudo xcodebuild -license
brew install python git openjdk@17 cocoapods libusb
export JAVA_HOME="$(brew --prefix openjdk@17)/libexec/openjdk.jdk/Contents/Home"
export PATH="$JAVA_HOME/bin:$PATH"
Завершите запросы лицензии. Если Xcode расположен в другом месте, укажите его путь. См. подготовку macOS для Flutter.
Windows 10/11, x64: если нет winget, установите App Installer/WinGet.
Выполните команды в PowerShell; установщик Visual Studio может запросить UAC.
winget install --exact --id Git.Git
winget install --exact --id Python.Python.3.13
winget install --exact --id EclipseAdoptium.Temurin.17.JDK
winget install --exact --id Rustlang.Rustup
winget install --exact --id Microsoft.VisualStudio.2022.Community --override "--wait --passive --add Microsoft.VisualStudio.Workload.NativeDesktop --includeRecommended"
Start-Process 'ms-settings:developers'
После установки откройте новое окно PowerShell. Проверьте наличие workload Desktop development with C++ и CMake tools в Visual Studio. Одного VS Code недостаточно. В открытой странице Windows Settings включите Developer Mode для symlinks Flutter-плагинов. См. подготовку Windows для Flutter и параметры установщика Microsoft.
В новой среде macOS/Linux установите Rust через официальный установщик, а Flutter — на проверенном теге:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/watch-rustup-init.sh
sh /tmp/watch-rustup-init.sh
. "$HOME/.cargo/env"
mkdir -p "$HOME/develop"
git clone --branch 3.47.4 --depth 1 https://github.com/flutter/flutter.git "$HOME/develop/flutter"
export PATH="$HOME/develop/flutter/bin:$PATH"
git clone https://github.com/LordixDemon/apple_watch_connector.git
cd apple_watch_connector
rustup toolchain install 1.98.1 --profile minimal --component rustfmt --component clippy
rustup override set 1.98.1
На Windows, после установки через WinGet:
New-Item -ItemType Directory -Force "$env:USERPROFILE\develop" | Out-Null
git clone --branch 3.47.4 --depth 1 https://github.com/flutter/flutter.git "$env:USERPROFILE\develop\flutter"
$env:Path = "$env:USERPROFILE\develop\flutter\bin;$env:Path"
$env:JAVA_HOME = Split-Path (Split-Path (Get-Command javac.exe).Source -Parent) -Parent
$env:Path = "$env:JAVA_HOME\bin;$env:Path"
git clone https://github.com/LordixDemon/apple_watch_connector.git
cd apple_watch_connector
rustup toolchain install 1.98.1 --profile minimal --component rustfmt --component clippy
rustup override set 1.98.1
Если каталог SDK или про
Start here · Call map · Pairing · API · Build · Evidence