Ideal Reality

興味の赴くままに
  1. トップ
  2. 投稿
  3. FlexiSpot E7 Proのコントローラーの信号を解析してみた
add_22026年2月1日 18時22分

FlexiSpot E7 Proのコントローラーの信号を解析してみた

FlexiSpotの電動昇降式デスクにESP32を接続して、WebブラウザやHome Assistant等から操作できるようにするというのは、まあまあメジャーな改造かと思います。

GitHub - iMicknl/LoctekMotion_IoT

Learn how to connect your Flexispot (LoctekMotion) desk to the internet. This repository contains a collection of scripts to get your started, combined with research and instructions.

コントローラーとの通信についてはこちらのGitHubレポジトリにまとまってるし、HomeAssistantと連携させるためのESPHomeのyamlファイルも配布されているため、簡単に導入することができます。
とはいえ、こちらはあくまで純正コントローラーを残しつつESP32を追加する改造であるため、純正コントローラーを置き換えできる代物ではありません。
今回、自分はFlexiSpotのコントローラーを外して、M5Stack Tab5を設置する計画なので、なるべく純正コントローラーと同等の使い勝手を実現できるように通信仕様を調査してみました。

スポンサーリンク

マイコン・接続

制御するマイコンにはESP32S3(AtomS3-Lite)を使いました。接続は参考元のGitHubレポジトリとそんなに相違ありません。マイコンが違うからGPIOピンが違うくらい

RJ45 PinFlexiSpotAtom-S3 Lite
8+5V (VDD)5V (VIN_5V)
7GNDG (GND)
6RXG5 (UART1 TX)
5TXG6 (UART1 RX)
4DetectG7 (GPIO7)
3
2
1

Pin8と7は電源
Pin6と5を使ってUARTで通信します。Pin6がESP32->FlexiSpot方向の通信で、Pin5がFlexiSpot->ESP32の通信になります。
Pin4について、参考元のGitHubではPIN 20と記載されており、その他調べるとWake Upピンと説明されていることがあったりしますが、自分が手元で試した感じだと

  • Highにしている間FlexiSpot本体からUARTのパケットが届く
  • LowにするとFlexiSpotからの通信が止まる
  • コントローラーは常時5Vを出力
  • Low -> HighになったタイミングでLCD出力

という感じでコントローラーが接続されているかどうかを検出するために使っていそうな挙動だったので、Detectって表記にしました。
なので、ここはESP32と接続する必要はなく、Pin8とショートさせるだけでいいのですが、Low->Highと切り替えることで任意のタイミングでLCDを点灯させることができるため繋いでおいても損はないと思います。

なお、FlexiSpotが電源Offの場合にPin4を1秒間Highにして電源Onにする必要があるとの記述も見かけましたが、とくにその必要はなさそうでした。
Pin4がHighなら通信可能 == Pin4は常時Highにしておけばいい、それだけです。

パケット

通信は9600bpsのUART上で、パケット単位でやり取りされます。

構造

UART上での通信に使ってるパケット構造です。0x9Bから始まり、0x9Dで終わります。
その他特殊なエンコードとかはありません。

SizeNote
0x9B1開始バイト
Length1パケットの長さ。Type,Payload,Checksum,0x9Dのサイズ合計値
Type1データタイプ
Payload0~N実際にやり取りするデータ
Checksum2Length,Type,Payload(上図の青い範囲)のCRC-16/MODBUS, Big Endian
0x9D1終了バイト

主なパケット

状態リクエスト

DirectionLengthTypePayload
本体 -> コントローラー40x11なし

本体からコントローラーに対する状態取得リクエストです。Payloadはありません。
約40ms間隔で本体からコントローラーに送られ、コントローラーはこのパケットを受信すると次セクションで説明するボタンの状態を本体に応答します。

ボタンの状態

DirectionLengthTypePayload
コントローラー -> 本体60x02ボタンの押下状態(2バイト)

参考元ではCommandと表現されていますが、これはコントローラーからFlexiSpotを操作するコマンドとかではなく、単に押されているボタンの状態を表現しています。
HID KeyboardのInput Reportみたいなものです。
なので、Payloadが 00 00 のパケットは、Wake Upコマンドではなく、単にボタンが押されていないという状態を送っているだけですね。

ボタンPayload Bit
Up01 00
Down02 00
M20 00
Preset 104 00
Preset 208 00
Preset 3 (stand)10 00
Preset 4 (sit)00 01

