Skip to content
On this page

Developers

SynKink F developer guide

SynKink F talks to your code directly. Over Wi-Fi it has REST routes and a live WebSocket stream. Over Bluetooth it streams the same sensor data. In DID mode it becomes a Bluetooth keyboard and mouse. Everything here runs on your own network. You do not need an account.

The device has 8 pressure sensors along the sleeve. Each one reports a value from 0 (no pressure) to 2500. The sum of all 8 runs from 0 to 20000. From these values you can tell how deep, how fast and how hard each thrust is.

Connect

Wi-Fi

SynKink F always runs its own open Wi-Fi network, called SynKink-F by default. Join it and the device is at http://192.168.4.1. All Wi-Fi routes use plain HTTP on port 80.

To use it on your home network, send your Wi-Fi name and password with POST /api/wifi/connect. The setup network stays on after that.

On your home network, the device gives your router the name SynKink-F. Many routers let you reach it as http://SynKink-F. If yours does not, find its address with GET /api/wifi/status on the setup network.

The device takes 4 connections at once, WebSocket included. A fifth closes the one used longest ago, and that can be an open WebSocket. Keep yours few.

Bluetooth

In normal mode, SynKink F shows up as SynKink F. Its advertising data holds company ID 0x0FD5, then the bytes 01 00. The 01 marks SynKink F. Look for the company ID and you find the device even after a rename.

You do not need to pair to read data. Connect and subscribe. One app can connect over Bluetooth at a time.

In DID mode it shows up as SynKink F DID and pairs like any keyboard, with no code.

Get a token

Most REST routes need a token. You get one when someone presses the button on the device. This keeps other people on your network out.

  1. Ask for a login. The light on the device blinks fast.

    Request Shell
    curl http://192.168.4.1/api/auth/request
    Response JSON
    {
      "status": "ok",
      "request_id": "cb3031ade64d3a86e0c1ac1cf3a1316ad45ebb085dc3c5f444e609e217e95234",
      "message": "Press the button on your device"
    }
  2. Press the button on the device once, within 30 seconds.

  3. Ask for the result about once a second, with the request_id from step 1.

    Request Shell
    curl "http://192.168.4.1/api/auth/result?id=cb3031ade64d3a86e0c1ac1cf3a1316ad45ebb085dc3c5f444e609e217e95234"
    Response while waiting JSON
    {"state":"pending"}
    Response after the press JSON
    {"state":"granted","token":"d6941d514d2585707d4aa7c1a71cde4bd8b7dfa879f263a1dbdcb3f81bf14a51"}

    Any state other than pending or granted means it did not work. Start again at step 1.

  4. Send the token in the Authorization header. Send the token only, with no word in front of it.

    Request Shell
    curl -H "Authorization: d6941d514d2585707d4aa7c1a71cde4bd8b7dfa879f263a1dbdcb3f81bf14a51" http://192.168.4.1/api/sensors/current
  • The device shows the token once. Save it.
  • A token stops working after 10 minutes without use. A restart ends all tokens.
  • Up to 4 tokens work at once. A fifth replaces the one used longest ago.
  • Only one login can wait for the button at a time. A second one gets status 409.
  • GET /api/auth/status with the header tells you if your token still works.

Without a valid token, a route answers with status 401:

Response JSON
{"status":"error","error":"Unauthorized - authentication required"}

REST routes

Use http://192.168.4.1 on the setup network, or the device address on your home network. Requests and answers are JSON. Bodies can be up to 2048 bytes. When a call fails, the answer has "status":"error" and a short reason.

Answers can hold more fields than shown here, so skip the ones you do not need.

Call these routes from a script or an app, not from a web page. Browsers send an extra check before a call with a token or a JSON body. The device rejects it, so the call fails.

Device

GET /api/status No token

Model, firmware version, device ID, name and Wi-Fi state.

