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.
-
Ask for a login. The light on the device blinks fast.
Request Shell curl http://192.168.4.1/api/auth/requestResponse JSON { "status": "ok", "request_id": "cb3031ade64d3a86e0c1ac1cf3a1316ad45ebb085dc3c5f444e609e217e95234", "message": "Press the button on your device" } -
Press the button on the device once, within 30 seconds.
-
Ask for the result about once a second, with the
request_idfrom 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
pendingorgrantedmeans it did not work. Start again at step 1. -
Send the token in the
Authorizationheader. 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/statuswith the header tells you if your token still works.
Without a valid token, a route answers with status 401:
{"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.
{
"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.
{"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.
{
"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.
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.
{
"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:
{"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.
{"type":"sensor_update","timestamp":523164,"sensors":[0,12,140,880,1630,2210,950,40],"sum":5862} Send this to stop it:
{"type":"unsubscribe","channel":"sensors"} Every client gets two more messages, with or without the stream:
| Message | Description |
|---|---|
button_event | The event is short_press the moment the button goes down, and long_press when it is let go after 3 seconds. |
wifi_status | Sent when the home network connects or drops. It holds connected, and when connected also ip and rssi. |
{"type":"button_event","event":"short_press"} Bluetooth
The device's own services use this base UUID, with the short ID in place of XXXX:
0000XXXX-6136-4d92-9bb7-5bab7345fa61 Sensor data service: 0004
| Characteristic | Access | Description |
|---|---|---|
00000401-6136-4d92-9bb7-5bab7345fa61 | Read and notify | Sensor 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-5bab7345fa61 | Read only | Sensor sum. 2 bytes, an unsigned number with the low byte first, from 0 to 20000. |
{"timestamp":523120,"values":[0,12,140,880,1630,2210,950,40]} Button service: 0006
| Characteristic | Access | Description |
|---|---|---|
00000601-6136-4d92-9bb7-5bab7345fa61 | Notify only | Button event. Sent the moment the button goes down. |
{"event":"short_press","timestamp":523120} Device information service: 180A
| Characteristic | Name | Value |
|---|---|---|
2A24 | Model number | SynKink F |
2A26 | Firmware version | For example 1.0.3 |
2A27 | Hardware version | For example 1.24.2 |
2A50 | PnP ID | 7 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/enableandPOST /api/hid/disable. - To start in DID mode after every restart, call
POST /api/config/hid-startupwith 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.
DELETE /api/hid/trigger?id=0
Clears one slot.
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.
{
"events": [
{
"timestamp": 523114220,
"trigger": 0,
"description": "Click -> Key 0x2C (mod 0x00)"
}
]
} POST /api/hid/test/stop
Ends test mode.
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_type | Name | When it fires |
|---|---|---|
0 | Rise | The sum was at or below pressure_min a moment ago and is now at or above pressure_max. |
1 | Fall | The sum was at or above pressure_max a moment ago and is now at or below pressure_min. |
2 | Hold | The sum stays at or above pressure_min for pressure_time_window_ms. |
3 | Release | The sum drops below pressure_min after it was at or above pressure_max. |
4 | Above | The sum is at or above pressure_min. |
5 | Button press | The button goes down. This also fires at the start of every long press. Use slots 8 to 10. |
7 | Long button press | The button is let go after 3 seconds and before 5. Use slots 8 to 10. |
Fields
| Field | Type | Description |
|---|---|---|
id | number | The slot. Use 0 to 7 for pressure triggers and 8 to 10 for button triggers. |
trigger_type | number | When the trigger fires. See the trigger types above. |
pressure_min | number | From 0 to 20000. |
pressure_max | number | From 0 to 20000. |
pressure_time_window_ms | number | From 1 to 10000. Every pressure trigger needs it, even when its type does not use it. |
output_type | number | 0 sends a key. 1 sends a mouse click. |
key_code | number | The 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_keys | number | Adds Ctrl, Shift, Alt or the Windows or Command key. Add the values below to combine keys. |
mouse_button | number | The button to click when output_type is 1: 1 for left, 2 for right and 4 for middle. |
repeat_enabled | boolean | With 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_ms | number | From 1 to 5000. |
repeat_delay_ms | number | How 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
| Key | Left | Right |
|---|---|---|
| Ctrl | 1 | 16 |
| Shift | 2 | 32 |
| Alt | 4 | 64 |
| Windows or Command | 8 | 128 |
Example
Save a trigger that presses Space on a quick thrust:
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:
{
"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.
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.
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())