Turn your Desktronic standing desk into a smart desk!

Great News - The desk is movable by software!

Summary of what I did the last few days

I already updated my repository on GitHub: GitHub - MhouneyLH/esphome_custom_components: A collection of custom components for esphome.


The desk can move up and down. Finally. I used the G-Pin (the second one) for showing the controller-box that something has to move. I augmented my adapter-construction with another white-cable for the touchpad-tx (in the code this is remote-uart). With that one I can write the corresponding messages via uart.


But how does the desk know, if it should go up or down?
I have defined a function called move_to(target_height). It checks the boundaries, sets the new target-height and updates the current-operation based on the current-height:

void Desktronic::move_to(const float height_in_cm)
{
    if (height_in_cm < MIN_HEIGHT || height_in_cm > MAX_HEIGHT)
    {
        ESP_LOGE(TAG, "Moving: Height must be between 72.0 and 119.0 cm");

        return;
    }

    target_height_ = height_in_cm;
    current_operation = must_move_up(height_in_cm) ? DESKTRONIC_OPERATION_RAISING : DESKTRONIC_OPERATION_LOWERING;
}

The main-loop looks like this (no need to move, if we are idling anyway):

void Desktronic::loop()
{
    read_desk_uart();

    if (current_operation == DesktronicOperation::DESKTRONIC_OPERATION_IDLE)
    {
        read_remote_uart();
        return;
    }

    move_to_target_height();
}

The read_desk_uart()-method updates the current_height_. That’s pretty important for the later check, if the target-height is reached or if the desk should move further more. This check you will see in the move_to_target_height()-method.

The interesting part is the move_to_target_height()-method. Summarized, I do some checks at the beginning, check wether I have to move up or down and do that until I reach the wished height (inside of the target-boundaries). The target boundaries are defined with a deviation from the target height of 0.6cm. The move_pin_ is the G-Pin in this case, which has to be high when moving:

void Desktronic::move_to_target_height()
{
    if (!move_pin_)
    {
        ESP_LOGE(TAG, "Moving: Move pin is not configured");
        return;
    }

    if (!remote_uart_)
    {
        ESP_LOGE(TAG, "Moving: Remote UART is not configured");
        return;
    }

    if (!isCurrentHeightValid())
    {
        ESP_LOGE(TAG, "Moving: Height must be between 72.0 and 119.0 cm");
        move_pin_->digital_write(false);
        current_operation = DesktronicOperation::DESKTRONIC_OPERATION_IDLE;

        return;
    }

    move_pin_->digital_write(true);
    switch (current_operation)
    {
    case DesktronicOperation::DESKTRONIC_OPERATION_RAISING:
        ESP_LOGE(TAG, "Moving: Up");
        for (int i = 0; i < REMOTE_UART_SEND_MESSAGE_COUNT; i++)
        {
            remote_uart_->write_array(REMOTE_UART_MESSAGE_MOVE_UP, REMOTE_UART_MESSAGE_LENGTH);
        }

        break;
    case DesktronicOperation::DESKTRONIC_OPERATION_LOWERING:
        ESP_LOGE(TAG, "Moving: Down");
        for (int i = 0; i < REMOTE_UART_SEND_MESSAGE_COUNT; i++)
        {
            remote_uart_->write_array(REMOTE_UART_MESSAGE_MOVE_DOWN, REMOTE_UART_MESSAGE_LENGTH);
        }

        break;
    default:
        return;
    }

    if (isCurrentHeightInTargetBoundaries())
    {
        ESP_LOGE(TAG, "Moving: Finished");
        move_pin_->digital_write(false);
        current_operation = DesktronicOperation::DESKTRONIC_OPERATION_IDLE;
    }
}

I was also able to reduce the flash memory to 44%. I removed unneeded header files as well as yaml configurations that are useless now. My yaml file now looks like this:

esphome:
  name: esphome-web-755eb2

esp8266:
  board: esp01_1m

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: ...

ota:

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  manual_ip:
    static_ip: ...
    gateway: ...
    subnet: ...

external_components:
  - source:
      type: git
      url: https://github.com/MhouneyLH/esphome_custom_components
      ref: develop
    refresh: 4s
    components: [ desktronic ]

uart:
  - id: desk_bus
    tx_pin: 5 # D1
    rx_pin: 4 # D2
    baud_rate: 9600
  - id: remote_bus
    tx_pin: 3 # RX
    rx_pin: 1 # TX
    baud_rate: 9600

desktronic:
  id: my_desktronic
  desk_uart: desk_bus
  remote_uart: remote_bus
  height:
    name: Desk Height
  move_pin:
    number: 14 # D5
  up:
    name: Up Button
  down: 
    name: Down Button
  memory1:
    name: Memory1 Button
  memory2:
    name: Memory2 Button
  memory3:
    name: Memory3 Button

switch:
  - id: move_switch
    name: ↑↓
    platform: gpio
    pin:
      number: 2 # D4
      inverted: true
    on_turn_on:
      then:
       - lambda: id(my_desktronic).move_to(80.0);
      #  - lambda: id(my_desktronic).stop();

binary_sensor:
  - id: is_moving_bsensor 
    name: Desk is moving
    platform: template
    lambda: return id(my_desktronic).current_operation != desktronic::DESKTRONIC_OPERATION_IDLE;

