The IKEA IDÅSEN standing desk uses a LINAK motor system with Bluetooth Low Energy control. The official LINAK Desk Control app is slow, buggy, and requires pulling out your phone every time you want to change height. So I built a Windows desktop controller instead.

This was a Friday evening project using Claude Code. About 300 lines of Python, one evening of work.

Finding the Desk

The first step was scanning for BLE devices using Python’s bleak library. The desk shows up as “Desk 4166” and pairs with Windows like any other Bluetooth device. Nothing special here.

The Protocol

The IDÅSEN uses proprietary LINAK GATT services with UUIDs in the 99fa0001 through 99fa0030 range. The community has already reverse-engineered most of this, but not every command works on every desk variant. Here’s what actually works on the IDÅSEN:

Characteristic Command Action
99fa0002 0x47, 0x00 Move UP
99fa0002 0x46, 0x00 Move DOWN
99fa0002 0xFF, 0x00 STOP
99fa0021 (read/notify) Current height

Height comes as 2 bytes, little-endian, in 0.1mm units with a 620mm base offset. So a raw value of 1000 means 620 + 100 = 720mm.

One notable finding: the “reference input” characteristic (99fa0031) that some LINAK desks use for absolute position targeting does nothing on the IDÅSEN. You only get UP, DOWN, and STOP. This matters a lot for what comes next.

The GUI

A dark-themed tkinter app with:

  • Live height display updated via BLE notifications
  • Customizable preset buttons (stored in a JSON config file)
  • A big red STOP button, plus Space/ESC keyboard shortcuts
  • Right-click to edit or delete presets
  • Auto-reconnect when the connection drops

Standard stuff. The interesting part is making the presets actually work precisely.

The Hard Part: Precision Positioning

With only UP/DOWN/STOP commands and no absolute positioning, hitting a target height is a controls problem. The desk motors have momentum. When you send STOP, the desk keeps moving for a bit. BLE command latency is around 100ms on top of that.

I went through several approaches:

Attempt 1: Move and stop at target. Overshoots by 1-2cm every time.

Attempt 2: Predictive coasting. Stop early and let the desk coast to the target. Didn’t work. The desk brakes hard instead of coasting, so the stopping distance is unpredictable.

Attempt 3: Zone-based slowdown. Define zones approaching the target and somehow slow down. Better, but inconsistent because the only speed control is rapid start/stop cycling, which the desk doesn’t respond well to.

What actually worked: Velocity-based stopping with calibration.

A calibration script runs 5 trial moves in each direction, measuring how far the desk travels after receiving a STOP command at different velocities. This produces a stopping factor: velocity × factor = stopping_distance. The calibrated values ended up being 0.205 for upward movement and 0.260 for downward (the desk overshoots more going down, probably gravity).

The app uses these factors to predict when to send STOP so the desk lands on target. It also does gentle ongoing learning: if the final position is off by more than 3mm, it adjusts the factor by 0.05% and saves it to config. Over time, the predictions get better.

Micro-Pulse Fine-Tuning

After the main move, if the position is off by more than 0.5mm, the app enters a fine-tuning phase. It sends ultra-short BLE command pulses (12ms) followed by an immediate STOP, nudging the desk in tiny increments. Each pulse moves the desk roughly 0.2-0.5mm. It repeats until the height matches the target.

This two-phase approach (calibrated prediction for the bulk move, micro-pulses for the last millimeter) gets consistent sub-millimeter accuracy from a motor that only understands on and off.

Stack

  • Python 3.12
  • bleak for BLE communication
  • tkinter for the GUI
  • asyncio + threading to keep BLE operations off the UI thread
  • JSON file for config and presets
  • .bat file pinned to the taskbar for one-click launch

Takeaway

The Bluetooth protocol is trivial. Three commands. The real challenge is precise positioning with a binary motor and ~100ms network latency. This is fundamentally a controls engineering problem, and the solution is the same one real control systems use: measure the plant’s actual behavior, build a model from empirical data, and keep refining it. Trying to predict stopping distance from theory was a waste of time compared to just running calibration trials and measuring what happens.

References

The LINAK BLE protocol was reverse-engineered by the community. These were helpful:

Source code: KhalSCI/linak_desk_control