Response JSON
{
  "uptime": 523,
  "free_heap": 143212,
  "firmware_version": "1.0.3",
  "hardware_version": "1.24.2",
  "model": "SynKink F",
  "product": "euphoria_f1",
  "device_id": "3485189A2B10",
  "device_name": "SynKink F",
  "ble_enabled": true,
  "captive_enabled": true,
  "wifi_connected": true
}

GET /api/firmware/info

Firmware version as text and as numbers, and the flash slots it runs from.

POST /api/control/calibrate

Sets the current readings as zero. Do this with no pressure on the sleeve. The device also does it each time it starts. In normal mode, two button presses less than 1 second apart do it too. Do not press the button twice in a row while the sleeve is under pressure.

POST /api/control/restart

Restarts the device after 3 seconds.

Sensors

GET /api/sensors/current

One reading of all 8 sensors and their sum. For a live feed, use the WebSocket.

Here and in the live streams, timestamp is in milliseconds since the device started.

Response JSON
{"timestamp":523114,"sensors":[0,12,140,880,1630,2210,950,40],"sum":5862}

Settings

GET /api/config

All settings: names, sound, sleep and DID start.

The settings, as they are on a new device.

Response JSON
{
  "device": {
    "name": "SynKink F",
    "ble_name": "SynKink F",
    "ble_hid_name": "SynKink F DID",
    "ap_name": "SynKink-F",
    "hostname": "SynKink-F"
  },
  "audio": {
    "mute": false
  },
  "sleep": {
    "timeout": 600000,
    "enabled": true,
    "sensitivity": 5
  },
  "hid_startup": {
    "enabled": false
  }
}

POST /api/config/device

Change names. Send any of name, ble_name, ble_hid_name, ap_name and hostname. Keep names at 20 letters or fewer. The Bluetooth scan name comes from name, or from ble_hid_name in DID mode. Some names change after a restart. A new ble_name or ble_hid_name forgets all paired Bluetooth devices.

POST /api/config/audio

Send mute as true or false to turn the beeps off or on.

POST /api/config/sleep

Send timeout in milliseconds, enabled, and sensitivity from 0 to 100. With no movement for timeout, the device turns off. Movement means the sum changes by at least sensitivity percent of 20000. Each request with a valid token also counts as use.

POST /api/config/hid-startup

Send enabled as true to start in DID mode after each restart.

Wi-Fi

GET /api/wifi/scan

Starts a scan for networks. Then call /api/wifi/scan?status=1 until state is complete or failed. A failed scan has an error message. Each network has ssid, rssi, auth and supported.

POST /api/wifi/connect

Joins your home network and saves it. Send ssid and password. You can also send hostname.

Request Shell
curl -X POST http://192.168.4.1/api/wifi/connect \
  -H "Authorization: d6941d514d2585707d4aa7c1a71cde4bd8b7dfa879f263a1dbdcb3f81bf14a51" \
  -H "Content-Type: application/json" \
  -d '{"ssid":"Home","password":"your-wifi-password"}'

GET /api/wifi/status No token

Shows connected and connecting. When connected, it also shows ssid, ip, rssi and hostname. After a failed try, error is wrong_password, not_found or failed.

Response after joining JSON
{
  "connected": true,
  "connecting": false,
  "ssid": "Home",
  "ip": "192.168.1.42",
  "rssi": -58,
  "hostname": "SynKink-F"
}

DELETE /api/wifi/disconnect

Leaves the home network until the next restart.

Live stream over WebSocket

Open ws://192.168.4.1/ws, or the same path on the home network address.

No token is needed.

Send this to start the sensor stream:

Send JSON
{"type":"subscribe","channel":"sensors"}

You then get a sensor_update every 50 milliseconds, so 20 a second. sensors holds the 8 values. sum adds them up.

Receive JSON
{"type":"sensor_update","timestamp":523164,"sensors":[0,12,140,880,1630,2210,950,40],"sum":5862}

Send this to stop it:

Send JSON
{"type":"unsubscribe","channel":"sensors"}

Every client gets two more messages, with or without the stream:

MessageDescription
button_eventThe event is short_press the moment the button goes down, and long_press when it is let go after 3 seconds.
wifi_statusSent when the home network connects or drops. It holds connected, and when connected also ip and rssi.
Receive JSON
{"type":"button_event","event":"short_press"}

Bluetooth

The device's own services use this base UUID, with the short ID in place of XXXX:

Base UUID
0000XXXX-6136-4d92-9bb7-5bab7345fa61

Sensor data service: 0004

CharacteristicAccessDescription
00000401-6136-4d92-9bb7-5bab7345fa61Read and notifySensor values. Once you subscribe, the device sends this JSON text every 20 milliseconds, so 50 times a second. Each message is up to 75 bytes, so use an MTU of 78 or more. The device accepts up to 256.
00000402-6136-4d92-9bb7-5bab7345fa61Read onlySensor sum. 2 bytes, an unsigned number with the low byte first, from 0 to 20000.
Sensor values JSON
{"timestamp":523120,"values":[0,12,140,880,1630,2210,950,40]}

Button service: 0006

CharacteristicAccessDescription
00000601-6136-4d92-9bb7-5bab7345fa61Notify onlyButton event. Sent the moment the button goes down.
Button event JSON
{"event":"short_press","timestamp":523120}

Device information service: 180A

CharacteristicNameValue
2A24Model numberSynKink F
2A26Firmware versionFor example 1.0.3
2A27Hardware versionFor example 1.24.2
2A50PnP ID7 bytes: 01 D5 0F 01 00 00 01

DID keyboard mode

In DID mode, SynKink F works as a Bluetooth keyboard and mouse. Your thrusts and the button send key presses or mouse clicks. It works with any computer or phone that takes a Bluetooth keyboard.

Turn it on and off

  • Hold the button for 5 seconds and DID mode turns on or off. Let go before 7 seconds, because at 7 seconds the device turns off.
  • Or call POST /api/hid/enable and POST /api/hid/disable.
  • To start in DID mode after every restart, call POST /api/config/hid-startup with enabled set to true.

While DID mode is on, Bluetooth sends only keys and clicks. The Bluetooth sensor stream stops, but Wi-Fi, the REST routes and the WebSocket keep working.

Button presses go to your triggers. Pair from the Bluetooth settings of your computer or phone.

Routes

GET /api/hid/status

Shows enabled, connected (a computer or phone is connected) and device_name.

POST /api/hid/enable

Turns DID mode on.

POST /api/hid/disable

Turns DID mode off.

GET /api/hid/config

Shows enabled, active_trigger_count and triggers, a list of all 11 slots.

POST /api/hid/trigger

Saves one slot and turns it on. Fields you leave out are saved as 0. A value out of range returns 500.

POST /api/hid/test/start

Test mode. Triggers are logged but no keys are sent. DID mode must be on.

GET /api/hid/test/events

The last 50 test events. Each timestamp is in microseconds since the device started. Stop and start test mode to clear the list.

Response JSON
{
  "events": [
    {
      "timestamp": 523114220,
      "trigger": 0,
      "description": "Click -> Key 0x2C (mod 0x00)"
    }
  ]
}

GET /api/control/list-bonds

Lists the Bluetooth addresses of paired devices.

POST /api/control/forget-bonds

Forgets all paired devices.

Triggers

A trigger turns pressure or a button press into a key or a click. There are 11 slots, numbered 0 to 10. Slots 0 to 7 are pressure triggers and use the sum of all 8 sensors. Slots 8 to 10 are button triggers.

Trigger types

trigger_typeNameWhen it fires
0RiseThe sum was at or below pressure_min a moment ago and is now at or above pressure_max.
1FallThe sum was at or above pressure_max a moment ago and is now at or below pressure_min.
2HoldThe sum stays at or above pressure_min for pressure_time_window_ms.
3ReleaseThe sum drops below pressure_min after it was at or above pressure_max.
4AboveThe sum is at or above pressure_min.
5Button pressThe button goes down. This also fires at the start of every long press. Use slots 8 to 10.
7Long button pressThe button is let go after 3 seconds and before 5. Use slots 8 to 10.