For more information about the code, etc. you can check the mentioned repository. With this state of the project, I accomplished a major (and the most difficult intermediate step) of the planned project. :slight_smile: Thank you, especially @Mahko_Mahko and @ssieb!

The next steps and goals

  • currently it is not possible to see the current height or other stuff on the touchpad, when the remote-tx is connected to the chip.
    → potential solution: use a third uart-bus for sending the remote-tx-data separately.
  • I want to create a more beautiful ui in HomeAssistant.
  • I want to connect a weight-sensor to the home-assistant eco-system. The weight-sensor lays under my chair and if I sit down, the desk automatically goes down.
  • add 2 methods with which I can adjust the height of the desk one by one. (so I dont have to give it a fixed height everytime)
  • improving the hardware circumstances: At the moment, I have soldered the chip on improperly. I should now do this again properly because I noticed in between that there were some loose contacts.
2 Likes

Nice one!

For chair occupancy take a look at items 1 and 13 here. You might find a battery operated device works better for a chair.

1 Like

Do you have a wiring diagram you could share please?

I’m actually working on a similar project that uses a pass through set-up and I’m not sure how to wire for reading and writing to both the rx and tx. I understand you had to do something like this for your set-up. Sorry if I’ve missed it.

https://community.home-assistant.io/t/i-scored-a-scorbot-er-iii/540324/2?u=mahko_mahko

1 Like

I could create a diagram. But this weekend I’m busy with other stuff. I could create one next week. :slight_smile:

1 Like

Hey, I wanted to give a short update.

@Mahko_Mahko I did not have forgotten the diagram, but I did not had the time since a few weeks.
Thats a not completely finished diagram, if you still need it:

Currently I am working on a project to control the desk via app on a smartphone. The progress you can see in this repository. A friend of mine also made a nicer connection to the chip with properly soldered cables.

1 Like

Nice. Thanks for getting back to me. I figured it out with some help from some people on Discord though.

My “Man in the middle” wiring looks like this.

The green wire both intercepts and injects uart messages. For the yellow wire I only need to read the messages.

I need the max3232 convertors to convert the 12v logic to 5v and back again.

1 Like

Thank you for the great project base! :palms_up_together:

I have a similar odd desk.


I am embarrassed to be still very confused about the wiring to the ESP dongle between desk and the controller, and how the UART wires should be.

The cable itself has 10 pins, which i counted after ordering the expanders from Aliexpress with 8 pins :confused: But i discarded the outermost ones that control the 12 Volts, and it almost works, it fits to the base nicely and i can use the inner 8.

But here are the problems:

  1. When inserting WHITE to TX on ESP32, i can get the binary sensors (pressed button up, button down, etc)
  2. When inserting WHITE to RX on ESP32, i can get the table to move with buttons
    1. Modified your cpp code to expose more public functions for testing, so i can move it up or down.
    void move_to_from(const float height_in_cm, const float current_height);
    void move_to(const float height_in_cm);
    void stop();
    void move_up();
    void move_down();
    void move_to_memory_1();
    void move_to_memory_2();
  1. But never have I got the desk height :confused:
  2. Not have I got the dongle to work together with the physical controller. The display just says 888. and buttons dont work.

So about wiring:

  • should I keep the existing connection between table and controller, and add my wires for ESP just in the middle?
  • or remove the existing WHITE and BLACK to just go through my ESP32?
  • i have 2 uart buses configured, where do the wires go? exactly? :flushed:
  - id: remote_bus
    tx_pin: 1 # when WHITE - allows to control the desk
    rx_pin: 3 # when WHITE - reads sensor data from controller
   # together they break

  - id: desk_bus
    rx_pin: 17 # no combo does anything
    tx_pin: 16 # no combo does anything

I’m ashamed to have spent too much time on this, and now I just want to finish it out of spite.

Tried the uart_mitm component for no avail.

So I almost hacked out the height from desk, logging and reading the hex data, parsing it to numbers (without proper wiring):

    debug:
      direction: BOTH 
      dummy_receiver: true
      after:
        delimiter: "\xA5"  
      sequence:
        - lambda: |-
            UARTDebug::log_hex(direction , bytes, ',');  //Still log the data

            auto uart_direction = "IDK";

            if (direction = uart::UART_DIRECTION_TX ){
              uart_direction = "TX: ";
            }              
            else if (direction = uart::UART_DIRECTION_RX ){
              uart_direction = "RX_ ";
            }
              auto bytes_size = bytes.size();
                std::string log_message = uart_direction;
                for(int i = 0; i < bytes_size; ++i){
                  char buffer[4]; 
                  sprintf(buffer, "%02x ", bytes[i + bytes_size - 1 ]); 
                  log_message += buffer; 
                }
              if ( bytes_size >= 5){
                ESP_LOGW("desk_bus", "%s", log_message.c_str());
              }
              else {
                ESP_LOGI("desk_bus", "%s", log_message.c_str());
              }         

even got this far as to read the height value out of the uart buffer, somewhat semi-consistently:

script:
  - id: get_debug_height
    mode: single
    then: 
      - lambda: |- 
          // provided map gives error on "4" // data[2] is data2 = EDGE CASE if contain 4 (74, 84, 94, 104, 114, 124)
          // static const int SEGMENT_MAP[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x67, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};
                                              //  0     1     2     3     4     5     6     7     8     9     
          static const int SEGMENT_MAP[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x67, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};
                                                                    //0x66
          uint8_t data[5];
          while (id(desk_uart).available() >= 5) {

            id(desk_uart).read_array(data, 5);
            if (data[0] != 0x5A) {
              ESP_LOGE("seisuk", "[0] %02x must be 0x5A, [3] is %02x", data[1], data[3]);
              //break;
              continue;
            }
            if ((data[1] | data[2] | data[3]) == 0x00) {
              ESP_LOGD("seisuk", "null height");
              //break;
              continue;
            }
            int data0 = -1, data1 = -1, data2 = -1;
            for (int i = 0; i < 10; i++) {
              if (data[1] == SEGMENT_MAP[i]) data0 = i;
              if (data[2] == SEGMENT_MAP[i]) data1 = i;
              if (data[3] == SEGMENT_MAP[i]) data2 = i;
            }

            float got_height = 0.0;

            if (data0 < 0 || data1 < 0 || data2 < 0) {
              if (data0 < 0 ) { 
                ESP_LOGE("data", "data0: %02x", data[1] );  // if ( data[1] != 0x00 ) { }               
              }
              if (data1 < 0 ) { 
                // true for values above 100, false for below 100
                ESP_LOGW("data", "data1: %02x", data[2] );  // if ( data[2] != 0x00 ) { }
              }
              if (data2 < 0 ) {
                ESP_LOGW("seisuk", "data2:  %02x", data[3] );
              }
              //break;
              continue;
            }

            // get decimal
            int decimal = ((~data[4] + 256) % 256) / 10;

            // sum if up            
            got_height = data0 * 100 + data1 * 10 + data2 + decimal * 0.1;

            add decimal 
            //if (got_height >= 100.0){
            //  got_height += decimal * 0.1;
            //}

            // If belo 3 digis, e.g 100
            // INVERT HEIGHT FOR BELOW ZERO

            if (data[2] & 0x80 && data1 < 0) { 

              got_height = got_height / 10 + 10;

              float under_99 = ( ( ~data[4] + 256 ) % 256 ); 

              if (under_99 > 100 ){
                //under_99 /= 10;
              }

              float inverted = ( ( data[4] + 256 ) % 256); // + 100;

              if (inverted > 100 ){
                //inverted /= 10;
              }

              float reinverted = ( ( data[4] ) % 256) / 2; // + 100;

              if (reinverted < 10 ){
                //reinverted *= 10;
              }

              if (under_99 < 101 && under_99 > 65  ){ // && under_99 != 68.00
                ESP_LOGI("under_99", "%.2f", under_99 );               
              }
              if (inverted < 101 && inverted > 65 ){
                ESP_LOGW("inverted", "%.2f", inverted );               
              }
              if (reinverted < 101 && reinverted > 65  ){ // && reinverted != 100.00
                ESP_LOGE("reinverted", "%.2f", reinverted );       
              }

              if (data1 < 0 ) { 
                // ESP_LOGE("data1", "%02x", data[2] );  // if ( data[2] != 0x00 ) { }
              }

              if (under_99 <= 100 && under_99 > 66 ){
                //got_height = under_99;
                //ESP_LOGI("got_height", "%.2f", under_99);
              }
              
              if (got_height <= 100 ){
                ESP_LOGI("lt100", " %.1f", got_height );                                   
              }
            }
            
            // IF BELOW 100, divide by 10, get decimal place

            ESP_LOGW("height", "data[2]: %02x, data1: %d", data[2], data1);              

            // IF ALL IS CORRECT, PUBLISH VALUES, DO STUFF

            if (got_height <= id(max_height) && got_height >= id(min_height) ){
              ESP_LOGI("height", "%.1f ", got_height );
              id(uart_height).publish_state(got_height);
              id(height) = got_height;
            }

            if (got_height >= id(max_height) ||  got_height <= id(min_height) ){
              id(seisuk).stop();
            }
          }
1 Like

I haven’t been following this in detail, but based on my experiences with another project, here is how I believe uart wiring needs to be for two controlling devices to be able to issue commands…

  1. If you only need to “read signals on a wire” (listen to them, let them “pass through”, but not inject your own commands), then you can just “Hop” the wire. See yellow wire.

  2. If you need to both read signals on the wire and inject your own (listen for keypad presses and insert your own from the ESP), then the wire must only pass through the ESP via a “uart forward”. This is where I got stuck for a long time with my project. I believe this is key to a “Man-In-The-Middle” (MITM) set-up. See green wire. If you try to hop a signal you need to both read and write from, it won’t work when both devices are connected.

In my diagram, you can pretend the convertors aren’t there (wires go straight through). Pendant is control panel. Robot is desk control box.

I’m not 100% if this applies to this project, but it took me a while to figure this out so sharing in case it is of use.

I also found for my project that the MITM may introduce a small lag as it recieves and forwards uart messages. This may or may not create issues if communication is two way (there is a request that waits for a response and may timeout).

1 Like

