Home Assistant - Balance Board

In a github issue posted to my previous balance board project, someone asked if they could integrate it into Home Assistant. While I couldn't give a simple, and perhaps satisfactory answer, it did get me thinking about a different approach to accessing balance board data.

A big problem in my balance board tool, and similar projects, is that the Bluetooth setup can be finnicky, and that the board may not be within communication range of the Bluetooth receiver. This isn't a new problem of course, and in the Home Assistant sphere a fairly good solution is to use ESPHome, which can use Bluetooth+Wifi-enabled microcontrollers as Bluetooth Proxies.

For the balance board it isn't as simple as that though, first and foremost because Bluetooth proxies only support BLE devices and the balance board is a BR/EDR device. And secondly, Wiimotes and balance boards are peculiar Bluetooth devices. To get around this I wrote my first ESPHome external component, aptly and unoriginally named balance board.

To use the above you'll need three things:

  1. A Home Assistant setup.
  2. A Balance Board.
  3. An ESP32 device that supports Bluetooth Classic.

Point 3 is important, as many of the ESP32 devices only support Bluetooth BLE. In testing and development I've used an original ESP32-WROOM32 and a ESP32-PICO-D4 dongle, both which support Bluetooth Classic.

If all three points are in place, add the following config to your ESPHome yaml, in addition to the standard setup for wifi/board/...:

external_components:
  - source:
      type: git
      url: https://github.com/gulrotkake/esphome
      ref: balance-board
    components: [ wii_balance_board ]

wii_balance_board:
    id: board
    standard_deviation: 0.3

button:
  - platform: template
    name: "Start Sync"
    on_press:
      then:
        - lambda: 'id(board)->sync(true);'

and upload to your ESP32 device:

$ esphome run config/wii_balance_board.yaml

After this, if all went well, the device should now be available in Home Assistant. To sync/pair a balance board, go to the device page in Home Assistant and find the Start Sync control:

Press it and the red sync button on the Balance Board within a few seconds of each other. If the pairing was successful, the LED on Balance Board should light up and you can take your first measurement. The Balance Board will disconnect once it has a stable measurement, or after a 60 second timeout. Subsequent reconnects do not need to use the red-sync button. Pressing the A-button on the board should be sufficient:

Technicals

Wii on the ESP32

Much of Wiimote bluetooth communication was based on code from https://github.com/takeru/Wiimote. Though to avoid the dreaded red-sync button connection flow, I needed to extend it fairly significantly and implement support for pairing and receiving connections.

Pairing + Connecting

The protocol to pair a Wiimote is fairly standard:

tx
011104028100 # Transmit HCI Authentication request
rx
041706B400D81D1900 # Receive Link key request
tx
010C0406B400D81D1900 # Transmit Negative link reply
rx
041606B400D81D1900 # Receive PIN request

With a twist when we send the PIN reply message:

tx
010D0417B400D81D190006E2E62D0B65F400000000000000000000

If the transmitted PIN code is the ESP32's Bluetooth MAC address in reverse, the Wiimote will save the address and in the future attempt to reconnect to the ESP host when any button on the Wiimote is pressed.

uint8_t pin_data[6];
// The pin is the mac of the host controller reversed
auto mac = bt->macAddress();
for (size_t i = 0; i < 6; ++i) {
  pin_data[i] = mac[5 - i];
}

log_i("Sending pin reply");
bt->sendPinReply(result.bdaddr, pin_data, 6);

Additionally, when the Wiimote establishes the HCI connection, it also establishes the L2CAP channels for control and data. So to support (re-)connecting a paired device, the ESP must implement support for L2CAP connection requests and avoid sending them.

Balance Board Measurement

The balance board consists internally of four load sensors:

+---------------+---------------+
|               |               |
|       tl      |       tr      |
|               |               |
+---------------|---------------|
|               |               |
|       bl      |       br      |
|               |               |
+---------------+---------------+

that are calibrated for three different loads and a stored reference temperature: 0kg, 17kg and 34kg. To read the calibration data, we follow the extension controller protocol after receiving a status report from Wiimote indicating an extension controller is attached:

rx
0281200C0008004100A1200000020000BE

We start with a command to set control register memory (0x04) at the address 0xA400F0 to a magic byte 0x55. This is part one of a small two step dance to disable extension controller encryption:

tx
0281201B0017006B00A21604A400F00155000000000000000000000000000000
rx
0281200A0006004100A12200001600

And part two writes 0x00 to 0xA400FB

tx
0281201B0017006B00A21604A400FB0100000000000000000000000000000000
rx
0281200A0006004100A12200001600

To confirm the extension controller is a Balance Board, we read memory from 0xA400FA:

tx
0281200C0008006B00A21704A400FA0006
rx
0281201B0017004100A12100005000FA0000A420040200000000000000000000