Fields

FieldTypeDescription
idnumberThe slot. Use 0 to 7 for pressure triggers and 8 to 10 for button triggers.
trigger_typenumberWhen the trigger fires. See the trigger types above.
pressure_minnumberFrom 0 to 20000.
pressure_maxnumberFrom 0 to 20000.
pressure_time_window_msnumberFrom 1 to 10000. Every pressure trigger needs it, even when its type does not use it.
output_typenumber0 sends a key. 1 sends a mouse click.
key_codenumberThe key to send when output_type is 0. A USB keyboard key code from 0 to 101. For example, 4 is A, 44 is Space and 79 is Right Arrow. Media keys are not supported.
modifier_keysnumberAdds Ctrl, Shift, Alt or the Windows or Command key. Add the values below to combine keys.
mouse_buttonnumberThe button to click when output_type is 1: 1 for left, 2 for right and 4 for middle.
repeat_enabledbooleanWith true, the device sends the key again every repeat_interval_ms, after repeat_delay_ms. A repeating pressure trigger stays on while the sum is between pressure_min and pressure_max. A pressure_max of 0 means no upper limit.
repeat_interval_msnumberFrom 1 to 5000.
repeat_delay_msnumberHow long the trigger stays on before the repeats start, in milliseconds.

A key or click stays down while the trigger is on, then lets go. For a button press trigger, it stays down while you hold the button.

Modifier keys

KeyLeftRight
Ctrl116
Shift232
Alt464
Windows or Command8128

Example

Save a trigger that presses Space on a quick thrust:

Request Shell
curl -X POST http://192.168.4.1/api/hid/trigger \
  -H "Authorization: d6941d514d2585707d4aa7c1a71cde4bd8b7dfa879f263a1dbdcb3f81bf14a51" \
  -H "Content-Type: application/json" \
  -d '{"id":0,"name":"Click","trigger_type":0,"output_type":0,"key_code":44,"pressure_min":100,"pressure_max":400,"pressure_time_window_ms":50}'

That slot then looks like this in GET /api/hid/config:

Slot in the response JSON
{
  "enabled": true,
  "name": "Click",
  "trigger_type": 0,
  "output_type": 0,
  "key_code": 44,
  "modifier_keys": 0,
  "mouse_button": 0,
  "repeat_enabled": false,
  "repeat_interval_ms": 0,
  "repeat_delay_ms": 0,
  "pressure_min": 100,
  "pressure_max": 400,
  "pressure_time_window_ms": 50,
  "button_press_min_ms": 0,
  "button_press_max_ms": 0,
  "button_multi_window_ms": 0
}

Examples

Read the live stream over WebSocket

This runs in Node.js 22 or newer. Use the home network address if you are not on the setup network.

stream.js JavaScript
const ws = new WebSocket('ws://192.168.4.1/ws')

ws.onopen = () => {
  ws.send(JSON.stringify({ type: 'subscribe', channel: 'sensors' }))
}

ws.onmessage = (event) => {
  const message = JSON.parse(event.data)
  if (message.type === 'sensor_update') {
    console.log(message.timestamp, message.sensors, message.sum)
  }
}

Read the live stream over Bluetooth

This runs in Python 3 with the bleak package (pip install bleak). It finds SynKink F by its company ID and prints readings for 30 seconds.

stream.py Python
import asyncio
import json

from bleak import BleakClient, BleakScanner

SENSOR_VALUES = "00000401-6136-4d92-9bb7-5bab7345fa61"


def on_values(_, data):
    reading = json.loads(data)
    print(reading["timestamp"], reading["values"])


async def main():
    device = await BleakScanner.find_device_by_filter(
        lambda _, adv: 0x0FD5 in adv.manufacturer_data
    )
    async with BleakClient(device) as client:
        await client.start_notify(SENSOR_VALUES, on_values)
        await asyncio.sleep(30)


asyncio.run(main())