As wire colors are different, I only have black and white to play with, and red for control takeover. I use green/purple to power the ESP. Yellow and Brown are ignored because I messed up buying the 8-pin over 10-pin cable expander :no_mouth: This means I cant get USB power from the control panel, but that’s okay for now…

I can do without knowing the binary sensors for if controller button is pressed, i just would like to get the desk height, so i can move it to set height. Currently using my modified code i can move it up and down with switches, but since i dont know the height, i cant set the position…

P → Key1 # RED = P (Panel?)
R → D-TX # BLACK = Rx
T → D-RX # WHITE = TX
5 → +5V # GREEN = 5g
G → G # PURPLE = GND
YELLOW = DC + 12v
BROWN = DC_G - 12v

1 Like

Are the height messages the same protocol as the OP? I assume they are on the black wire?

1 Like

Good find, they are not exactly the same. As when using the original external component, I didn’t get anything as height, but with my custom lambda script at least something did come out…

 // provided map gives error on "4" // data[2] is data2 = EDGE CASE if contain 4 (74, 84, 94, 104, 114, 124)
 // static const int SEGMENT_MAP[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x67, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};
                                     //  0     1     2     3     4     5     6     7     8     9     
 static const int SEGMENT_MAP[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x67, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};

using the original map i logged some weird jumps, and missing numbers (the component provided sensor didn’t work still):

5A,07,FF,7F,85 -> 138.0
5A,07,FF,7F,85 -> 138.0
5A,07,FF,6F,75 -> 136.0
5A,07,FF,6F,75 -> 136.0
5A,07,EF,3F,35 -> 113.0
5A,07,EF,3F,35 -> 113.0
5A,07,EF,06,FC -> 110.0
5A,07,EF,06,FC -> 110.0
5A,07,EF,5B,51 -> 112.0
5A,07,EF,4F,45 -> 113.0
5A,07,EF,4F,45 -> 113.0
5A,07,EF,6D,63 -> 114.0
5A,07,EF,6D,63 -> 114.0
5A,07,EF,07,FD -> 110.0
5A,07,EF,07,FD -> 110.0
5A,07,EF,6F,65 -> 115.0
5A,07,EF,6F,65 -> 115.0
5A,7D,FD,4F,C9 -> 64,9
5A,7D,FD,5B,D5 -> 62,5
5A,7D,FD,3F,B9 -> 60,9
5A,7D,ED,6F,D9 -> 79,9
5A,7D,ED,07,71 -> 77,1
5A,7D,ED,6D,D7 -> 75,7
5A,7D,ED,66,D0 -> 74,0
5A,7D,ED,4F,B9 -> 73,9
5A,7D,ED,5B,C5 -> 72,5
5A,7D,ED,06,70 -> 71,0
5A,7D,ED,3F,A9 -> 70,9
5A,7D,FD,6D,E7 -> 65,4
5A,7D,FD,07,81 -> 67,1
5A,7D,FD,7F,F9 -> 68,9
5A,7D,FD,6F,E9 -> 69,9
5A,7D,FD,4F,C9 
5A,7D,FD,5B,D5 -> 62,5 
5A,7D,FD,3F,B9 -> 60,9 
5A,7D,ED,6F,D9 -> 79,9 
5A,7D,ED,07,71 -> 77,1 
5A,7D,ED,6D,D7 -> 75,7 
5A,7D,ED,66,D0 -> 74,0 
5A,7D,ED,4F,B9 -> 73,9 
5A,7D,ED,5B,C5 -> 72,5 
5A,7D,ED,06,70 -> 71,0 
5A,7D,ED,3F,A9 -> 70,9 
5A,7D,FD,6D,E7 -> 65,4 
5A,7D,FD,07,81 -> 67,1 
5A,7D,FD,7F,F9 -> 68,9 
5A,7D,FD,6F,E9 -> 69,9  - bottom target reached

Got it working through some ugly hacks :slight_smile: Forgot to post, hope this will help someone else. Buttons on keypad and the screen is not working though, maybe i’ll deal with it in the future. Maybe even add some fun things, since its possible to write random stuff on the 7-segment-display · GitHub Topics · GitHub

substitutions:
  friendly_name: 'Seisuk'
  device_name: 'seisuk'
  node_name: 'seisuk'
  device_description: 'Seisuk AOKE WP-H01 WP-CB01-001 Desktronic/JSDRIVE DCU_G-PRT5G'
  project_base: 'AOKE' # https://aoke-europe.com/troubleshooting
  project_name: 'WP-H01' # panel
  project_version: 'WP-CB01-001' # control box # https://aoke-europe.com/mwdownloads/download/link/id/88/

    # https://profeqprofessional.nl/media/productattach//e/r/error_codes_wp-cb01-001_2.pdf
    # https://motionwise-products.com/wp-content/uploads/2018/11/MotionWise_sf_Manual_081318_E_Rev-USB.pdf
    # https://profeqprofessional.nl/media/productattach//e/r/error_codes_wp-cb01-001_2.pdf

  min_height: "65.5" # real 65  # Min height + 0.5
  max_height: "129.5" # real 130  # Max height - 0.5
  initial_value: "90"

globals:
  - id: height
    type: float
    restore_value: yes
    initial_value: '100.0'

  - id: min_height
    type: float
    restore_value: yes
    initial_value: '65.5'

  - id: max_height
    type: float
    restore_value: yes
    initial_value: '129.5'

esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}
  name_add_mac_suffix: false
  comment: ${device_description}
  project:
    name: ${project_base}.${project_name}
    version: $project_version
  on_boot:
    - priority: 600.0 
      then:
        - button.press: stop
    - priority: -200.0 
      then:
        - lambda: |-
            id(uart_height).publish_state( id(height) );

esp8266:
  board: nodemcu
  early_pin_init: false
  restore_from_flash: true

packages:
  device_base: !include common/device.base.yaml

ota:
  on_begin:
    then:
      - button.press: stop

logger:
  esp8266_store_log_strings_in_flash: false  
  level: DEBUG
  baud_rate: 0

uart:
  - id: remote_bus
    tx_pin: 1 # WHITE (2nd pin)
    rx_pin: 3 # N/A (3rd pin)
    baud_rate: 9600

  - id: desk_bus
    tx_pin: 4 # N/A (D2)
    rx_pin: 5 # BLACK (D1) 
    baud_rate: 9600
    rx_buffer_size: 256

external_components:
  - source: components
    components: [ desktronic ]  

desktronic:
  id: seisuk
  desk_uart: desk_bus
  remote_uart: remote_bus
  height:
    name: Wnotworkign desktronic Height
    id: desk_height_sensor
    icon: mdi:human-male-height
    device_class: distance
    unit_of_measurement: cm
    accuracy_decimals: 1    
    internal: true
  move_pin:
    number: 14 # (D5)

binary_sensor:
  - id: moving
    name: Moving
    platform: template
    device_class: moving
    icon: mdi:hand-back-right-off
    lambda: return id(seisuk).current_operation != desktronic::DESKTRONIC_OPERATION_IDLE;
    on_state:
      - while:
          condition:
            binary_sensor.is_on: moving
          then:
            - script.execute: get_debug_height
            - delay: 50ms

  - id: going_up
    name: Moving up
    platform: template
    device_class: moving
    icon: mdi:transfer-up
    entity_category: diagnostic
    lambda: return (id(seisuk).current_operation == desktronic::DESKTRONIC_OPERATION_RAISING) || (id(seisuk).current_operation == desktronic::DESKTRONIC_OPERATION_UP);

  - id: going_down
    name: Moving down
    platform: template
    device_class: moving
    icon: mdi:transfer-down
    entity_category: diagnostic
    lambda: return (id(seisuk).current_operation == desktronic::DESKTRONIC_OPERATION_LOWERING) || (id(seisuk).current_operation == desktronic::DESKTRONIC_OPERATION_DOWN);

sensor:
  - id: uart_height
    platform: template
    name: Height
    icon: mdi:human-male-height
    device_class: distance
    unit_of_measurement: cm
    accuracy_decimals: 1
    update_interval: never
    lambda: |-
      float current = id(height);
      return current;

number:
  - platform: template
    id: desk_height
    name: Height
    icon: mdi:human-male-height-variant
    min_value: $min_height
    max_value: $max_height
    lambda: |-
      return id(height);
    device_class: distance
    unit_of_measurement: cm
    update_interval: never
    mode: box
    step: 0.1
    set_action:
      then:
        - while:
            condition: 
              - lambda: |-
                  float to = std::round(x * 10) / 10;
                  float from = std::round(id(height) * 10) / 10;
                  if ( to != from ) {
                    return true;
                  }
                  else{
                    return false;
                  }
            then:
              - script.execute: 
                  id: move_it
                  to: !lambda 'return x;'
              - delay: 150ms

cover:
  - platform: template
    name: None
    optimistic: true
    device_class: damper
    icon: mdi:desk
    assumed_state: true
    has_position: true    
    lambda: |-
      auto value = (int)id(height);
      float percentage = (float)(value - id(min_height)) / (id(max_height) - id(min_height));
      return percentage;
    open_action:
      - number.set:
          id: desk_height
          value: $max_height
    close_action:
      - number.set:
          id: desk_height
          value: $min_height
    stop_action:
      - button.press: stop
    position_action:
      - number.set:
          id: desk_height
          value: !lambda |-
            int value = id(min_height) + (int)( ( pos ) * (id(max_height) - id(min_height)) );
            return value;

button:
  - platform: template
    name: Stop
    icon: mdi:stop
    entity_category: config
    id: stop
    on_press:
      then:
        - switch.turn_off: takeover
        - script.stop: move_it
        - switch.turn_off: switch_up
        - switch.turn_off: switch_down
        - lambda: |-
            id(seisuk).stop();
        - number.set: 
            id: desk_height
            value: !lambda |-
              float x = id(height);
              return x;
        - component.update: desk_height