And once we've detected the byte string (0x0000A4200402) we can read calibration data from 0xA40024, 0xA40034 and 0xA40060:

tx
0281200C0008006B00A21704A400240010
rx
0281201B0017004100A1210000F00024045806B1477127B90B150D7D4E042E52
tx
0281200C0008006B00A21704A400340008
rx
0281201B0017004100A121000070003411DA1454549A34F40000000000000000
tx
0281200C0008006B00A21704A400600002
rx
0281201B0017004100A121000010006012010000000000000000000000000000

The above gives us this table of calibration data:

Sensor0kg17kg34kg
Top Right0x04580x0B150x11DA
Bottom Right0x06B10x0D7D0x1454
Top Left0x47710x4E040x549A
Bottom Left0x27B90x2E520x34F4

and a reference temperature of 0x12. Finally, we instruct the Balance Board to send data change reports of type 0x34 (Core Buttons with 19 Extension bytes):

tx
028120080004006B00A2120034

which triggers a stream of measurement data:

rx
0281201B0017004100A13400000EF70D1650232CC21C00870000000000000000
rx
0281201B0017004100A13400000F020D184FF22CB91C00870000000000000000
rx
0281201B0017004100A13400000F0A0CFF4FDF2CB01C00870000000000000000
rx
0281201B0017004100A13400000F1F0CE04FD52CA21C00870000000000000000
rx
0281201B0017004100A13400000F300CCB4FE62C9C1C00870000000000000000
rx
0281201B0017004100A13400000F3A0CBE4FF92C9E1C00870000000000000000
rx
0281201B0017004100A13400000F400CAC50162C9F1C00870000000000000000
rx
0281201B0017004100A13400000F4B0C9650232C961C00870000000000000000
rx
0281201B0017004100A13400000F4D0C8A502E2C9C1C00870000000000000000
rx
0281201B0017004100A13400000F570C8A50382CAE1C00870000000000000000
rx
0281201B0017004100A13400000F540C7B50452CB71C00870000000000000000
rx
0281201B0017004100A13400000F4D0C7950492CD01C00870000000000000000
...

The data format and how to interpret it is documented thoroughly on Wiibrew, but in short the load on each sensor is interpolated between the two calibration loads for that sensor. So, given a line such as:

rx
0281201B0017004100A13400000CEB0DAA4F612DCC1C00870000000000000000

our load sensor and calibration values are:

SensorWeight0kg17kg34kg
Top Right 0x0CEB 0x0458 0x0B15 0x11DA
Bottom Right 0x0DAA 0x06B1 0x0D7D 0x1454
Top Left 0x4F61 0x4771 0x4E04 0x549A
Bottom Left 0x2DCC 0x27B9 0x2E52 0x34F4

which gives us the following weights for each sensor (in kilograms):

f 17 ( w , s ) = 17 w - c 0 [ s ] c 0 [ s ] - c 17 [ s ]
f 34 ( w , s ) = 17 + 17 w - c 17 [ s ] c 17 [ s ] - c 34 [ s ]
f 34 ( 0 x 0 c e b , t r ) = 17 + 17 0 x 0 c e b - 0 x 0 b 15 0 x 11 da - 0 x 0 b 15 = 21.6105
f 34 ( 0 x 0 da a , b r ) = 17 + 17 0 x 0 da a - 0 x 0 d 7 d 0 x 1454 - 0 x 0 d 7 d = 17.4369
f 34 ( 0 x 4 f 61 , t l ) = 17 + 17 0 x 4 f 61 - 0 x 4e04 0 x 549 a - 0 x 4e04 = 20.519
f 17 ( 0 x 2 dc c , b l ) = 17 0 x 2 dc c - 0 x 27 b 9 0 x 2e52 - 0 x 27 b 9 = 15.6513

The total weight is the sum of all four loads, adjusted for temperature, using the equation:

w = .999 ( w ) ( 1 - 0.0007 ( t b o a r d - t r e f ) )
w = .999 75.2177 ( 1 - 0.0007 ( 0 x 1 c - 0 x 12 ) ) = 74.62

To get a stable measurement, the ESPHome component continuously fills a ring buffer with measurement samples until the standard deviation of the measured samples drops below a configurable level (default is 0.4), and then it returns the mean:

float variance = std::accumulate(samples, samples + size, 0.0,
    [mean, size](float acc, float val) {
        return acc + ((val - mean) * (val - mean) / (size - 1));
    });

float deviation = std::sqrt(variance);

if (mean > 10 && deviation < std_dev_) {  // Ignore means below 10kg.
    sample.measurement = mean;
    // ...
}

The code for the aforementioned bluetooth communication, and interpreting the Wii Balance Board data, are bundled as part of this component and can be seen here.

Useful links