Idåsen Controller

TL;DR I made a script to control my Ikea Idåsen standing desk. See it here.

A while ago I bought an Ikea Idåsen standing desk. The desk is motorised and can be controlled via a small switch that you mount to the underside of the desk. The switch can be moved up or down to move the desk in the same direction. The desk can also be controlled using a phone app, via bluetooth. The components that are responsible for moving the desk up and down (the switch, controller, actuators, and the app) are made by a company called Linak who make quite a large variety of these for desks and apparently beds as well.

<p>The Idåsen desk</p>

The Idåsen desk

The Linak Desk Control App

The app is well designed and functional but offers very little more than the physical switch. It has two controls that allow you to move the desk up and down which work like the physical switch i.e. it moves while you are pressing it. It also reports the current height of the desk above the floor in centimetres and allows you to save 3 heights as 'favourites'. To move the desk to a favourite position you must click and hold the favourite button and the desk will move towards the favourited height and stop once it gets there. I dislike needing to keep the button pressed. If the desk and app know it's current height and a target height then it should be able to move there with a single click. It seems like some of Linak's actuator/control models offer this functionality (they call it autodrive) but the ones included in the Ikea Idåsen do not. It's possible they reserve the autodrive feature in the app for desks that have physical buttons allowing autodrive. I assume this is either for consistency or to ensure you can't get away with buying the cheaper models and using the app to get functionality equal to the more expensive ones.

<p>The Linak Desk Control app</p>

The Linak Desk Control app

The other strange thing about the app is that it is for phones exclusively. Considering most standing desks are going to be used with desktop computers it seems like it would be a more natural workflow to control the desk from there. Linak do make some kind of desktop software to control their desks but it will only work over a cable connection (that you need to buy seperately) and it is only for Windows/Mac (fair enough but I use Ubuntu). I decided that it was surely possible to communicate directly with the desk, via bluetooth, from my computer.

Bluetooth LE and GATT

I had a couple of failed attempts at this project because I didn't know where to begin. This time however I stumbled upon the Linak Desk App which was a very useful starting point. That app is a full QT gui for controlling Linak desks but unfortunately there seem to be enough differences with the Ikea version that it didn't work. After playing around with it I decided it was probably easier to start from scratch myself than try to modify it (especially as I did not need a gui).

It was not entirely useless though as it demonstrated roughly how to communicate with these desks. They communicate over Bluetooth Low Energy using the GATT protocol. GATT is quite complex and I do not understand much of it but the useful bit is that the desk advertises characteristics. These are like addresses that bytes can be written to and read from, and you can request notifications of changes to the characteristic value. There are also services (which are supposed to be collections of similar characteristics) and descriptors (which describe the characteristic value e.g. by providing a human readable description) but they did not end up being relevant to getting this working.

The desk was happy to connect and pair with my computer like any other bluetooth device but clearly there are no desk drivers built into Ubuntu so you can't do anything more. There are a few Python libraries for using GATT and I started using gatt-python. Using this I was able to list the services, characteristics, and descriptors available on the desk controller. They advertise some properties like a UUID and permissions (read, write, write and notify). So I'm connected and have a list of addresses to write to but I have no idea what functionality each of these addresses corresponds to, or what sort of data they are expecting. I know where to find out though.

Decompiled Linak App

I searched around and found that it was possible to decompile Android apps. I pulled the Linak Desk Control app apk from my phone using adb and then used a program called BytecodeViewer to look through it. I was pleasantly surprised by how readable the resulting Java code was. It seemed like variable names and any comments were missing but class and method names were preserved. I don't have much Java experience, but I knew enough to be able to trace the logic I was interested in.

First I searched for the characteristic UUIDs to see if I could identify what they were for. I found them defined like this:

public enum LinakServices$Characteristic$Control implements Characteristics {
  COMMAND(UUID.fromString("99FA0002-338A-1024-8A49-009C0215F78A"))
}

It doesn't take much thinking to work out that COMMAND might the characteristic to send commands to.

The slightly more confusing one was REFERENCE_OUTPUT:

