Skip to content

Repository files navigation

Pavlok Remote

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.

Safety and validation

The app remains locked until the user:

  1. Accepts the versioned adult/self-use safety acknowledgment.
  2. Selects a peripheral whose standard model-number characteristic contains Pavlok-S.
  3. Confirms a manual vibration test.
  4. 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.

Latency

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.

Build and install

  1. Install the current stable Xcode with the iOS 26 SDK.
  2. Generate PavlokRemote.xcodeproj from project.yml with XcodeGen, or open the generated project if it is already present.
  3. In Signing & Capabilities, choose the paid Apple Developer team and replace com.rahul.PavlokRemote if that bundle identifier is not available to the account.
  4. Connect the iOS 26+ iPhone, enable Developer Mode, and run the PavlokRemote scheme.
  5. 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-S model 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:

  1. Prepare the microphone permission and on-device US-English model without listening.
  2. Explicitly arm one low-intensity session and speak the word or phrase once.
  3. Confirm a second match inside one second is suppressed, a later match sends another command, and the displayed command timings update.
  4. Background the app and lock the screen during an explicitly armed low-intensity session; confirm the same one-second command limit remains active.
  5. 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.

Verification

The pure Swift module can be compiled with:

swift build

The Xcode project includes Swift Testing unit tests and XCTest UI tests. Run the platform-independent unit suite with:

swift test

The 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 PavlokRemoteCoreChecks

Protocol evidence

Pavlok 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 Number 2A24, containing Pavlok-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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages