The Dreamdesk Project


The aim of this project was to create a framework that makes compatible any electrical standing desk with a home automation solution like Apple Homekit, Google Home, or Amazon Alexa, allowing your desk to be controlled by your phone, a computer, or a home speaker.

It has been tested and currently works with any desk relying on the Dynamic Motion system by Logicdata, like the Yaasa Desks, Gravit iDesk, or the Lidle LifeUP in addition to the IKEA Bekant desks. The code was developed in C for the ESP32 microcontrollers family and is available on GitHub.

The journey

It all started when I decided to get a new desk at home. From the beginning, the idea was to build something unique and cool that I could customize and upgrade over time. I found out that Yaasa was selling an electrically height-adjustable table frame that can be turned into a do-it-yourself desk to match my personal needs and preferences.


For the tabletop, I decided to work with a local woodshop that uses discarded wood and upcycle it in order to transform it into a designer piece. This way of doing offers a stylish and ecological alternative to cheap, mass-produced goods. High-quality design and sustainability are combined.

The first upgrade was to replace the stock feet with wheels so that I could move it quickly anywhere I wanted at home. The next addition was a Philips HUE led strip under the tabletop that produced a pleasant ambient light and a glowing atmosphere. After those first two easy upgrades, the whole journey started.

Following the White Rabbit

The desk controller is a DMPHub S from Logicdata, compatible with every product within the Dynamic Motion system. It is linked to the two motors (one in each foot stand) and the supplied remote control with a Mini-Fit Jr. 2x2 connector. One of the ports remained free, which was perfect for an additional custom remote control to extend the system.


The first thing to do was to open the included remote control to understand what kind of components were used and the communication protocol. The remote was easily openable by unscrewing two screws from the case back. What appeared was (from left to right) a microcontroller where probably the code and logic resides, a flexible flat cable connector that is connected to the physical buttons, a three digits seven-segment display that displays the height of the desk in centimeters, and finally, a small at the time unknown chip connected to the desk via a four pins wire-to-board connector.


It comes out that the desk is communicating with the remote control with a three-wire cable. I didn't understand at first what kind of protocol they were using, but it has to use only one wire for the data, as the other two were used respectively for power (VCC) and ground (GND).

By following the integrated circuit board path, the three wires (after passing through a diode, some capacitors, and resistors) ended up being connected to an NCV7428 microcontroller.

NCV7428 https://www.onsemi.com/pdf/datasheet/ncv7428-d.pdf

According to the datasheet, this NCV7428 is a System Basis Chip (SBC) integrating functions typically found in automotive Electronic Control Units (ECUs). NCV7428 provides and monitors the low−voltage power supply for the application microcontroller and other loads and includes a LIN transceiver. Finally, it is compliant with the versions LIN2.x and J2602. Great! At this point, I knew the protocol and the version that was being used, and it already gave me some clues and ideas for the next steps.

LIN Protocol

LIN (Local Interconnect Network) is a serial network protocol used for communication between components. It is a single wire, serial network protocol that supports communications up to 19.2 Kbit/s at a bus length of 40 meters. It is a broadcast serial network comprising 16 nodes (one master and typically up to 15 slaves). All messages are initiated by the master, with at most one slave responding to a given message identifier. The master node can also act as a slave by replying to its own messages. A master node contains the master task as well as a slave task. All other slave nodes contain a slave task only.

LIN bus https://www.cs-group.de/wp-content/uploads/2016/11/LIN_Specification_Package_2.2A.pdf

The frame structure consists of a number of fields, one break field followed by four to eleven-byte fields. The time it takes to send a frame is the sum of the time to send each byte plus the response space and the inter-byte spaces. The header starts at the falling edge of the break field and ends after the end of the stop bit of the protected identifier (PID) field. The response starts at the end of stop bit of the PID field and ends after the stop bit of the checksum field.

LIN Frame https://www.cs-group.de/wp-content/uploads/2016/11/LIN_Specification_Package_2.2A.pdf

A protected identifier field consists of two sub-fields: the frame identifier and the parity. Bits 0 to 5 are the frame identifier and bits 6 and 7 are the parity. Six bits are reserved for the frame identifier, and values in the range 0x00 to 0x3F can be used. The frame identifiers are split into three categories, values:

  • 00 (0x00) to 59 (0x3B) are used for signal carrying frames
  • 60 (0x3C) to 61 (0x3D) are used to carry diagnostic and configuration data
  • 62 (0x3E) to 63 (0x3F) are reserved for future protocol enhancements
The parity is calculated and stored within the last two bits of the frame identifier as shown in the equations below:
  • P0 = ID0 ⊕ ID1 ⊕ ID2 ⊕ ID4
  • P1 = ¬(ID1 ⊕ ID3 ⊕ ID4 ⊕ ID5)
The last field of a frame is the checksum. The checksum contains the inverted eight-bit sum with carry over all data bytes and the protected identifier. An eight-bit sum with carry is equivalent to sum all values and subtract 0xFF every time the sum is greater or equal to 0x100.

		#define P(pid, shift) ((pid & (1 << shift)) >> shift)

	uint8_t checksum(uint8_t *lin_data, uint8_t protected_id) {
		uint16_t checksum = protected_id;
		for (uint8_t i = 0; i < LIN_DATA_SIZE; i++) {
			checksum += lin_data[i];
			if (checksum > 0xFF) {
				checksum -= 0xFF;
		return (~checksum & 0xFF);
	uint8_t parity(uint8_t pid) {
		uint8_t p0 = P(pid, 0) ^ P(pid, 1) ^ P(pid, 2) ^ P(pid, 4);
		uint8_t p1 = ~(P(pid, 1) ^ P(pid, 3) ^ P(pid, 4) ^ P(pid, 5));
		return (p0 | (p1 << 1)) << 6;

Reverse Engineering

The communication channel and protocol having been identified, I needed a way to figure out what kind of data was being transferred between the remote control (slave) and the desk (master). A very convenient way to do that was to use the Logic Analyzer from Saleae. With it, you can observe and understand the state of the signal (High/Low) and also decode the data using their integrated parsers.

My analysis setup was composed of the Logic 8 Analyzer from Saleae, combined with some PCBite probes for easy hands-free measurements.


After some further research, the whole board is being managed by an STM8S005K6T6 microcontroller, a value line 8-bit chip that offers 32 Kbytes of flash program memory, plus 128 bytes of data EEPROM. Putting probes between the STM8S005K6T6C and the NCV7428 allowed the understanding of the exchanged data. According to the LQFP 32-pin pinout, the two pins used for transmitting Tx and receive Rx are respectively the 30th and the 31st.

stm8s005k6t6c https://www.st.com/resource/en/datasheet/stm8s003f3.pdf

Putting the logic analyzer on the Rx/Tx pins and on the LIN input connector allowed the analysis of the protocol and data being exchanged.


Another huge advantage of this analyzer is the ability to decode some standard protocols and even to create and implement your own plugins. I linked the integrated LIN and Asynchronous Serial Decoder to the corresponding cables and interpreted the data.


The first swimlane D0 contained the raw LIN data exchanged between the desk controller and the remote. The other two D1 and D2 collected the Rx/Tx data exchanged between the PIC STM8S005K6T6C and the NCV7428 LIN controller. To make the analysis a little simpler and customizable, I created a High-Level Analyzer called "LIN Data" that group the data bits 0-7 and highlight the PID. It also allows data visualization in table view and CSV printed in the terminal for easy export and post-processing.


This extension has been published into the Logic2 Marketplace and on GitHub

Operating Modes

The LIN protocol enables multiple operating modes in order to improve the lifespan of the components as well as for power efficiency. The main states are:

  • Shutdown when the power outlet is unplugged.
  • Normal when the table is moving, has been woke up, or is synchronizing.
  • Sleep if nothing happens for three to five seconds, the desk switch to sleep mode.
The following figure shows the different modes in which the table can reside. Shutdown state before marker 0, Normal after markers 1, 2, and 3 when the desk is respectively synchronizing, moving up and down, and finally Sleep mode when the dest is inactive.


The protocol used between the desk (master) and the remote controller (slave) contains three main parts: the pairing sequence, the status message, and the actual desk control.

Pairing sequence

The pairing sequence starts when the table is switched on from an off state. It is performed by the desk controller (master), which sends a series of messages with the PID 0x06, which increments one byte (Data 0) from 0x00 to 0x07. Here is the complete sequence.

FrameHeader BreakPIDData 0Data 1Data 2Data 3Data 4Data 5Data 6Data 7Checksum
The first frame content.
Header BreakPIDData 0Data 1Data 2Data 3Data 4Data 5Data 6Data 7Checksum


And the last pairing frame.

Header BreakPIDData 0Data 1Data 2Data 3Data 4Data 5Data 6Data 7Checksum


Status Messages

After the pairing phase, the desk controller sends a status message every few frames. I was able to figure out the different parts of it and to define a struct that will handle the corresponding messages.

		typedef struct lin_frame {
		uint8_t protected_id;
		uint8_t data[8];
		uint8_t checksum;
	} lin_frame_t;
	typedef struct status_frame {
		uint8_t protected_id;
		uint8_t reserved0[2];
		uint8_t ready;
		uint8_t status;
		uint8_t reserved1[1];
		uint8_t status_code;
		uint8_t error_code;
		uint8_t reserved2[1];
		uint8_t checksum;
	} status_frame_t;
	// Converting a lin_frame as a status_frame
	status_frame_t *status_frame = (status_frame_t*) lin_frame;

Different types of status messages:

Header BreakPIDReservedReservedReadyStatusReservedStatus CodeError CodeReservedChecksum
Error0x550x230x000x000x610xFD0x000x00(cf. table below)0x000xB1
Error codes with description:
Code Status
0x01 Firmware Error: Disconnect the Power Unit from the Mains. Then, disconnect System from the Power Unit. Reconnect the system again, then operate the DM System as normal.
0x02 Motor Over Current: Release all Keys and wait for 5 seconds. Then, try again.
0x03 DC Over Voltage: Release all Keys and wait for 5 seconds. Then, try again.
0x08 Impulse Detection Timeout: Perform a Position Reset Procedure (see System Manual).
0x0B Speed cannot be achieved: Release all Keys and wait for 5 seconds. Then, try again.
0x0C Power Stage Overcurrent: Release all Keys and wait for 5 seconds. Then, try again.
0x0D DC Under Voltage: Release all Keys and wait for 5 seconds. Then, try again.
0x0E Critical DC Over Voltage: Release all Keys and wait for 5 seconds. Then, try again.
0x0F Strain Gauge is defective: Release all Keys and wait for 5 seconds. Then, try again. Contact LOGICDATA if the problem persists. Do not operate the DM System if components are broken.
0x11 Error during pairing sequence: Disconnect the Power Unit from the Mains. Then, disconnect System from the Power Unit. Reconnect the system again, then operate the DM System as normal. If this fails, perform a factory reset (see DM System Manual).
0x12 Parameterization or firmware of different Actuators in the Table System are incompatible: Re-parameterize the Actuators. Contact LOGICDATA for further information.
0x13 Too many / too few Actuators connected: Connect the correct number of Actuators (as specified in setup).
0x14 Motor short circuit and/or open load: Contact LOGICDATA.
0x15 Firmware Error: Disconnect the Power Unit from the Mains. Then, disconnect System from the Power Unit. Reconnect the system again, then operate the DM System as normal.
0x16 Power Unit overload: Release all Keys and wait for 5 seconds. Then, try again.
0x17 Motor Under Voltage: Release all Keys and wait for 5 seconds. Then, try again.


First pairing frame received.

Header BreakPIDReservedReservedReadyStatusReservedStatus CodeError CodeReservedChecksum



Error frame, too many / too few Actuators connected: Connect the correct number of Actuators (as specified in setup).

Header BreakPIDReservedReservedReadyStatusReservedStatus CodeError CodeReservedChecksum



The reset frame is sent when the motors are in an unknown state (status code 0x01). For instance, if the controller is disconnected or in case of a power outage when the desk was moving. To recalibrate the motors, they must be lowered to the lowest possible height. At this position, the motors state are resetted, and the desk is at the position 0x00, which is about 60 cm with this desk.

Header BreakPIDReservedReservedReadyStatusReservedStatus CodeError CodeReservedChecksum


I noticed some glitches (and unintentional pulse in voltage) occurring from time to time in the Header Break field. This is really annoying because if the break is not correctly detected, the rest of the frame is not parsed either. This can occur due to radiated noise caused by nearby electronics, noise in the circuit itself, extremely slow rise/fall times in the digital data, and many other reasons.


The Logic software includes a glitch filter to suppress short digital pulses in the recorded data. It is designed to help remove noise picked up in the digital recording. This is really useful when using protocol analyzers since present noise may prevent proper decoding of digital data.


In this case, glitches had a duration of a few µs. I configured and made some tests with the filter, and 10-15 µs worked perfectly. As you can see in the picture below, the Header Break was then successfully detected, allowing the rest of the frame to be parsed as well.



The table height is stored within two bytes of a status frame. The fields Data 3 and Data 4 have to been combined to get the height in millimeters, so just divide it by ten to have the correct value in centimeters. The Data 5 field contains the value of the step motors, between 0x00 and 0xFF. A straightforward calculation also gives the value in percent.

Data 3Data 4Data 5

		uint8_t desk_height = ((lin_frame->data[3] << 8) + lin_frame->data[4]) / 10.0;
	uint8_t desk_percentage = (lin_frame->data[5] / 255.0) * 100;

With this example, the hex value 0x02BA equals 698 in decimal. Divided by ten, it represents a height of 69.8 centimeters.

Header BreakPIDData 0Data 1Data 2Data 3Data 4Data 5Data 6Data 7Checksum


Move Table

The Protected ID 0x22 is used to control the desk. The master broadcast this ID to all connected slaves, and if one responds, it will move accordingly to the data present in the response. This behavior is pretty cool as it permits to have multiple remote controls connected like in this setup and keep a physical control in addition to a new IoT version. As I've done for the status messages, I defined a struct and mapped the corresponding response.

		#define DESK_UP         (0x00)
	#define DESK_DOWN       (0x01)
	#define DESK_IDLE       (0x00)
	#define DESK_MOVE       (0x01)
	#define DESK_STOP       (0x0B)
	typedef struct response_frame {
		uint8_t random;
		uint8_t reserved0[1];
		uint8_t direction;
		uint8_t reserved1[3];
		uint8_t action;
		uint8_t reserved2[1];
		uint8_t checksum;
	} response_frame_t;
	response_frame_t response_frame = {
		.random = 0x00,
		.reserved0 = {0x00},
		.direction = DESK_DOWN,
		.reserved1 = {0x00, 0x00, 0xFF},
		.action = DESK_IDLE,
		.reserved2 = {0x01},
		.checksum = 0x00

	// A new random value and thus the checksum are generated for every response
	response_frame.random = rand() % 0xFF;                        
	response_frame.checksum = checksum((uint8_t*) &response_frame, lin_frame->protected_id);

	// The response is sent back to the master
	uart_write_bytes(UART_NUM_2, &response_frame, sizeof(response_frame));

Different types of response messages:

Header BreakPIDRandomReservedDirectionReservedReservedReservedActionReservedChecksum


As LIN is a single wire protocol, all messages are initiated by the master with at most one slave replying to a given message identifier. To move the desk, the master starts a frame withe the Protected ID 0x22, and any slaves can respond to it within a few miliseconds.


The slave then responds to the master and append the eight bytes data plus the checksum within usually one millisecond.


USB Console

A way to control the desk is to use the integrated UART to USB interface. I mapped the Up and Down functions with the keyboard arrow keys for convenient usage and added some memory functions linked to the numeric values that you can configure as desired. It is also helpful to display any warning or error messages in full text.



Logic To re-implement and upgrade the remote control, I choose an ESP32-S3. This chip is a dual-core XTensa LX7 MCU, capable of running at 240 MHz. Apart from its 512 KB of internal SRAM, it also comes with integrated 2.4 GHz, 802.11 b/g/n Wi-Fi, and Bluetooth 5 (LE) connectivity. It has 45 programmable GPIOs and supports a rich set of peripherals.

I use it over Wi-Fi and USB via the integrated UART with its great capabilities and extensibility. In addition, other technologies might be used in the future, like Bluetooth or RF. To help and accelerate with the development, I used the ESP32-S3-DevKitC-1 from Espressif. It is an entry-level development board equipped with either ESP32-S3-WROOM-1. Most of the IO pins on the module are broken out to the pin headers on both sides of this board for easy interfacing.


Another component I used is the ATA663211 click dev board which is a standalone LIN transceiver that communicates with the target MCU through the UART interface and runs on a 3.3V power supply. An onboard LDO (low-dropout regulator) enables it to get its power supply through the VS line screw terminal. This chip handles the raw LIN signal and splits it into a Rx/Tx UART serial that is then connected to the ESP32-S3.

PCB Design

The proof of concept has been made using two development boards, the ESP32-S3-DevKitC-1, and the ATA663211 click. For the final outcome, I wanted to have something cleaner and well-made. I designed a custom PCB with two different Mini-Fit Jr. connectors, a 2x2 and a 1x3 version, to make it compatible with the Logicdata and IKEA desks.

Logic Logic

I used Kicad for the rendering and took advantage of doing something custom to add a temperature, humidity, and air quality sensor. Those sensors will then be integrated into any home automation ecosystem easily.

Logic Logic

Assembly & flashing

I chose OSHPark to produce my prototypes. The quality is pretty good for a reasonable price, about $20 for three PCBs of size 80.3 x 30.5 mm. Cool as well are the geeky details that you receive when the boards are in production.

We've sent the panel containing your boards to the fabricator. We expect to get them back around March 26th. In case you're interested, there are 79 other orders on the panel along with yours, adding up to a total of 480 boards. Neat eh? - OSHPark

To solder the different components, I used some flux and solder paste for the SMD and a more traditional soldering gun for the through-hole parts. Once everything was soldered and tested to ensure that they was no bridges or circuit shortage, the ESP32-S3 needed to be flashed with some code.

To do that, I first connected the PCB to my computer using a UART to USB cable to the three pins I set up for this purpose on the right of the board. The ESP32-S3 will enter the serial bootloader when GPIO0 (BOOT button) is held low on reset. Otherwise, it will run the program in flash. This GPIO has an internal pullup resisttor, so it will be pulled high if the BOOT button is not pressed.



Logic In addition to the core functionality of controlling the desk, I added some optional sensors to monitor the environmental factors at my workplace. To do that, I choose the SCD4x product family from Sensirion, a high accuracy miniature CO₂, temperature, and relative humidity sensor.

CO₂ is a crucial indicator of indoor air quality as high levels compromise human's cognitive low power and well-being. Indoor air quality monitoring helps maintain a low CO₂ concentration for a healthy and productive environment. There are currently three products within this series, and any of those would fit this project.

Measurement RangeCO₂ AccuracyTemperature AccuracyRelative Humidity Accuracy
SCD40400 - 2000 ppm± 50 ppm± 0.8 °C± 6 %
SCD41400 - 5000 ppm± 40 ppm± 0.8 °C± 6 %
SCD42400 - 2000 ppm± 75 ppm± 0.8 °C± 6 %

The communication with the sensor uses the digital I2C interface, a synchronous multi-controller/multi-target, widely used for attaching lower-speed peripheral ICs to processors and microcontrollers in short-distance or for intra-board communication. The I2C bus consists of two lines, serial data line (SDA) and serial clock (SCL). Data sent to and received from the sensor consists of a sequence of 16-bit commands and/or 16-bit words.

The communication sequence between the I2C master and the SCD4x sensor is as follows:

  1. The sensor is powered up
  2. The I2C master sends a start_periodic_measurement command
  3. The I2C master sends five read_measurement and computes the average of them
  4. The I2C master sends a stop_periodic_measurement command to put the sensor back to idle mode
  5. Wait 15 minutes, and repeat this process
The values are displayed in the USB console with configurable ranges of good, mediocre, and poor air quality.


Home automation

Having an existing Apple HomeKit ecosystem, I decided to start with this implementation. HomeKit is a framework developed by Apple for communicating with and controlling connected accessories in a user's home using iOS or macOS devices. The HomeKit Open Source ADK is an open-source version of the HomeKit Accessory Development Kit. It can be used by any developer to prototype non-commercial smart home accessories.


Mapping a smart desk into an existing category was not straightforward. In fact, I didn't really found what could be the best fit. As I wanted to have a slider, I needed to find an accessory having this characteristic.

  • Door: This service describes a motorized door
  • Window: This service represents a motorized window
  • Window Covering: This service describes motorized window coverings or shades, like shutters, blinds, awnings, etc

I chose the last one and integrated it as a blind that I could move up and down. I'm not 100% sure about this implementation, but it's not that bad for a first PoC. We'll have to see if something more custom could be created and also add some memory buttons with predefined height. The idea is also to choose an accessory type available for Google Home and Amazon Alexa to have a continuity between the three home automation platforms.

There are two ways to add a new Homekit accessory into your Home ecosystem. The first one is to scan for new accessories in your home network, and then once found enter the pairing code predefined in the accessory setup partition.


The second option is to scan the QR code generated at the partition creation that already contains the required characteristics to pair the device. It has to be available in the network and doesn't have been already paired with a HomeKit-compatible device before that.


Once successfully paired, you need to specify a location where the accessory will reside in your home, a name for the accessory, and all the configured services. The values below are set up by default but could be changed if needed. Siri will then use those defined names to control the accessories.


You have the ability to choose in the CMake config at compile time whether to enable the sensors in the HomeKit accessory or not. If not, you'll only have the possibility to control the desk and move it up and down. Otherwise, the sensors values are mapped to their corresponding accessories:

  • Temperature Sensor - Describes the current temperature of the environment in Celsius.
  • Humidity Sensor - Describes the current relative humidity of the accessory's environment. The value is expressed as a percentage (%).
  • Carbon Dioxide Sensor - Detects abnormal levels of Carbon Dioxide, indicates the detected level of Carbon Dioxide and the highest value detected by the sensor in parts per million (ppm).
  • Air Quality Sensor - Assessment of air quality {Excellent, Good, Fair, Inferior, Poor}.

Over-The-Air Updates

OTA updates is a mechanism that allows a device to update itself based on data received while the normal firmware is running (for example over Wi-Fi or Bluetooth). OTA requires configuring the partition table of the device with at least two "OTA app slot" partitions (ota_0 and ota_1) and an "OTA Data Partition".

The OTA operation functions write a new app firmware image to whichever OTA app slot that is currently not selected for booting. Once the image is verified, the OTA Data partition is updated to specify that this image should be used for the next boot.

Every twelve hours and after boot, the microcontroller checks if a newer version is available online at a predefined URL. If so, it will be downloaded, verified, and flashed into the currently unused OTA partition.


The bootloader will be compiled with code to verify that an app is signed before booting it. In addition, the signature will be also proofed before updating the firmware and adds significant security against network-based attacks by preventing spoofing of OTA updates.



It is impressive how this project has evolved and allowed me to apply knowledge in so many areas. From basic electronics to signal analysis, protocol decoding, PCB design, and many more! It has been the first project I've achieved after buying the Logic 8 Analyzer from Saleae, and I couldn't be happier with it. Totally worth it and highly recommended if you're looking for a logic analyzer! There is still a lot of work to do, but I decided to publish this current version of the project. The next steps will come over time. So don't hesitate to contact me for questions or comments and come back for future updates.

next steps

  • IKEA desk testing
  • PCB prototype
  • PCB assembly
  • OTA updates
  • DDNS updates
  • NVS encryption
  • HomeKit memory integration
  • HomeKit sensors integration
  • Sensors (humidity + air + temperature)
  • Google Home support
  • Amazon Alexa support
  • Dump Logicdata firmware


MIT License

Copyright (c) 2022 ma.lwa.re

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.