public enum LinakServices$Characteristic$ReferenceOutput implements Characteristics {
  DETECT_MASK,
  EIGHT(UUID.fromString("99FA0028-338A-1024-8A49-009C0215F78A")),
  FIVE(UUID.fromString("99FA0025-338A-1024-8A49-009C0215F78A")),
  FOUR(UUID.fromString("99FA0024-338A-1024-8A49-009C0215F78A")),
  MASK(UUID.fromString("99FA0029-338A-1024-8A49-009C0215F78A")),
  ONE(UUID.fromString("99FA0021-338A-1024-8A49-009C0215F78A")),
  SEVEN(UUID.fromString("99FA0027-338A-1024-8A49-009C0215F78A")),
  SIX(UUID.fromString("99FA0026-338A-1024-8A49-009C0215F78A")),
  THREE(UUID.fromString("99FA0023-338A-1024-8A49-009C0215F78A")),
  TWO(UUID.fromString("99FA0022-338A-1024-8A49-009C0215F78A"));
}

My desk actually only advertised ONE as a characteristic and I found that it could be subsrcribed to to recieve the current height of the desk when it moved. I actually found that by just subscribing to all the characteristics and seeing if any of them made a notification when the desk moved, so seeing it in the decompiled code was confirmation.

I now knew what the characteristics did so I had to find out what to send them. I found where the app was sending commands to the desk:

public void moveUp() {
  this.getDevice().sendLinakCommand(LinCommand.REF_1_UP);
}

I followed this through but the only additional thing that got added was the mac address of the device and then it used some BLE library to do the actual communication. I looked up the LinCommand object and found it to be a list of consts.

public static final LinCommand NEXT_MASSAGE_MODE = new LinCommand(129);
public static final LinCommand REF_1_DOWN = new LinCommand(70);
public static final LinCommand REF_1_UP = new LinCommand(71);
public static final LinCommand REF_2_DOWN = new LinCommand(72);

There are over a hundred commands. Lots of them for Linak beds it seemed (massage mode etc.) and quite a few duplicate commands for difference REFs which I think might be a way to control multiple actuators with one device. The relevent ones for my desk were simply REF_1_UP, and REF_1_DOWN.

I looked into the LinCommand class to see how this number was encoded and as far as I could tell it was just doing some implicit byte array conversion:

public LinCommand(byte[] var1) {
  this.bytes = var1;
}

At this point I tried writing bytes to the command characterisitic. I could have looked up what the default Java behaviour is here but in the end I ended up sending different encodings of 71 until I found the one that made the desk move up, which was sending it as an unsigned short.

Controlling the Desk

I could now send move commands, and read the current height of the desk so I could play around with controlling it and the desk behaviour is interesting.

Sending move commands to the desk seems to make the motors run for about one second in the desired direction. If another move command is sent within that second then the motion continues with no slowing or stopping. If no move command is recieved in that second then the motor slows down towards the end and then stops. If you send a move command late, then there will some stuttering as the desk may have already started to slow the motors. You can stop the motion part way through by sending a stop command though it sometimes does not respond immediately. As the desk moves it sends notifications of the current height to a characteristic. This can be monitored to work out when to stop moving, but it also seems to be a little bit slow and the final notified value is often not the same as the actual final value if a measuremment is made at rest.

The height values the desk provides are in 10ths of a millimetre, and correspond to the height above the desks lowest setting i.e. if you lower the desk as far as it will go then the desk will report its height as being zero. The minimum raw height value is zero and the maximum height is 6500. This corresponds to a range of 620mm to 1270mm off the floor.

The desk appears to be pretty good at not doing anything stupid if you send it stupid commands. It won't try to go below the minimum height or above the maximum height and it doesn't do much if you send lots of commands in quick succession. The usual hit detection works, and it will stop moving if it hits an object and will not respond to further commands until a stop command is sent.

Conclusion

The Python script I made can be seen in the github repo. It takes some config to set the mac address, stand height, and sit height and then when supplied with either the --sit or --stand argument it moves desk.

You can trigger the script however you like an as long as the desk has been paired it doesn't need to be connected, the script should reconnect. I use the albert launcher along with two .desktop files to allow me to trigger this script from the launcher. The desktop files for this are:

[Desktop Entry]
Name=Desk - Sit
Exec=/home/user/idasen-controller/venv/bin/python /home/user/idasen-controller/main.py --sit
Icon=/home/user/idasen-controller/sit-icon.png
Type=Application
Comment=Lower desk to sitting height.
[Desktop Entry]
Name=Desk - Stand
Exec=/home/user/idasen-controller/venv/bin/python /home/user/idasen-controller/main.py --stand
Icon=/home/user/idasen-controller/stand-icon.png
Type=Application
Comment=Lower desk to standing height.