Pavlok Remote is a personal SwiftUI controller for one user-owned Pavlok 3. It supports a deliberate foreground manual hold and an explicitly armed microphone session that can continue while another app is onscreen or the iPhone is locked. It connects directly over Core Bluetooth and does not use a Pavlok account, cloud endpoint, analytics, shortcuts, schedules, remote recipients, or automatic retries.
The app remains locked until the user:
- Accepts the versioned adult/self-use safety acknowledgment.
- Selects a peripheral whose standard model-number characteristic contains
Pavlok-S. - Confirms a manual vibration test.
- Confirms one manually initiated zap at 20% or lower.
Only then does the slider unlock the full 1–100 range. Manual commands require a deliberate hold: one second through 50%, two seconds above 50%, followed by a five-second cooldown. Holding longer never repeats a command.
Mic mode uses iOS 26 SpeechAnalyzer with the downloaded US-English model, configured for Apple's responsiveness-biased fast, volatile results and a 256-frame microphone buffer. A non-listening preparation step requests permission and downloads the model independently of the Pavlok connection; a connected and fully validated Pavlok is required only before arming. The first exact live word or phrase candidate can trigger before transcription is final. Its speech segment is consumed immediately so later revisions or finalization cannot replay the command. Matching is case- and diacritic-insensitive and ignores punctuation between complete words, so yeah, I matches the trigger yeah. Substrings cannot match. Any audible person, television, speaker, or recording can match because there is no speaker identification or wake prefix, and a tentative candidate may later be revised.
Arming requires a two-second foreground hold, freezes the current trigger words and phrases and 1–100% intensity, and starts a session lasting at most 10 minutes. Once armed, microphone capture, live/finalized transcription, and the verified Bluetooth connection stay active when the app is backgrounded or the screen is locked. Every newly consumed exact live trigger candidate attempts a zap when at least one second has passed since the previous command dispatch. Matches during that one-second interval or an active write are discarded, never queued. There is no per-session attempt cap, so a 10-minute session can attempt up to roughly 600 commands. Editing the trigger list or intensity, stopping, Bluetooth loss, an audio interruption, a permission/model error, or a failed or unknown write immediately disarms the session.
The finalized session transcript accumulates in both foreground and background. It remains visible in memory after listening stops so the user can review or copy it, and is cleared explicitly, when a new session starts, or when the app terminates. Audio, transcripts, armed state, and match history are never written to persistent storage.
Background operation is bounded and best-effort. If iOS terminates the app, the session ends and is never restored or automatically rearmed. Starting a new session and changing its settings still require the foreground interface.
The Bluetooth gate also requires the standard firmware-revision value, the documented stimulus service, a vibration characteristic, and a zap characteristic that supports writes with a response. A missing or unreadable value stops validation as unsupported.
The Core Bluetooth callback is displayed as Command sent, not proof that a physical stimulus occurred. Disconnects and timeouts after writing are reported as Result unknown and are never retried automatically.
The saved Pavlok is connected and verified as soon as the app becomes active, keeping the GATT write path warm. Hold progress uses monotonic absolute deadlines rather than chained sleeps, preventing scheduler delays from accumulating across progress updates. All 100 two-byte zap packets are prepared when the Bluetooth adapter starts, and cooldown-expiry scheduling is kept out of the post-hold write path.
After an accepted manual write, the app reports:
- Dispatch: time directly from the one- or two-second hold deadline to the Core Bluetooth call.
- GATT: time from enqueueing the acknowledged write to Core Bluetooth's write callback.
- Hold end → GATT acknowledgment: a direct monotonic timestamp difference that includes every intervening gap.
After a mic-triggered attempt, correlated monotonic timestamps report:
- Recognition: spoken trigger end to the first exact live on-device candidate.
- Dispatch: live recognition to the Core Bluetooth call.
- GATT: Core Bluetooth call to its write callback.
- Trigger end → GATT acknowledgment: a direct end-to-end command-timing difference.
The physical-device recognition target is under 500 ms. This is a measured target rather than a guarantee: fastResults and volatile candidates trade recognition certainty for responsiveness, and iOS, device load, acoustics, and the downloaded model still affect timing.
The total is displayed prominently as command timing after every accepted command. GATT acknowledgment does not prove when—or whether—the electrical stimulus occurred. No self-reported or "felt" timing is collected.
- Install the current stable Xcode with the iOS 26 SDK.
- Generate
PavlokRemote.xcodeprojfromproject.ymlwith XcodeGen, or open the generated project if it is already present. - In Signing & Capabilities, choose the paid Apple Developer team and replace
com.rahul.PavlokRemoteif that bundle identifier is not available to the account. - Connect the iOS 26+ iPhone, enable Developer Mode, and run the
PavlokRemotescheme. - Keep the official Pavlok app closed during initial testing if it is holding the Bluetooth connection.
Before considering the build accepted, use the physical iPhone and Pavlok 3 to record:
- The displayed
Pavlok-Smodel and firmware revision. - Whether iOS connects directly or presents a system pairing prompt.
- A successful manual vibration confirmation.
- One explicitly initiated shock at 20% or lower.
- Reconnection after disconnect, selection when multiple Pavloks advertise, and behavior while the official Pavlok app owns the connection.
After those gates pass, validate mic mode in this order:
- Prepare the microphone permission and on-device US-English model without listening.
- Explicitly arm one low-intensity session and speak the word or phrase once.
- Confirm a second match inside one second is suppressed, a later match sends another command, and the displayed command timings update.
- Background the app and lock the screen during an explicitly armed low-intensity session; confirm the same one-second command limit remains active.
- Confirm immediate disarming on Stop and Bluetooth loss, and confirm that force-quitting never restores an armed session.
No high-intensity physical mic-mode test is required.
Stop if the observed profile or behavior differs. Do not try another packet or add a cloud fallback.
The app enables only the audio and bluetooth-central background modes needed for an already armed session. There is deliberately no network transcription fallback, credential storage, persisted audio/transcript data, persistent armed state, relaunch replay, or automatic retry.
The pure Swift module can be compiled with:
swift buildThe Xcode project includes Swift Testing unit tests and XCTest UI tests. Run the platform-independent unit suite with:
swift testThe app target, UI tests, signing, and physical Bluetooth validation require completed Xcode first-launch setup and the actual Pavlok 3. A framework-free smoke-test harness is also available for command-line toolchains that do not bundle Swift Testing:
swift run PavlokRemoteCoreChecksPavlok does not publish a native BLE SDK. The adapter is narrowly based on the community-documented Pavlok-S profile:
- Model verification: Device Information
180A, Model Number2A24, containingPavlok-S - Stimulus service:
156E1000-A300-4FEA-897B-86F698D74461 - Vibration characteristic:
00001001-0000-1000-8000-00805F9B34FB - Zap characteristic:
00001003-0000-1000-8000-00805F9B34FB - Single zap:
[0x81, intensity]
Source: buttplugio/stpihkal#137
If the physical device exposes a different model, service, characteristic, or write property, the app stops as unsupported rather than guessing another packet or falling back to the cloud.