switch:
 - platform: gpio
   name: Takeover
   icon: mdi:gesture-tap
   id: takeover
   pin:
     number: 14 # (D5)
     mode: OUTPUT 
   restore_mode:  ALWAYS_OFF   
   entity_category: diagnostic
   disabled_by_default: true
   on_turn_on:
    then:
      - delay: 50ms
      - switch.turn_off: takeover

 - platform: template
   optimistic: true
   name: Move Up
   entity_category: config
   restore_mode: ALWAYS_OFF
   icon: mdi:arrow-up-bold-box
   id: switch_up
   on_turn_on:
     then:
       - switch.turn_on: takeover
       - switch.turn_off: switch_down
       - lambda: |-
           id(seisuk).move_up();
   on_turn_off:
     then:
       button.press: stop
 
 - platform: template
   name: Move Down
   entity_category: config
   restore_mode: ALWAYS_OFF
   optimistic: true
   icon: mdi:arrow-down-bold-box
   id: switch_down
   on_turn_on:
     then:
       - switch.turn_on: takeover
       - switch.turn_off: switch_up #  interlock: [up]
       - lambda: |-
           id(seisuk).move_down();
   on_turn_off:
     then:
       button.press: stop

script:
  - id: move_it
    parameters:
      to: float
    mode: restart
    then:
      - if:
          condition:
              - binary_sensor.is_off: moving
          then:
            - lambda: |-
                float target = std::round(to * 10) / 10;
                float from = std::round(id(height) * 10) / 10;
      
                ESP_LOGI("seisuk", "target %.1f from %.1f", target, from);
      
                if ( target != from ) {  
                  if ( target < from ) { 
                    id(switch_down).turn_on();
                  }
                  else if ( target > from ) {
                    id(switch_up).turn_on();
                  }
                  else if ( target == from ) {
                    id(stop).press();
                    ESP_LOGI("seisuk", "target == from");
                  }          
                  else {
                    id(stop).press();
                    ESP_LOGI("seisuk", "else height same");
                  }
                }
                else{
                  id(stop).press();
                  ESP_LOGI("seisuk", "height not not same");
                }
          else:
          - if:
              condition:
                  - binary_sensor.is_on: going_up 
              then:
              - lambda: |-
                  float target = std::round(to * 10) / 10;
                  float from = std::round(id(uart_height).state * 10) / 10;

                  ESP_LOGI("seisuk", "moving to %.1f from %.1f", target, from);

                  if ( target <= from ) { 
                    id(stop).press();
                    ESP_LOGE("seisuk", "height more");

                    auto call = id(desk_height).make_call();
                    call.set_value(id(uart_height).state);
                    call.perform();                  
                  }
          - if:
              condition:
                  - binary_sensor.is_on: going_down
              then:
              - lambda: |-
                  float target = std::round(to * 10) / 10;
                  float from = std::round(id(uart_height).state * 10) / 10;

                  ESP_LOGI("seisuk", "moving to %.1f from %.1f", target, from);

                  if ( target >= from ) { 
                    id(stop).press();
                    ESP_LOGE("seisuk", "height less");

                    auto call = id(desk_height).make_call();
                    call.set_value(id(uart_height).state);
                    call.perform();                  
                  }
      - delay: 100ms

  - id: get_debug_height
    mode: single
    then: 
      - lambda: |- 

          // before continuing tell its not really moving

          // id(moving).publish_state(false);
          // id(going_up).publish_state(false);
          // id(going_down).publish_state(false);

          uint8_t data[5];
          while (id(desk_bus).available() >= 5) {

            id(desk_bus).read_array(data, 5);
            if (data[0] != 0x5A) {
              break;
            }
            if ((data[1] | data[2] | data[3]) == 0x00) {
              break;
            }

            enum Segment : uint8_t
            {
                SEGMENT_INVALID = 0x00,
                SEGMENT_0 = 0x3f,
                SEGMENT_1 = 0x06,
                SEGMENT_2 = 0x5b,
                SEGMENT_3 = 0x4f,
                SEGMENT_4 = 0x67,
                SEGMENT_5 = 0x6d,
                SEGMENT_6 = 0x7d,
                SEGMENT_7 = 0x07,
                SEGMENT_8 = 0x7f,
                SEGMENT_9 = 0x6f,
            };
          
            auto segment_to_number = [](const uint8_t segment) {
                switch (segment & 0x7f)
                {
                case SEGMENT_0:
                    return 0;
                case SEGMENT_1:
                    return 1;
                case SEGMENT_2:
                    return 2;
                case SEGMENT_3:
                    return 3;
                case SEGMENT_4:
                    return 4;
                case SEGMENT_5:
                    return 5;
                case SEGMENT_6:
                    return 6;
                case SEGMENT_7:
                    return 7;
                case SEGMENT_8:
                    return 8;
                case SEGMENT_9:
                    return 9;
                default:
                    ESP_LOGD("desktronic", "idk");
                }
                return -1;
            };            

            int data0 = segment_to_number(data[1]);
            int data1 = segment_to_number(data[2]);
            int data2 = segment_to_number(data[3]);

            float got_height = 0.0;

            if (data0 < 0x00 || data1 < 0x00 || data2 < 0x00) {
              break;
            }

            got_height = data0 * 100 + data1 * 10 + data2; // + decimal * 0.1;

            // flip height for below 100
            if (data[2] & 0x80 ) { 
              got_height /= 10.0;
            }
            
            // If all correct, publish values
            if (got_height <= id(max_height) - 0.5 && got_height >= id(min_height) + 0.5 ){
              id(height) = got_height;
              id(uart_height).publish_state(got_height);              
            }

            if (got_height >= id(max_height) || got_height <= id(min_height) ){
              // id(stop).press();
              ESP_LOGE("seisuk","stopped, out of bounds");
            }

            // now its moving
            // id(moving).publish_state(true);
            // id(going_up).publish_state(true);
            // id(going_down).publish_state(true);            

          }
      - delay: 50ms


