Boschの公式ライブラリをRaspberry Piで使うには

[Bosch](https://www.bosch-sensortec.com/)のセンサーをRaspberry Piで使いたいものの、最新のセンサーとなると、C言語で書かれたBoschの公式ライブラリしかなく、お困りの方へ。

最新のセンサーになると、データシートを見ると初期化や値の計算でやることが多い上に、詳細は公式ライブラリを見ろという記載も多く、1から実装となるとかなりの手間になります。

また、IMUに関しては単に加速度/ジャイロを読む以外に状態の推定等の機能が増えつつあるので、公式の実装例をすぐに使えるとかなり便利、というのもあります。

# TL;DR

Bosch公式ライブラリをRaspberry Piで呼び出すCとPython(Cython)ラッパーを作りました。

以下のセンサーで確認しました。また、I2C接続で動かすこと、Raspberry Pi OSのバージョンはBookwormを前提です。

- 気圧センサー BMP580, BMP581: [BMP5_SensorAPI](https://github.com/boschsensortec/BMP5_SensorAPI)
- IMU(加速度+ジャイロ) BMI270: [BMI270_SensorAPI](https://github.com/boschsensortec/BMI270_SensorAPI/)
- 地磁気センサー BMM150: [BMM150_SensorAPI](https://github.com/boschsensortec/BMM150_SensorAPI/)
- 地磁気センサー BMM350: [BMM150_SensorAPI](https://github.com/boschsensortec/BMM350_SensorAPI/)

# C言語のサンプルプログラム

各ライブラリのexamples/commonフォルダに実装のヒントがありました。センサーを表す構造体 `(センサー名)_dev ` にI2C通信の関数ポインタを定義して渡す等すれば、動くようになります。

以下にBMM350を例としたサンプルプログラムを示します。BMM350_SensorAPIと同じフォルダに配置してください。

```c:bmm350_sample.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>

#include "bmm350.h"

#define I2C_DEVICE "/dev/i2c-1"
#define BMM350_I2C_ADDR 0x14

int8_t i2c_read(uint8_t reg_addr, uint8_t *data, uint32_t len, void *intf_ptr) {
    int fd = *(int *)intf_ptr;
    if (write(fd, &reg_addr, 1) != 1) return -1;
    if (read(fd, data, len) != (ssize_t)len) return -1;
    return 0;
}

int8_t i2c_write(uint8_t reg_addr, const uint8_t *data, uint32_t len, void *intf_ptr) {
    int fd = *(int *)intf_ptr;
    uint8_t buffer[len + 1];
    buffer[0] = reg_addr;
    for (uint32_t i = 0; i < len; i++) {
        buffer[i + 1] = data[i];
    }
    if (write(fd, buffer, len + 1) != (ssize_t)(len + 1)) return -1;
    return 0;
}

void delay_us(uint32_t period, void *intf_ptr) {
    (void)intf_ptr;
    usleep(period);
}

int main() {
    struct bmm350_dev dev;
    struct bmm350_mag_temp_data mag_temp_data = { 0 };
    struct bmm350_pmu_cmd_status_0 pmu_cmd_stat_0 = { 0 };
    int8_t rslt;
    uint8_t err_reg_data = 0;
    int fd;

    fd = open(I2C_DEVICE, O_RDWR);
    if (fd < 0) {
        perror("Failed to open the i2c bus");
        return -1;
    }

    if (ioctl(fd, I2C_SLAVE, BMM350_I2C_ADDR) < 0) {
        perror("Failed to acquire bus access and/or talk to slave");
        close(fd);
        return -1;
    }

    dev.intf_ptr = &fd;
    dev.read = i2c_read;
    dev.write = i2c_write;
    dev.delay_us = delay_us;

    rslt = bmm350_init(&dev);
    if (rslt != BMM350_OK) {
        printf("BMM350 initialization failed\n");
        close(fd);
        return -1;
    }

    rslt = bmm350_get_pmu_cmd_status_0(&pmu_cmd_stat_0, &dev);
    if (rslt != BMM350_OK) {
        printf("BMM350 initialization failed\n");
        close(fd);
        return -1;
    }
    printf("Expected : 0x07 : PMU cmd busy : 0x0\n");
    printf("Read : 0x07 : PMU cmd busy : 0x%X\n", pmu_cmd_stat_0.pmu_cmd_busy);

    rslt = bmm350_get_regs(BMM350_REG_ERR_REG, &err_reg_data, 1, &dev);
    if (rslt != BMM350_OK) {
        printf("BMM350 initialization failed\n");
        close(fd);
        return -1;
    }
    printf("Expected : 0x02 : Error Register : 0x0\n");
    printf("Read : 0x02 : Error Register : 0x%X\n", err_reg_data);

    /* Set ODR and performance */
    rslt = bmm350_set_odr_performance(BMM350_DATA_RATE_25HZ, BMM350_AVERAGING_8, &dev);
    printf("bmm350_set_odr_performance [%d]\n", rslt);

    /* Enable all axis */
    rslt = bmm350_enable_axes(BMM350_X_EN, BMM350_Y_EN, BMM350_Z_EN, &dev);
    printf("bmm350_enable_axes [%d]\n", rslt);

    rslt = bmm350_set_powermode(BMM350_NORMAL_MODE, &dev);
    printf("bmm350_set_powermode [%d]\n", rslt);

    while (1) {
        rslt = bmm350_get_compensated_mag_xyz_temp_data(&mag_temp_data, &dev);
        printf("%.3f, %.3f, %.3f\n", mag_temp_data.x, mag_temp_data.y, mag_temp_data.z);
        sleep(1);
    }

    close(fd);
    return 0;
}
```

大事なのは、main関数の最初の方にある以下のコードです。

```
    dev.intf_ptr = &fd;
    dev.read = i2c_read;
    dev.write = i2c_write;
    dev.delay_us = delay_us;
```

`dev`(この例は`bmm350_dev`)に何を渡すかは若干増減しますが、`dev.intf_ptr` にファイルデスクリプタのアドレス、`dev.read`にI2Cで受信を行う関数、`dev.write`にI2Cで送信を行う関数、`dev.delay_us`にマイクロ秒単位でスリープする関数をそれぞれ渡します。

その前にある宣言や、後半のコードはexamplesのコードをそのまま使用しています。

コンパイルは以下で行います。
```
$ gcc bmm350_sample.c bmm350.c
```

# Pythonで使うには

公式ライブラリを共有ライブラリでビルドし、パスが通るところに配置し、Cythonでラッパークラスを作り呼び出します。


## 共有ライブラリのコンパイルとインストール

公式ライブラリの直下で以下のコマンドを実行します。gccでコンパイルする時に`-lbmm~`で関数を呼び出せるようになります。

### [BMP5_SensorAPI](https://github.com/boschsensortec/BMP5_SensorAPI) 
```
$ gcc -shared -fPIC -O2 -o libbmp5.so bmp5.c
$ sudo mv libbmp5.so /usr/local/lib/
$ sudo cp bmp5.h bmp5_defs.h /usr/local/include/
$ sudo ldconfig
```
### [BMI270_SensorAPI](https://github.com/boschsensortec/BMI270_SensorAPI/)
```
$ gcc -shared -fPIC -O2 -o libbmi270.so bmi270.c bmi2.c
$ sudo mv libbmi270.so /usr/local/lib/
$ sudo cp bmi2.h bmi270.h bmi2_defs.h /usr/local/include/
$ sudo ldconfig
```
### [BMM150_SensorAPI](https://github.com/boschsensortec/BMM150_SensorAPI/)
```
$ gcc -shared -fPIC -O2 -o libbmm150.so bmm150.c
$ sudo mv libbmm150.so /usr/local/lib/
$ sudo cp bmm150.h bmm150_defs.h /usr/local/include/
$ sudo ldconfig
```
### [BMM350_SensorAPI](https://github.com/boschsensortec/BMM350_SensorAPI/) 
```
$ gcc -shared -fPIC -O2 -o libbmm350.so bmm350.c
$ sudo mv libbmm350.so /usr/local/lib/
$ sudo cp bmm350.h bmm350_defs.h /usr/local/include/
$ sudo ldconfig
```


## Cythonでラッパークラスを作る

コード量が長くなるので、[拙作のレポジトリ](https://github.com/hishizuka/pizero_bikecomputer/tree/master/modules/sensor/i2c/cython)のコードを参照ください。

BMM350を例にして説明します。

### Cプログラム

先ほどの例(bmm350_sample.c)を分割し、マクロを用いていろいろな呼び出しに対応しました。
- [i2c_bmm350.h](https://github.com/hishizuka/pizero_bikecomputer/blob/master/modules/sensor/i2c/cython/i2c_bmm350.h)
- [i2c_bmm350.c](https://github.com/hishizuka/pizero_bikecomputer/blob/master/modules/sensor/i2c/cython/i2c_bmm350.c)
- [i2c_common.h](https://github.com/hishizuka/pizero_bikecomputer/blob/master/modules/sensor/i2c/cython/i2c_common.h)
- [i2c_common.c](https://github.com/hishizuka/pizero_bikecomputer/blob/master/modules/sensor/i2c/cython/i2c_common.c)

この状態で動くかは以下で確認を行います。
```
$ gcc i2c_bmm350.c i2c_common.c -lbmm350
```

### Cythonプログラム

[i2c_helper.pyx](https://github.com/hishizuka/pizero_bikecomputer/blob/master/modules/sensor/i2c/cython/i2c_helper.pyx)という名称のファイル内にBMM350を例に解説します。


以下の宣言で外部に公開しているBMM350の関数をcython内で定義します。

```
cdef extern from "i2c_bmm350.h":
  int i2c_bmm350_init()
  void i2c_bmm350_read_mag(float* mag)
  void i2c_bmm350_close()
```

Pythonから呼び出せるクラスは下記になります。

```
cdef class BMM350_C:
  cdef float mag[3]
  cdef public bint status

  def __cinit__(self, int bus):
    self.status = rslt_to_bool(i2c_bmm350_init())
    self.reset_value()
  
  def __dealloc__(self):
    i2c_bmm350_close()

  cdef reset_value(self):
    self.mag = [0.0, 0.0, 0.0]
  
  cpdef read_mag(self):
    if self.status:
      i2c_bmm350_read_mag(&self.mag[0])
  
  @property
  def magnetic(self):
    self.read_mag()
    return [self.mag[0], self.mag[1], self.mag[2]]
```

Pythonから`BMM350_C`をimportしてクラス定義すると、magneticという名称のプロパティを用いてアクセスできるようになります。この辺りはAdafruit circuitpythonライブラリと揃えました。


### CythonのビルドとPythonからの呼び出し

ラッパークラスとファイル名称を揃えた[i2c_helper.pyxbld](https://github.com/hishizuka/pizero_bikecomputer/blob/master/modules/sensor/i2c/cython/i2c_helper.pyxbld)を用意します。
詳細はコードを直接参照していただきたいですが、共有ライブラリの有無を判定して中身のあるクラスをコンパイルするか、空のクラスを作るかをしています。

Pythonからの呼び出しは至って単純です。下記のbuild.pyを同じディレクトリから実行します。

```python:buid.py
import time

import pyximport
pyximport.install(inplace=True)
from i2c_helper import BMM150_C, BMI270_C, BMM350_C, BMP5_C

bmm150 = BMM150_C(1)
bmm350 = BMM350_C(1)
bmi270 = BMI270_C(1)
bmp5   = BMP5_C(1)

print(f"{bmm150.status=}, {bmm350.status=}, {bmi270.status=}, {bmp5.status=}")

while True:
    try:
        print(f"BMM150 mag: {bmm150.magnetic}")
        print(f"BMM350 mag: {bmm350.magnetic}")
        print(f"BMI270 acc: {bmi270.acceleration}")
        print(f"BMI270 gyr: {bmi270.gyro}")
        print(f"BMP5      : {bmp5.pressure}, {bmp5.temperature}")
        print()
        time.sleep(1)
    except KeyboardInterrupt:
        print()
        break
```

3行目からの以下3行がCおよびCythonで書かれたラッパークラスの呼び出しになります。

```
import pyximport
pyximport.install(inplace=True)
from i2c_helper import BMM150_C, BMI270_C, BMM350_C, BMP5_C
```

初回実行時はコンパイルが走るので数十秒かかりますが、一度実行してしまえば次回以降の実行ではコンパイルが走らなくなります。逆に、CまたはCythonファイルを修正した場合は、作成された共有ライブラリ(i2c_helper.cpython〜.so)を削除する必要があります。