本体側から 9b 04 11 7c c3 9d が届いたときに、ボタンが何も押されていなければ 9b 06 02 00 00 6c a1 9d を、Upが押されていれば 9b 06 02 01 00 fc a0 9d を送るようなシーケンスになります。
また、Up/Downボタン同時押しで緊急停止の感度調整ができたりしますが、そういうボタンの同時押しがされているときは、たとえばUpとDown両方同時押しなら 01 0002 00 の OR をとった 03 00 をPayloadに入れて 9b 06 02 03 00 9c a1 9d を送る形になります。

画面表示の状態

DirectionLengthTypePayload
本体 -> コントローラー70x127セグメントLEDの表示状態(3バイト)

これに関しては参考元のGitHubレポジトリ記載の通りです。
7セグメントLEDのどの場所を点灯させるか指定する形で表示内容が届きます。Payloadは3桁の表示内容で3バイトで、画面が消えているときはもちろん 00 00 00 が来ます。
これを読み取ってうまく文字列・数値に変換することで高さを取得することができます。
また、数字以外にもたとえばMボタンを押したときに 5- と表示されるハイフンだったり、ロック時の LoC とかリセット時の RST なんかが表示されるため、以下の表くらいの表示は対応しておくといいかと思います。
ちなみに、RSTのSは5と区別がつかないので、そこはうまくR5Tが来たときに置き換えるとかで対処する必要があります。

Payloadの値表示Payloadの値表示Payloadの値表示
0x3F00xBF0.0x40-
0x0610x861.0x39C
0x5B20xDB2.0x79E
0x4F30xCF3.0x38L
0x6640xE64.0x77R
0x6D50xED5.0x31T
0x7D60xFD6.0x5Co
0x0770x877.
0x7F80xFF8.
0x6F90xEF9.

不明なパケット1

DirectionLengthTypePayload
本体 -> コントローラー40x15なし

本体からコントローラーに対して頻繁に送られているパケットです。
約16msごとに本体から届いていて、状態リクエストよりも頻度が高いです。
Keep-aliveとか、コントローラーのクロックとかで使う用なのかなと推測してて、コントローラーを自作するときは特に読み取る必要なさそうだなと思っています。

不明なパケット2

DirectionLengthTypePayload
コントローラー -> 本体50x020x82

コントローラーが電源On直後、最初の状態リクエストが届くまで100ms間隔で本体に送っています。
おそらくこれがコントローラー側から本体を叩き起こすWake Upコマンドに相当するものじゃないかなと思っているのですが、それがなくてもE7 Proは動いてくれるので、これも特に必要はなさそうです。

スポンサーリンク

ロック

Mボタン長押しでロックする際に、青色のLEDが点滅・点灯したり音が鳴ります。
これに関してはFlexiSpot本体側ではなくコントローラー側で制御してそうな感じがしました。解析結果としては以下のような感じ。

  • LED点灯(ロック)・消灯(ロック解除)時で画面表示以外のパケットに変化がない
  • 純正コントローラーとESP32両方接続して、ESP32側からMボタン長押し操作をしてもLED点滅・ブザー鳴動しない
  • コントローラーから電源とTx/Rx以外の接続を遮断してもLED・ブザーが動作する

なので、ロック時の挙動を再現するなら

  • Mボタンが押されている && LED表示がLoC: LED点滅 + ブザー
  • 現在or最後のLED表示内容がLoC: LED点灯
  • それ以外: LED消灯

のような形でいいかなと思います。

実装

特に語ることはないです。UARTで通信して終わりです。

#pragma once
#include "esp_err.h"
#include <stdint.h>
#include <stdbool.h>

#ifdef __cplusplus
extern "C" {
#endif

typedef void (*flexispot_notify_callback_t)();

typedef enum {
    FLEXISPOT_BUTTON_NONE    = 0,
    FLEXISPOT_BUTTON_UP      = 0x0001,
    FLEXISPOT_BUTTON_DOWN    = 0x0002,
    FLEXISPOT_BUTTON_MEMORY  = 0x0020,
    FLEXISPOT_BUTTON_PRESET1 = 0x0004,
    FLEXISPOT_BUTTON_PRESET2 = 0x0008,
    FLEXISPOT_BUTTON_PRESET3 = 0x0010,
    FLEXISPOT_BUTTON_PRESET4 = 0x0100,
} flexispot_button_t;
extern flexispot_button_t flexispot_button_state;

void flexispot_send_packet(uint8_t type, const uint8_t *payload, size_t size);
void flexispot_set_button_state(flexispot_button_t buttons);
void flexispot_get_lcd_str(char *buffer, size_t size);
void flexispot_register_notify_callback(flexispot_notify_callback_t callback);
esp_err_t flexispot_init(void);

#ifdef __cplusplus
}
#endif
#include "flexispot.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include <string.h>

