Cross-project findings: ES-CS20M hardware variants
I maintain ble-scale-sync (Node.js/TypeScript alternative for BLE scales) and we've been debugging the same ES-CS20M problems in our issue #34. Sharing our findings here since they're relevant to openScale users.
The ES-CS20M ships in at least 3 hardware variants
| Variant |
Advertised name |
Protocol |
Services |
GATT connectable |
| 1 |
"QN-Scale" |
QN/Qingniu |
0xFFE0 / 0xFFF0 |
Yes |
| 2 |
"ES-CS20M" or unnamed |
Yunmai |
0x1A10 |
Yes |
| 3 |
"Renpho Scale" or unnamed |
QN/Qingniu (broadcast) |
none |
No (ADV_NONCONN_IND) |
Variant 1 works with the QN handler. Variant 2 was fixed by PR #1300. Variant 3 is the problematic one.
Variant 3: broadcast-only (non-connectable)
Some ES-CS20M units advertise as non-connectable (ADV_NONCONN_IND). No BLE stack on any OS can establish a GATT connection to these devices. This explains the "unable to connect" reports from users where the scale is visible but connection always fails, on both Windows and Android.
These devices use QN/Qingniu manufacturer data advertisements with the AABB marker:
FF FF AA BB [MAC 6 bytes] [data bytes...] [stability byte near end]
Impedance is not available in broadcast mode (confirmed independently by two users).
Weight byte position is still unconfirmed. Our initial assumption (bytes [10-11] as big-endian uint16 / 100) produced incorrect readings: 135 kg for a user who actually weighs ~70 kg. We are currently collecting controlled data samples (known weights + raw hex) from a user with this variant to determine the correct byte positions and divisor. Will update once the format is confirmed.
Two raw samples captured so far:
Idle (stable): FF FF AA BB ED 67 39 DC 60 60 08 D6 00 00 FF FF FF 02 00 18 1A 4C 09 03 FC 01
With weight (meas): FF FF AA BB ED 67 39 DC 60 60 13 00 00 00 FF FF FF 02 00 55 05 4C 09 03 27 00
Bytes that change between samples: [10-11], [19-20], [24], [25]. The last byte appears to be a stability flag (0x01 = stable, 0x00 = measuring).
How to identify the variant
The easiest way is to check the advertisement type in nRF Connect:
Connectable + service UUID 0xFFE0/0xFFF0 = Variant 1 (QN handler)
Connectable + service UUID 0x1A10 = Variant 2 (ES-CS20M handler)
Non-connectable + manufacturer data starting with AABB = Variant 3 (needs broadcast parser)
Implementation in ble-scale-sync
Added broadcast mode support that reads weight from advertisement data without requiring a GATT connection. Body composition is estimated via the Deurenberg BMI formula since impedance is unavailable. The broadcast parser is functional but the weight byte layout needs correction (see above). This also includes a BLE diagnostic tool that detects broadcast-only devices and shows parsed weight from advertisements.
Possibly related: #1302 (unhandled opcode 0xa1)
Issue #1302 shows a connectable QN variant ("Renpho-Scale", manufacturer: "Qing Niu Technology") that completes the full handshake (0x12 → 0x13 → 0x14 → 0x20 → 0x21 → 0xA0) but the 0xA1 response is unhandled. The 0xA1 frame is likely the ACK to the user profile command (0xA0). After this, no weight data arrives. This might be a separate issue with the QN handler not completing the handshake for certain firmware versions.
Cross-project findings: ES-CS20M hardware variants
I maintain ble-scale-sync (Node.js/TypeScript alternative for BLE scales) and we've been debugging the same ES-CS20M problems in our issue #34. Sharing our findings here since they're relevant to openScale users.
The ES-CS20M ships in at least 3 hardware variants
Variant 1 works with the QN handler. Variant 2 was fixed by PR #1300. Variant 3 is the problematic one.
Variant 3: broadcast-only (non-connectable)
Some ES-CS20M units advertise as non-connectable (
ADV_NONCONN_IND). No BLE stack on any OS can establish a GATT connection to these devices. This explains the "unable to connect" reports from users where the scale is visible but connection always fails, on both Windows and Android.These devices use QN/Qingniu manufacturer data advertisements with the
AABBmarker:Impedance is not available in broadcast mode (confirmed independently by two users).
Weight byte position is still unconfirmed. Our initial assumption (bytes [10-11] as big-endian uint16 / 100) produced incorrect readings: 135 kg for a user who actually weighs ~70 kg. We are currently collecting controlled data samples (known weights + raw hex) from a user with this variant to determine the correct byte positions and divisor. Will update once the format is confirmed.
Two raw samples captured so far:
Bytes that change between samples: [10-11], [19-20], [24], [25]. The last byte appears to be a stability flag (0x01 = stable, 0x00 = measuring).
How to identify the variant
The easiest way is to check the advertisement type in nRF Connect:
Connectable+ service UUID 0xFFE0/0xFFF0 = Variant 1 (QN handler)Connectable+ service UUID 0x1A10 = Variant 2 (ES-CS20M handler)Non-connectable+ manufacturer data starting withAABB= Variant 3 (needs broadcast parser)Implementation in ble-scale-sync
Added broadcast mode support that reads weight from advertisement data without requiring a GATT connection. Body composition is estimated via the Deurenberg BMI formula since impedance is unavailable. The broadcast parser is functional but the weight byte layout needs correction (see above). This also includes a BLE diagnostic tool that detects broadcast-only devices and shows parsed weight from advertisements.
Possibly related: #1302 (unhandled opcode 0xa1)
Issue #1302 shows a connectable QN variant ("Renpho-Scale", manufacturer: "Qing Niu Technology") that completes the full handshake (0x12 → 0x13 → 0x14 → 0x20 → 0x21 → 0xA0) but the 0xA1 response is unhandled. The 0xA1 frame is likely the ACK to the user profile command (0xA0). After this, no weight data arrives. This might be a separate issue with the QN handler not completing the handshake for certain firmware versions.