Converting it to template cover is really nice, and allows exposing to to Google Home for voice control

Thanks again for the code @MhouneyLH!

1 Like

Hey @veli, the interface for controlling the desk in HomeAssistant looks pretty good! I will have a look at it, as this was my first project using HomeAssistant :slight_smile:

I haven’t been working on the desk project for the last few months, so I haven’t checked the forum at all. Sorry about that. But I’m glad to see it’s working for you now. It took me a few weeks to figure out how to modify the desk. You should not feel bad about the time you invested. The result (and of course the adventure) is what counts :slight_smile:

I am currently working on an app that will allow you to control the desk with your smartphone. For the app I will have to update the component a bit. I will try to keep this thread updated. I don’t know if anyone would use it, but theoretically you could test the app on your desk.

1 Like

An app separate from HA/ESPHome? Happy to test it, but personally I use HA to get rid of extra apps :grinning_face_with_smiling_eyes:

I had a small business plan (too lazy to execute it, feel free to steal or collaborate with me) of creating the dongles to sell to our local standing desk manufacturer. An app would for sure be a necessity for that since their customers woult not use HA or like to connect to the ESP AP to control it…

I rewrote bits of the component, hence using it as this - public functions, more binary sensors, etc:

  - source: components
    components: [ desktronic ]  
1 Like

A friend of mine and I also had this business plan. But at the moment (when the app is ready) it is in the first place just a fun gadget to use. Maybe the business plan will be executed in the future, but currently I’m too lazy too.

Of course, people who use HomeAssistant, probably won’t use the app the as you explained. :slight_smile:

1 Like

Technically, JS already has a BT adapter on the market: Standing Desk Bluetooth from China manufacturer - JS Technology

No WiFi adapter yet.

I have the same jsm-2.1-0.3w controller in my desk, however the control panel pinout seems to be quite different (as well as the colours for the pins).

The RJ50 end has colours in order of brown, black, white, orange, red, blue, white, red, green and yellow.

The controller on the other hand, uses blue, green, white, black, red, brown and yellow (with a not connected pin between red and brown).

The labels are, same order as the colours: “G”, “5”, “T”, “R”, “P”, NC, “U_G”, “DC”

The cabling actually seems to line up with @MhouneyLH’s from the first post, with the main difference limited to a colour change - purple, aka “G” is blue on mine, purple on theirs.

I’ve ordered an RJ50 male to female short cable (30cm) that I’m going to cannibalize and embed an ESP32 in a small through-package.

Oh and I also managed to coax out a bit of documentation from the seller of my desk, EYOJYA - they’ve sent over the documentation for a BLUETOOTH adapter they don’t even have on sale! Nonetheless here’s the PDF, it seems to use the same, or at the very least, incredibly similar command structure, even if it’s kinda full of typos.

@veli would you mind sharing your changes to the desktronic component?

Oh, and also, JS also has some quite much fancier versions of the controllers, with (apparently controllable?) RGB lighting, motorised knob, and a proper OLED display:

1 Like

In the desktronic.h, just moved things from private to public so i can lambda call id(desk).move_up() and down. Will check for any not needed test comments in files, fork the OP code on github and perhaps we can make it work without hacks :slight_smile:

#pragma once

#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/number/number.h"

namespace esphome
{
namespace desktronic
{

enum DesktronicOperation : uint8_t
{
    DESKTRONIC_OPERATION_IDLE = 0U,
    DESKTRONIC_OPERATION_RAISING,
    DESKTRONIC_OPERATION_LOWERING,
    DESKTRONIC_OPERATION_UP,
    DESKTRONIC_OPERATION_DOWN,
    DESKTRONIC_OPERATION_MEMORY_1,
    DESKTRONIC_OPERATION_MEMORY_2,    
};

enum Segment : uint8_t
{
    SEGMENT_INVALID = 0x00,
    SEGMENT_0 = 0x3f,
    SEGMENT_1 = 0x06,
    SEGMENT_2 = 0x5b,
    SEGMENT_3 = 0x4f,
    SEGMENT_4 = 0x67,
    SEGMENT_5 = 0x6d,
    SEGMENT_6 = 0x7d,
    SEGMENT_7 = 0x07,
    SEGMENT_8 = 0x7f,
    SEGMENT_9 = 0x6f,
};

enum MovingIdentifier : uint8_t
{
    MOVING_IDENTIFIER_UP = 0x20,
    MOVING_IDENTIFIER_DOWN = 0x40,
    MOVING_IDENTIFIER_MEMORY_1 = 0x02,
    MOVING_IDENTIFIER_MEMORY_2 = 0x04,
    MOVING_IDENTIFIER_MEMORY_3 = 0x08,
    MOVING_IDENTIFIER_MEMORY_4 = 0x10,    
};

static const char* desktronic_operation_to_string(const DesktronicOperation operation);
static int segment_to_number(const uint8_t segment);

class Desktronic : public Component
{
public:
    float get_setup_priority() const override { return setup_priority::LATE; }
    void setup() override;
    void loop() override;
    void dump_config() override;

