Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thirty-grapes-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zendrex/buttplug.js": patch
---

version bump
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,4 @@ docs/.source/
docs/out/
docs/node_modules/
docs/bun.lock
docs/next-env.d.ts
docs/next-env.d.ts
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
"source.fixAll.biome": "explicit"
},

// Hidden
"files.exclude": {
"**/node_modules": true,
"**/.husky": true,
"**/.turbo": true,
"**/.git": true
},

// Language Specific / Biome
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
Expand Down
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# @zendrex/buttplug.js

TypeScript client for the [Buttplug](https://buttplug.io) protocol v4. Connect to [Intiface Central](https://intiface.com/central/), discover devices, and control them with a type-safe API.
Modern TypeScript client for the [Buttplug](https://buttplug.io) intimate hardware protocol v4. Connect to [Intiface Central](https://intiface.com/central/), discover devices, and control them with a type-safe API.

## Features

- Buttplug protocol v4 over WebSocket
- Full Buttplug protocol v4 implementation over WebSocket
- 10 output types — vibration, rotation, position, oscillation, constriction, temperature, LED, spray, and more
- 5 sensor types — battery, RSSI, pressure, button, position (one-shot reads and subscriptions)
- Pattern engine with 7 built-in presets, custom keyframes, and easing curves
- Auto-reconnect with exponential backoff
- Zod-validated protocol messages
- Zod-validated protocol messages with full type inference
- ESM and CJS dual-package output with `.d.ts` types
- Zero config — point at Intiface Central and go

## Prerequisites
Expand Down Expand Up @@ -58,6 +59,7 @@ await client.connect();
await client.startScanning();
await client.stopAll();
await client.disconnect();
client.dispose(); // cleanup listeners and internal state

client.connected; // boolean
client.devices; // Device[]
Expand Down Expand Up @@ -120,7 +122,17 @@ engine.stopAll();
engine.dispose();
```

**Presets:** `pulse`, `wave`, `ramp_up`, `ramp_down`, `heartbeat`, `surge`, `stroke`
**Presets:**

| Preset | Description | Loops |
|--------|-------------|-------|
| `pulse` | Square wave on/off | yes |
| `wave` | Smooth sine wave oscillation | yes |
| `ramp_up` | Gradual increase to maximum | no |
| `ramp_down` | Gradual decrease to zero | no |
| `heartbeat` | Ba-bump heartbeat rhythm | yes |
| `surge` | Build to peak then release | no |
| `stroke` | Full-range position strokes | yes |

**Easings:** `linear`, `easeIn`, `easeOut`, `easeInOut`, `step`

Expand Down Expand Up @@ -149,6 +161,19 @@ const client = new ButtplugClient("ws://127.0.0.1:12345", {

On reconnection, the client re-handshakes, reconciles the device list, and emits `reconnected`. The pattern engine automatically stops patterns for removed devices.

### Cleanup

```typescript
// Graceful shutdown
await client.disconnect();

// Release all internal state and event listeners
client.dispose();

// Pattern engine cleanup
engine.dispose();
```

## Documentation

Full API reference and guides are available in the [`docs/`](./docs) directory. To run locally:
Expand Down
30 changes: 8 additions & 22 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions docs/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,16 @@ export default function HomePage() {
<div className="mt-2 flex flex-wrap justify-center gap-3">
<Link
className="inline-flex items-center gap-2 rounded-lg bg-fd-primary px-6 py-3 font-medium text-fd-primary-foreground transition-colors hover:bg-fd-primary/90"
href="/docs"
href="/docs/guide"
>
Get Started
<ChevronRight className="size-4" />
</Link>
<Link
className="inline-flex items-center gap-2 rounded-lg border border-fd-border px-6 py-3 font-medium transition-colors hover:bg-fd-accent"
href="/docs/client/api/client"
href="https://github.com/zendrex/buttplug.js"
>
API Reference
GitHub
</Link>
</div>
<div className="mt-6 text-left">
Expand Down Expand Up @@ -137,16 +137,16 @@ export default function HomePage() {
<div className="flex flex-wrap justify-center gap-3">
<Link
className="inline-flex items-center gap-2 rounded-lg bg-fd-primary px-6 py-3 font-medium text-fd-primary-foreground transition-colors hover:bg-fd-primary/90"
href="/docs/client/getting-started"
href="/docs/guide"
>
Read the Guide
<ChevronRight className="size-4" />
</Link>
<Link
className="inline-flex items-center gap-2 rounded-lg border border-fd-border px-6 py-3 font-medium transition-colors hover:bg-fd-accent"
href="/docs/patterns"
href="https://github.com/zendrex/buttplug.js"
>
Explore Patterns
GitHub
</Link>
</div>
</section>
Expand Down
8 changes: 3 additions & 5 deletions docs/app/docs/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,17 @@ import type { Metadata } from "next";

import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/layouts/docs/page";
import { createRelativeLink } from "fumadocs-ui/mdx";
import { notFound, redirect } from "next/navigation";
import { notFound } from "next/navigation";

import { CopyMarkdownButton } from "@/components/copy-markdown-button";
import { source } from "@/lib/source";
import { getMDXComponents } from "@/mdx-components";

export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
const params = await props.params;
if (!params.slug || params.slug.length === 0) {
redirect("/docs/client");
}
const slug = params.slug ?? [];

const page = source.getPage(params.slug);
const page = source.getPage(slug);
if (!page) {
notFound();
}
Expand Down
19 changes: 1 addition & 18 deletions docs/app/docs/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,11 @@
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import { AudioWaveform, Cable } from "lucide-react";

import { baseOptions } from "@/lib/layout.shared";
import { source } from "@/lib/source";

export default function Layout({ children }: LayoutProps<"/docs">) {
return (
<DocsLayout
tree={source.getPageTree()}
{...baseOptions()}
sidebar={{
tabs: {
transform(option, _node) {
if (option.url === "/docs/client") {
return { ...option, icon: <Cable /> };
}
if (option.url === "/docs/patterns") {
return { ...option, icon: <AudioWaveform /> };
}
return option;
},
},
}}
>
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
{children}
</DocsLayout>
);
Expand Down
22 changes: 22 additions & 0 deletions docs/app/llms-full.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { source } from "@/lib/source";

export const revalidate = false;

export async function GET(): Promise<Response> {
const pages = source.getPages();
const sections: string[] = [];

for (const page of pages) {
const title = page.data.title;
const description = page.data.description ?? "";
const content = await page.data.getText("processed");

const section = [`# ${title}`, description ? `\n> ${description}` : "", "", content].join("\n");

sections.push(section);
}

return new Response(sections.join("\n\n---\n\n"), {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
27 changes: 27 additions & 0 deletions docs/app/llms.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { source } from "@/lib/source";

export const revalidate = false;

export function GET(): Response {
const pages = source.getPages();

const lines = [
"# buttplug.js",
"",
"> TypeScript client for the Buttplug Intimacy Protocol v4",
"",
"## Pages",
"",
];

for (const page of pages) {
const title = page.data.title;
const description = page.data.description ?? "";
const url = page.url;
lines.push(`- [${title}](${url})${description ? `: ${description}` : ""}`);
}

return new Response(lines.join("\n"), {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
Loading