static const char *TAG = "flexispot";
flexispot_button_t flexispot_button_state = FLEXISPOT_BUTTON_NONE;

// UART Configuration for M5AtomS3
#define FLEXISPOT_UART_NUM      UART_NUM_1
#define FLEXISPOT_TXD_PIN       GPIO_NUM_5
#define FLEXISPOT_RXD_PIN       GPIO_NUM_6
#define FLEXISPOT_DETECT_PIN    GPIO_NUM_7
#define FLEXISPOT_BUF_SIZE      1024

static uint16_t crc16_modbus(const uint8_t *data, size_t len) {
    uint16_t crc = 0xFFFF;
    for (size_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (int j = 0; j < 8; j++) {
            if (crc & 1) crc = (crc >> 1) ^ 0xA001;
            else crc >>= 1;
        }
    }
    return crc;
}

void flexispot_send_packet(uint8_t type, const uint8_t *payload, size_t size) {
    uint8_t packet[32] = { 0x9b, size + 4, type };
    if (size) memcpy(&packet[3], payload, size);
    uint16_t checksum = crc16_modbus(&packet[1], size + 2);
    packet[3 + size] = checksum >> 8;    // CRC high byte first (big endian)
    packet[4 + size] = checksum & 0xFF;  // CRC low byte
    packet[5 + size] = 0x9d;

    char hex_str[128] = {0};
    for (int i = 0; i < 6 + size; i++) {
        sprintf(hex_str + i * 3, "%02x ", packet[i]);
    }
    // ESP_LOGI(TAG, "TX: %s", hex_str);

    int len = uart_write_bytes(FLEXISPOT_UART_NUM, packet, 6 + size);
    if (len != 6 + size) ESP_LOGE(TAG, "Failed to send packet");
}

static void send_button_state(flexispot_button_t buttons) {
    // ESP_LOGI(TAG, "Sending button state [0x%04d]", buttons);
    flexispot_send_packet(0x02, (uint8_t[]){ buttons & 0xFF, buttons >> 8 }, 2);
}

/*
* MARK: RX
*/
static int rx_offset = -1, rx_remain = 0;
static uint8_t rx_buf[128];
static SemaphoreHandle_t current_state_mutex;
static struct {
    char lcd_str[32];
    int height;
} current_state;
static flexispot_notify_callback_t notify_callback;

static char *decode_7seg(char *str, uint8_t byte) {
    struct { char c; uint8_t code; } chars[] = {
        { '-', 0x40 },
        { '0', 0x3f },
        { '1', 0x06 },
        { '2', 0x5b },
        { '3', 0x4f },
        { '4', 0x66 },
        { '5', 0x6d },
        { '6', 0x7d },
        { '7', 0x07 },
        { '8', 0x7f },
        { '9', 0x6f },
        { 'C', 0x39 },
        { 'E', 0x79 },
        { 'L', 0x38 },
        { 'R', 0x77 },
        { 'T', 0x31 },
        { 'o', 0x5c },
    };

    // Remove decimal point bit for comparison
    uint8_t segments = byte & 0x7f;
    for (int i = 0; i < sizeof(chars) / sizeof(chars[0]); i++) {
        if (chars[i].code == segments) {
            *str++ = chars[i].c;
            break;
        }
    }
    if (byte & 0x80) {
        *str++ = '.';
    }
    return str;
}

static void display_packet_received(uint8_t *data, int size) {
    if (size != 7) return;
    char buf[8] = {}, *ptr = buf;
    for (int i = 1; i <= 3; i++) ptr = decode_7seg(ptr, data[i]);
    if (ptr != buf) {
        ESP_LOGI(TAG, "Display: %s", buf);
        xSemaphoreTake(current_state_mutex, portMAX_DELAY);
        strncpy(current_state.lcd_str, buf, sizeof(current_state.lcd_str));
        xSemaphoreGive(current_state_mutex);
        if (notify_callback) notify_callback();
    }
}

void flexispot_get_lcd_str(char *buffer, size_t size) {
    xSemaphoreTake(current_state_mutex, portMAX_DELAY);
    strncpy(buffer, current_state.lcd_str, size);
    xSemaphoreGive(current_state_mutex);
}

void flexispot_register_notify_callback(flexispot_notify_callback_t callback) {
    notify_callback = callback;
}

