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:
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:
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.
The protocol to pair a Wiimote is fairly standard:
With a twist when we send the PIN reply message:
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.
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:
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:
And part two writes 0x00
to 0xA400FB
To confirm the extension controller is a Balance Board, we read memory from 0xA400FA
:
And once we've detected the byte string (0x0000A4200402
) we can read calibration data from 0xA40024
, 0xA40034
and 0xA40060
:
The above gives us this table of calibration data:
Sensor | 0kg | 17kg | 34kg |
---|---|---|---|
Top Right | 0x0458 | 0x0B15 | 0x11DA |
Bottom Right | 0x06B1 | 0x0D7D | 0x1454 |
Top Left | 0x4771 | 0x4E04 | 0x549A |
Bottom Left | 0x27B9 | 0x2E52 | 0x34F4 |
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):
which triggers a stream of measurement data:
...
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:
our load sensor and calibration values are:
Sensor | Weight | 0kg | 17kg | 34kg |
---|---|---|---|---|
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):
The total weight is the sum of all four loads, adjusted for temperature, using the equation:
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.