Turn your Desktronic standing desk into a smart desk!

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

Indeed, here is the fork. :slight_smile:

1 Like

Hello guys, this is my first time ever in the world of home assistant.

I have a desk that is exactly the same as Veli’s from what i saw.
Using this post i thought i could make my desk also smart (then it would be smarter than me).

I bought a d1 mini v3 (esp01_1m) and I cloned GitHub - velijv/esphome_custom_components: A collection of custom components for esphome. .

I also copied the following config Turn your Desktronic standing desk into a smart desk! - #38 by veli with the only changes being that i changed the board from nodemcu to esp01_1m and also adding “allow_other_uses: true” to all references of pin number 14 (compiler was giving me errors without it)

Then set up the rj45 cable to pins as follows

I Know that i may be doing something wrong (by my side as always) because I have some trouble making it work.
Firstly my d1 mini does not power up when connected with the referred pinout (where as if i give 5v from my arduino to 5v pin in d1 mini OR usb it powers up normally).

Having it as is (without it getting power) the manual control panel works (ups,lowers desk and memory buttons work)

Now lets say that while i had it connected using the previously mentioned pinout and powering it with a usb as soon as it gets voltage it raises the desk to its max height without me pressing anything
So my final pinout is as follows:

P → RED = D5
R → BLACK = Rx
T → WHITE = TX
5 → GREEN = 5V
G → PURPLE = GND

Any tip or guidance is more than welcome ^^

Appreciate all the work all of you mates put to make it possible up until now.
Cheers! :smiley:

Hi @kostaskam!

Sorry for the late reply. Try to set this switch to not internal, and toggle it off - then it should stop. this is the takeover switch for me:

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

Depending on the ESP model, you might need to fiddle with:

  • early_pin_init (Optional, boolean): Specifies whether pins should be initialised as early as possible to known values. Recommended value is false where switches are involved, as these will toggle when updating the firmware or when restarting the device. Defaults to true.
  • restore_from_flash (Optional, boolean): Whether to store some persistent preferences in flash memory. Defaults to false.

I firstly would like to say, kudos for doing this, I am also one of them geeks that seems to be automating “everything”. I actually stumbled into this thread due to the fact I have one of these white box desks that I purchased from Amazon during COVID. I started actually playing about to figure out how it ticked and wanted to have it a little more automated aka via ESPHome. Anyhow, this is where I am today. I have a ESP32 (M5 Stack Atom) running fine off the voltage coming from the pins and the code @veli created works (ish).

However, I have a problem, I am not sure why but the code does not fully work. The desk moves a little bit sometimes when requested and also does not move sometimes on the same turn of the page, which is makes the commands very hit and miss. I have tried to identify the differences that everyone has been recommended aka the ESP8266 compared to the ESP32 and the only thing I can see is the CPU clock is 80mhz rather than 240mhz that the ESP32 clock is. So I have down clocked the ESP32 to 80mhz with still no fix. Looking at the console, with debug on, I get logs of checksum mismatches?? which I assume is the start of the issue.

I am a little at a loss on what to do here its almost like it wants to work but just cant do the last thing which is move the desk. Everything else works, aka I can see the height of the desk consistently and when it does move the height changes (Yes it only changes by .1 but I can see it change) and send it to pre-sets but when the motors need to move they are either moving slow, and or lumpy or not at all. Anything anyone can suggest on what to try?

It would seem my fork was not perfect as described here partly:

Happy if someone would collaborate to improve it :slight_smile:

I still haven’t got even the buttons to register as sensors, and latest updates have rendered my code broke, as it wont stop in promised location as previously… might have to swap the nodemcu with something more modern.


seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
seisuk:617 - idk
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.50000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.80000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.80000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.80000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.80000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.80000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.80000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.90000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.90000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 89.90000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.00000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.00000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.00000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.10000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.10000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.20000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.30000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.30000 cm with 1 decimals of accuracy
seisuk:617 - idk
sensor:093 - 'Seisuk': Sending state 90.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.60000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 90.80000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.00000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.00000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.10000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.20000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.30000 cm with 1 decimals of accuracy
seisuk:617 - idk
sensor:093 - 'Seisuk': Sending state 91.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.70000 cm with 1 decimals of accuracy
sensor:093 - 'Seisuk': Sending state 91.80000 cm wit

Might be a simple case of fixing the segment map:

// 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};

I don’t see why you’re quoting the debug menu, it’s not relevant to the UART proxying.

Also, I had the MC and HC replaced by my retailer as mine died, and on the new controller (slightly different model with push buttons, PCB has no labels at all), the default values changed somewhat:

Menu option Old value New value
1 1 0
2 3 3
3 6 6
4 6 6
5 5 4
6 60 60
7 1 1

I’m guessing field 6 is the minimum height (as it is 60cm on my unit), however changing this does not effect the displayed values, even after a reset.

It’s still unclear what the rest means.

As for your issues with updates, I presume you mean ESPHome updates?

This is great to have found. In late-December 2021 I worked on reversing the protocol used in this controller, but other things got in the way of taking it further (notes are here in case there is anything there that I found that might be useful).

One funny thing is the encoding used, which I don’t think has been noted here beyond at hex mapping, quoted below:

Codes are related directly to the display:

     01234567 01234567 01234567 sum(data)
66.0 01111101 11111101 00111111 10111001
66.1 01111101 11111101 00000110 10000000
66.2 01111101 11111101 01011011 11010101
66.3 01111101 11111101 01001111 11001001
66.4 01111101 11111101 01100110 11100000
66.5 01111101 11111101 01101101 11100111
66.6 01111101 11111101 01111101 11110111
66.7 01111101 11111101 00000111 10000001
66.8 01111101 11111101 01111111 11111001
66.9 01111101 11111101 01101111 11101001
67.0 01111101 10000111 00111111 01000011
67.1 01111101 10000111 00000110 00001010
67.2 01111101 10000111 01011011 01011111
67.3 01111101 10000111 01001111 01010011
67.4 01111101 10000111 01100110 01101010
67.5 01111101 10000111 01101101 01110001
67.6 01111101 10000111 01111101 10000001
67.7 01111101 10000111 00000111 00001011
74.0 00000111 11100110 00111111 00101100

7-segment display

  -5-
|     |
2     6
|     |
  -1-
|     |
3     7
|     |
  -4-   0

Last byte is a literal checksum (0th+1st+2nd).

Digits:
0 = 0x3f  0. = 0xbf
1 = 0x06  1. = 0x86
2 = 0x5b  2. = 0xdb
3 = 0x4f  3. = 0xcf
4 = 0x66  4. = 0xe6
5 = 0x6d  5. = 0xed
6 = 0x7d  6. = 0xfd
7 = 0x07  7. = 0x87
8 = 0x7f  8. = 0xff
9 = 0x6f  9. = 0xef


Note that 4 does not match the pattern, it should be x1100011 but is x1100110

Hi @MhouneyLH - Thanks for your write-up and nice that it works.

I have the same desk controller and trying to build the same thing, but I’m a little confused about the wiring… There is desk_bus and remote_bus in the YAML for ESPHome. I don’t really get the difference - sorry if this sounds stupid.

If I understand it correctly (please correct me if I’m wrong), I have to wire up the RX and TX from the ESP to the desk in parallel to the remote and that should be it?

Or do I have to wire the RX/TX from the remote to the ESP and then RX/TX from the ESP to the desk?