static void rx_consume_byte(uint8_t byte) {
    if (rx_offset < 0) {
        if (byte == 0x9b) rx_offset = 0;
        return;
    }
    if (rx_remain == 0) {
        rx_remain = byte;
        return;
    }
    rx_buf[rx_offset++] = byte;
    if (--rx_remain == 0) {
        if (rx_buf[rx_offset - 1] == 0x9d) {
            char hex_str[64] = {0};
            for (int i = 0; i < rx_offset && i < 20; i++) {
                sprintf(hex_str + i * 3, "%02x ", rx_buf[i]);
            }
            // ESP_LOGI(TAG, "RX[%d]: %s", rx_offset, hex_str);
            if (rx_buf[0] == 0x11) send_button_state(flexispot_button_state);
            if (rx_buf[0] == 0x12) display_packet_received(rx_buf, rx_offset);
        }
        rx_offset = -1;
    }
}

static void rx_task(void *pvParameters) {
    uint8_t rx_buf[16];

    ESP_LOGI(TAG, "UART Rx task started");

    while (1) {
        int len = uart_read_bytes(FLEXISPOT_UART_NUM, rx_buf, sizeof(rx_buf), pdMS_TO_TICKS(100));
        for (int i = 0; i < len; i++) rx_consume_byte(rx_buf[i]);
    }
}

static esp_err_t rx_task_start(void) {
    BaseType_t ret = xTaskCreate(rx_task, "flexispot", 4096, NULL, 5, NULL);
    if (ret != pdPASS) {
        ESP_LOGE(TAG, "Failed to create UART Rx task");
        return ESP_FAIL;
    }

    ESP_LOGI(TAG, "UART Rx task created");
    return ESP_OK;
}

esp_err_t flexispot_init(void) {
    current_state_mutex = xSemaphoreCreateMutex();
    if (current_state_mutex == NULL) {
        ESP_LOGE(TAG, "Failed to create current_state_mutex");
        return ESP_FAIL;
    }

    esp_err_t ret;

    // Configure UART parameters
    uart_config_t uart_config = {
        .baud_rate = 9600,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
        .source_clk = UART_SCLK_DEFAULT,
    };

    // Install UART driver
    ret = uart_driver_install(FLEXISPOT_UART_NUM, FLEXISPOT_BUF_SIZE * 2, 0, 0, NULL, 0);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to install UART driver: %s", esp_err_to_name(ret));
        return ret;
    }

    // Configure UART parameters
    ret = uart_param_config(FLEXISPOT_UART_NUM, &uart_config);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to configure UART parameters: %s", esp_err_to_name(ret));
        uart_driver_delete(FLEXISPOT_UART_NUM);
        return ret;
    }

    // Set UART pins
    ret = uart_set_pin(FLEXISPOT_UART_NUM,
                      FLEXISPOT_TXD_PIN,
                      FLEXISPOT_RXD_PIN,
                      UART_PIN_NO_CHANGE,
                      UART_PIN_NO_CHANGE);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to set UART pins: %s", esp_err_to_name(ret));
        uart_driver_delete(FLEXISPOT_UART_NUM);
        return ret;
    }

    // Configure Detect PIN
    gpio_config_t detect_pin_config = {
        .pin_bit_mask = (1ULL << FLEXISPOT_DETECT_PIN),
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    ret = gpio_config(&detect_pin_config);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to configure detect pin: %s", esp_err_to_name(ret));
        uart_driver_delete(FLEXISPOT_UART_NUM);
        return ret;
    }
    gpio_set_level(FLEXISPOT_DETECT_PIN, 1); // set detect pin always high

    // Start UART Rx Task
    ret = rx_task_start();
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to start UART Rx Task: %s", esp_err_to_name(ret));
        uart_driver_delete(FLEXISPOT_UART_NUM);
        return ret;
    }
    ESP_LOGI(TAG, "Flexispot UART initialized successfully");
    ESP_LOGI(TAG, "  UART: UART%d, TX: GPIO%d, RX: GPIO%d",
            FLEXISPOT_UART_NUM, FLEXISPOT_TXD_PIN, FLEXISPOT_RXD_PIN);

    return ESP_OK;
}
スポンサーリンク

余談

今回僕の目的としてはM5Stack Tab5から操作することなので、とりあえずAtomS3-LiteをGATT ServerにしてBluetooth経由で操作しました。
FlexiSpot用のサービスにボタン用のWrite可能なCharacteristicと、ディスプレイ表示用のRead+Notify可能なCharacteristicを用意する形です。

共有

この記事が役に立ったならば、シェアしていただけると嬉しいです。

スポンサーリンク
関連する投稿
関連する投稿はありません
プロフィール
Hiroki Kawakami

サイトを作り替えています

スポンサーリンク