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

FlexiSpotの電動昇降式デスクにESP32を接続して、WebブラウザやHome Assistant等から操作できるようにするというのは、まあまあメジャーな改造かと思います。
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 Pin | FlexiSpot | Atom-S3 Lite |
|---|---|---|
| 8 | +5V (VDD) | 5V (VIN_5V) |
| 7 | GND | G (GND) |
| 6 | RX | G5 (UART1 TX) |
| 5 | TX | G6 (UART1 RX) |
| 4 | Detect | G7 (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で終わります。
その他特殊なエンコードとかはありません。
| Size | Note | |
|---|---|---|
| 0x9B | 1 | 開始バイト |
| Length | 1 | パケットの長さ。Type,Payload,Checksum,0x9Dのサイズ合計値 |
| Type | 1 | データタイプ |
| Payload | 0~N | 実際にやり取りするデータ |
| Checksum | 2 | Length,Type,Payload(上図の青い範囲)のCRC-16/MODBUS, Big Endian |
| 0x9D | 1 | 終了バイト |
主なパケット
状態リクエスト
| Direction | Length | Type | Payload |
|---|---|---|---|
| 本体 -> コントローラー | 4 | 0x11 | なし |
本体からコントローラーに対する状態取得リクエストです。Payloadはありません。
約40ms間隔で本体からコントローラーに送られ、コントローラーはこのパケットを受信すると次セクションで説明するボタンの状態を本体に応答します。
ボタンの状態
| Direction | Length | Type | Payload |
|---|---|---|---|
| コントローラー -> 本体 | 6 | 0x02 | ボタンの押下状態(2バイト) |
参考元ではCommandと表現されていますが、これはコントローラーからFlexiSpotを操作するコマンドとかではなく、単に押されているボタンの状態を表現しています。
HID KeyboardのInput Reportみたいなものです。
なので、Payloadが 00 00 のパケットは、Wake Upコマンドではなく、単にボタンが押されていないという状態を送っているだけですね。
| ボタン | Payload Bit |
|---|---|
| Up | 01 00 |
| Down | 02 00 |
| M | 20 00 |
| Preset 1 | 04 00 |
| Preset 2 | 08 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 00 と 02 00 の OR をとった 03 00 をPayloadに入れて 9b 06 02 03 00 9c a1 9d を送る形になります。
画面表示の状態
| Direction | Length | Type | Payload |
|---|---|---|---|
| 本体 -> コントローラー | 7 | 0x12 | 7セグメントLEDの表示状態(3バイト) |
これに関しては参考元のGitHubレポジトリ記載の通りです。
7セグメントLEDのどの場所を点灯させるか指定する形で表示内容が届きます。Payloadは3桁の表示内容で3バイトで、画面が消えているときはもちろん 00 00 00 が来ます。
これを読み取ってうまく文字列・数値に変換することで高さを取得することができます。
また、数字以外にもたとえばMボタンを押したときに 5- と表示されるハイフンだったり、ロック時の LoC とかリセット時の RST なんかが表示されるため、以下の表くらいの表示は対応しておくといいかと思います。
ちなみに、RSTのSは5と区別がつかないので、そこはうまくR5Tが来たときに置き換えるとかで対処する必要があります。
| Payloadの値 | 表示 | Payloadの値 | 表示 | Payloadの値 | 表示 | ||
|---|---|---|---|---|---|---|---|
| 0x3F | 0 | 0xBF | 0. | 0x40 | - | ||
| 0x06 | 1 | 0x86 | 1. | 0x39 | C | ||
| 0x5B | 2 | 0xDB | 2. | 0x79 | E | ||
| 0x4F | 3 | 0xCF | 3. | 0x38 | L | ||
| 0x66 | 4 | 0xE6 | 4. | 0x77 | R | ||
| 0x6D | 5 | 0xED | 5. | 0x31 | T | ||
| 0x7D | 6 | 0xFD | 6. | 0x5C | o | ||
| 0x07 | 7 | 0x87 | 7. | ||||
| 0x7F | 8 | 0xFF | 8. | ||||
| 0x6F | 9 | 0xEF | 9. |
不明なパケット1
| Direction | Length | Type | Payload |
|---|---|---|---|
| 本体 -> コントローラー | 4 | 0x15 | なし |
本体からコントローラーに対して頻繁に送られているパケットです。
約16msごとに本体から届いていて、状態リクエストよりも頻度が高いです。
Keep-aliveとか、コントローラーのクロックとかで使う用なのかなと推測してて、コントローラーを自作するときは特に読み取る必要なさそうだなと思っています。
不明なパケット2
| Direction | Length | Type | Payload |
|---|---|---|---|
| コントローラー -> 本体 | 5 | 0x02 | 0x82 |
コントローラーが電源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で通信するだけですが、Pattern detectionを使って、0x9dが来たタイミングで処理できるようにしておくと、UARTでパケットを受信してから実際に処理するまでの遅延を小さくできるのでいいでしょう。
以下にIDF Componentとして使える形で公開しているので参考にどうぞ。
Reusable ESP-IDF Components. Contribute to Hiroki-Kawakami/idf-components development by creating an account on GitHub.
ESP-IDF環境なら、idf_component.ymlに以下のように記述するとそのまま使えます。
dependencies:
flexispot:
git: https://github.com/Hiroki-Kawakami/idf-components.git
path: flexispot
version: main余談
今回僕の目的としてはM5Stack Tab5から操作することなので、とりあえずAtomS3-LiteをGATT ServerにしてBluetooth経由で操作しました。
FlexiSpot用のサービスにボタン用のWrite可能なCharacteristicと、ディスプレイ表示用のRead+Notify可能なCharacteristicを用意する形です。