    void set_remote_uart(uart::UARTComponent* uart) { remote_uart_ = uart; }
    void set_desk_uart(uart::UARTComponent* uart) { desk_uart_ = uart; }
    void set_move_pin(GPIOPin* pin) { move_pin_ = pin; }
    void set_height_sensor(sensor::Sensor* sensor) { height_sensor_ = sensor; }
    void set_up_bsensor(binary_sensor::BinarySensor* sensor) { up_bsensor_ = sensor; }
    void set_down_bsensor(binary_sensor::BinarySensor* sensor) { down_bsensor_ = sensor; }
    void set_memory1_bsensor(binary_sensor::BinarySensor* sensor) { memory1_bsensor_ = sensor; }
    void set_memory2_bsensor(binary_sensor::BinarySensor* sensor) { memory2_bsensor_ = sensor; }
    void set_memory3_bsensor(binary_sensor::BinarySensor* sensor) { memory3_bsensor_ = sensor; }

    void move_to_from(const float height_in_cm, const float current_height);
    void move_to(const float height_in_cm);
    void stop();
    void move_up();
    void move_down();
    void move_to_memory_1();
    void move_to_memory_2();

public:
    DesktronicOperation current_operation{DesktronicOperation::DESKTRONIC_OPERATION_IDLE};

private:
    void read_remote_uart();
    void read_desk_uart();
    void publish_remote_states(const uint8_t data);
    void reset_remote_buffer();
    void reset_desk_buffer();

    bool must_move_up(const float height_in_cm) const;
    void move_to_target_height();

    bool isCurrentHeightValid() const;
    bool isCurrentHeightInTargetBoundaries() const;

protected:
    uart::UARTComponent* remote_uart_{nullptr};
    uart::UARTComponent* desk_uart_{nullptr};
    GPIOPin* move_pin_{nullptr};
    sensor::Sensor* height_sensor_{nullptr};
    binary_sensor::BinarySensor* up_bsensor_{nullptr};
    binary_sensor::BinarySensor* down_bsensor_{nullptr};
    binary_sensor::BinarySensor* memory1_bsensor_{nullptr};
    binary_sensor::BinarySensor* memory2_bsensor_{nullptr};
    binary_sensor::BinarySensor* memory3_bsensor_{nullptr};

    std::vector<uint8_t> remote_buffer_;
    std::vector<uint8_t> desk_buffer_;
    bool is_remote_rx_uart_message_start_found{false};
    bool is_desk_rx_uart_message_start_found{false};
    float current_height_{0.0};
    float target_height_{-1.0};
};
} // namespace desktronic
} // namespace esphome
2 Likes

I’ve done some further investigation.

The BT protocol I was sent? It uses a secondary UART port on the 12V lines - B-RX, B-TX and B-C1 pins on the MC side.

Here’s the FCCID page for the BT dongle

I’ve also grabbed the close-up shots of the board:

With some image editing wizardry the PCB layout should be somewhat salvageable. However the logic is pretty straightforward, it takes the RJ50 incoming port, splits it out to 10 pins, puts 5V, GND, KEY1, KEY2 and the UART pins forward, and breaks out B-RX, B-TX, and B-C1 for the Bluetooth MCU:

The orange path, B-TX, is pretty straightforward, it goes onot an UART pin of the MCU, with a voltage divider in the way.

B-RX on the other hand… It runs into a resistor that has B-C1 on the other end, then these two run into a diode that is hooked up to a separate pin on the MCU, and finally the output goes through another resistor before heading into the MCU’s other UART pin (transmit side of MCU).

Then finally purple… Now that’s a total mystery for me, it doesn’t seem to go anywhere or do anything.

According to the BLE chip’s spec sheet, Orange and Blue pins are an UART port, whereas Yellow and Purple are an I2C port. However I see no I2C component there, or anywhere on the board…

Nonetheless, this puts us a step closer to having a proper, non-injection control solution for these desks, as we have the documentation for the B-bus messages, as well as the pins to use.

Fair warning, one CAN NOT hook up an ESP32 or similar to the B-RX and B-TX pins, as these operate at 12V - there needs to be some level of voltage conversion, via optocouplers or similar!


I’ve also discovered a secret menu on my controller. By pressing the REMINDER/TIMER button first, then the M button, you’ll be presented with a 7-entry menu.

I’m yet to figure out all the menu options, but here’s what i’ve found so far:

1 - boolean, 0 or 1 as values, defaults to 1
2 - numeric, 0 to 8 as possible values, defaults to 3 on my controller
3 - numeric, 0 to 7 as possible values, defaults to 6 on my controller
4 - numeric, 0 to 7 as possible values, defaults to 6 on my controller
5 - numeric, 0 to 9 as possible values, defaults to 5 on my controller
6 - numeric, menu option is not displayed, defaults to 60 on my controller (I believe this is the minimum height of the desk)
7 - boolean, 0 or 1 as values, defaults to 1
1 Like

Oh and one more thing, the DC and U_G pins on my HC seem to be 35V instead of 12V - can anyone else confirm what their desk reads at?

1 Like

@veli this looks nice and almost works for me! Did you change any code in the cpp file? I get messages saying the checksum did not match, and I wondered if you changed that.

1 Like