Pairlink BLE протокол Ergostol
Протокол управления стоячим столом Ergostol через BLE. Reverse-engineered из Android APK com.pairlink.ergostol v1.2.9.7 (jadx decompilation, март 2026).
Это не стандартный JIECANG протокол (который использует FFE1/FFE2 и фреймы F1 F1). Ergostol использует кастомный протокол Pairlink (normalLib SDK, тип LIB_DIRECT_DING).
BLE Connection
| Параметр | Значение |
|---|---|
| Device name | StruchkovErgostol (настраивается через FF06) |
| Device UUID | 47E28F21-C8A7-BDAA-0563-85A64B7CF23B |
| Service UUID | FF12 |
| Write characteristic | FF01 (Write, WriteWithoutResponse) |
| Notify characteristic | FF02 (Notify) |
| Name characteristic | FF06 (Read/Write, ASCII) |
Полные UUID характеристик:
- Write:
0000ff01-0000-1000-8000-00805f9b34fb - Notify:
0000ff02-0000-1000-8000-00805f9b34fb
Дополнительные сервисы (не для управления)
| Service | Characteristics | Назначение |
|---|---|---|
00006287-3C17-D293-8E48-14FE2E4DA212 | 6387 (Wn), 6487 (W,N) | OTA (предположительно) |
0000D0FF-3C17-D293-8E48-14FE2E4DA212 | FFD1, EFD2, FFD3, FFD4, FFD5, FFD8, FFF1, FFF2, FFE0 | Device info/config |
Static reads при подключении
| Characteristic | Значение | Описание |
|---|---|---|
| FF06 | 53 74 72 75 63 68 6B 6F 76 45 72 67 6F 73 74 6F 6C | ”StruchkovErgostol” |
| EFD2 | EC C5 7F 63 66 E1 | MAC address (reversed) |
| FFD3 | 01 00 24 00 | Unknown config |
| FFD4 | 04 08 01 02 01 | Unknown config |
| FFF1 | 05 01 00 04 00 08 00 00 00 0F 00 00 | Device capabilities |
| FFE0 | 01 00 24 00 01 40 00 00 | Unknown config |
Формат пакета
Все пакеты — 6 байт:
[cmd] [param] [data_hi] [data_lo] [crc_lo] [crc_hi]
CRC16
Отправка (app → стол): CRC по [0x04, 0xFC, 0x42, 0x06, cmd, param, data_hi, data_lo]
Приём (стол → app): CRC по [0x01, 0xFC, 0x41, 0x06, cmd, param, data_hi, data_lo, crc_lo, crc_hi] = 0
Lookup table:
[0, 52225, 55297, 5120, 61441, 15360, 10240, 58369, 40961, 27648, 30720, 46081, 20480, 39937, 34817, 17408]
Алгоритм:
crc = 0xFFFF
for byte in data:
nibble_lo = (crc & 0x0F) ^ (byte & 0x0F) & 0x7F
crc = ((crc >> 4) ^ table[nibble_lo]) & 0xFFFF
nibble_hi = (crc & 0x0F) ^ ((byte >> 4) & 0x0F) & 0x7F
crc = ((crc >> 4) ^ table[nibble_hi]) & 0xFFFF
Источник: com.pairlink.ergostol.utils.c, метод a(byte[], int), строка 59-68.
Команды (отправка)
| cmd | param | Описание | Источник |
|---|---|---|---|
| 1 | 1 | Движение ВНИЗ | ControlFragment.onTouch |
| 2 | 1 | Движение ВВЕРХ | ControlFragment.onTouch |
| 3 | 1 | Пресет STAND | ControlFragment.onClick |
| 4 | 1 | Пресет MIDDLE | ControlFragment.onClick |
| 5 | 1 | Пресет SIT | ControlFragment.onClick |
| 6 | 1/2/3 | Сохранить позицию (1=sit, 2=stand, 3=middle) | CustomSetupFrg |
| 6 | 4 | Переключить cm/inch | |
| 7 | 1-11 | Запрос init данных (последовательно) | Init handshake |
| 8 | 1 | Запрос текущей высоты | Heartbeat/poll |
| 9 | 1 | СТОП | ControlFragment.stop_run |
| 10 | varies | Настройка чувствительности | |
| 11 | varies | Режим handset | |
| 12 | varies | Запрос reminder mode |
Special case: cmd=1 (DOWN) при ошибке
Когда код ошибки 18 (E02) или 258 (E102), data_lo устанавливается в 1 вместо 0. Это разрешает движение только вниз при ошибке anti-collision.
Ответы (приём)
cmd=7: Init данные
param определяет тип данных:
| param | data_hi:data_lo | Значение |
|---|---|---|
| 1 | hall value | Позиция Stand |
| 2 | hall value | Позиция Middle |
| 3 | hall value | Позиция Sit |
| 4 | 0:value | Настройка cm/inch (1=cm, 2=inch) |
| 5 | hall value | Base height (desk_base_h) |
| 6 | hall value | Min height (desk_min_h) |
| 7 | hall value | Max height (desk_max_h) |
| 8 | hall value | Текущая высота (desk_cur_h) |
| 9 | 0:version | MCU версия → определяет делитель (g.u) |
| 10 | 0:state | Состояние handset |
| 11 | 0:state | Handset running |
cmd=8: Обновление высоты
| param | Значение |
|---|---|
| 1 | Движение (высота в data) |
| 2 | Отскок |
| 3 | Движение завершено |
| 5 | Отскок завершён |
| 6 | Позиция сохранена |
| 7 | Reboot reset complete |
cmd=9: СТОП ACK
param=1, финальная высота в data.
cmd=11: Handset уведомление
Физические кнопки стола активны. Пример пакета:
0B 01 00 00 69 3D = cmd=11, param=1, data=0x0000, crc=0x3D69
Ошибка: param=0x80
Код ошибки в (data_hi << 8) | data_lo.
Кодирование высоты
hall_value = (data_hi << 8) | data_lo // big-endian uint16
height_cm = hall_value / divisor + base_height_cm
Делитель зависит от MCU версии (определяется при init, param=9):
| MCU ver | Делитель | base_min | base_max | range |
|---|---|---|---|---|
| 1 | 29.333334 | 1760 | 2200 | 1408 |
| 2 | 29.333334 | 1760 | 2200 | 1320 |
| 3 | 11.0 | 660 | 825 | 715 |
| 4 | 44.0 | 2640 | 3300 | 2860 |
| 5 | 26.0 | 1560 | 1950 | 1170 |
| 6 | 58.666668 | 3520 | 4400 | 2698 |
| 7 | 29.8 | 1788 | 2235 | 1430 |
| 8 | 26.0 | 1560 | 1950 | 1248 |
| 9 | 27.5 | 1650 | 2062 | 1787 |
| 10 | 44.0 | 2640 | 3300 | 2112 |
| 11 | 22.0 | 1320 | 1650 | 1430 |
Источник: com.pairlink.ergostol.utils.c, метод b(int), строка 319-433.
Init последовательность
Обязательна перед отправкой команд:
- Подключиться к BLE, подписаться на FF02
- Отправить
cmd=7, param=1 - Стол отвечает — отправить
cmd=7, param=2 - Продолжать до param=11 (каждый ответ триггерит следующий запрос: param=N+1)
- После param=9 (MCU version) устанавливается делитель (g.u)
- После param=11 → отправить
cmd=7, param=32 (0x20)(init complete) - Отправить
cmd=8для текущей высоты
Поток движения
- Отправить cmd=1 (вниз) или cmd=2 (вверх)
- Стол шлёт cmd=8 с текущей высотой
- Отправить cmd=9 для остановки
- Стол отвечает cmd=9 с финальной высотой
Пресеты: cmd=3/4/5 → стол движется → cmd=8 (param=3) когда достиг позиции.
Gotchas (реальное поведение)
Обнаружено при работе с реальным столом (MCU v4):
- param=32 echo: Стол отвечает на
cmd=7 param=32(init complete signal) своимcmd=7 param=32. Обработчик init должен игнорировать params вне 1-11, иначе бесконечный цикл. - Нет height notifications при ручном движении: При cmd=1/2 (вверх/вниз) стол НЕ шлёт cmd=8 param=1. Необходим polling — отправлять cmd=8 каждые 100-200мс.
- Height query response: На cmd=8 param=1 (query) стол отвечает cmd=8 param=6 (не param=1). Param=6 содержит актуальный hall value.
- Command echo: Стол эхом отвечает на движение (cmd=1 → RX cmd=1, cmd=2 → RX cmd=2). Эти ответы не содержат полезных данных.
- Стабилизация: Определять окончание движения по стабилизации hall value (5+ одинаковых показаний подряд), а не по param=3 (movement finished), который не всегда приходит.
Источники
- APK:
com.pairlink.ergostolv1.2.9.7, декомпилирован jadx
Ключевые классы
| Файл | Назначение |
|---|---|
com.pairlink.ergostol.utils.c | Формирование пакетов, CRC, логика отправки |
com.pairlink.ergostol.activity.MainActivity | processPDU() — парсинг ответов, on_recv_dat() |
com.pairlink.ergostol.fragment.ControlFragment | UI кнопки → команды |
com.pairlink.normalLib.BLEDeviceManager | BLE соединение, discovery характеристик, write queue |
com.pairlink.normalLib.BLEUtil | Hex конвертация, byte утилиты |
com.pairlink.ergostol.utils.g | Глобальные флаги состояния |