Kotlinでビット単位のデータフォーマットをパースする

# はじめに
少し前にKotlinで、ビット単位でデータフォーマットが決まっているデータをBLE経由でAndroidが受信し、フォーマットに従ってKotlinの`data class`で扱えるようにする実装をしました。
その時にビット単位での処理を実装した際に、細々としたことで苦労した記憶があったので、備忘録として書き出そうと思って、この記事を書きました。
何かの参考になれば幸いです。
また、記載しているコードの処理の中でより良くするアイデアがあれば、ご教授いただければと思います！

# シナリオ
今回、以下のように環境センサーから定期的にセンサー値が通知されるものとします。

```
[Android] <- SensorData - [環境センサー]
```

Android側で以下のデータフォーマットを`ByteArray`で受信するとします。

```
 0                   1                   2                   3   
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Sequence No  |  temperature  |   humidity  |     pressure 
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |  errCode  |
+-+-+-+-+-+-+-+-+
```
<details><summary>細かい制約</summary>

|パラメータ名|データサイズ|取りうる値|
|---|---|---|
|SequenceNo|1 byte|0-255|
|temperature|1 byte|-128-127|
|humidity|7 bit|0-100|
|pressure|11 bit|0-2047|
|errCode|6 bit|0-63|
</details>

今回は、例として以下のパラメータの値を受け取ることとします。
```
SequenceNo: 0
temperature: -1
humidity: 40
pressure: 1080
errCode: 0
```

これをKotlinの`ByteArray`型で表現したときは、以下のようになることとします。
```kotlin
val receivedSensorData = byteArrayOf(
    0b00000000,
    0b11111111.toByte(),
    0b01010001,
    0b00001110.toByte(),
    0b00000000
)
```

この受信したデータをパースして、以下のKotlinの`data class`を生成することをゴールとします。

```kotlin
data class SensorData (
  val sequenceNo: UByte,
  val temperature: Byte,
  val humidity: UByte,
  val pressure: UShort,
  val errCode: UByte
)
```

# データのパース
各項目毎にデータのパースをしていきます。

## sequenceNo
sequenceNoは、データサイズが1 byteなので、`ByteArray`型という利点をいかし、以下のように、0番目の配列の要素を取得するようにして、パースできます。
またここでは、最終的に`UByte`型として扱いたいので、`UByte`型にキャストしています。
```kotlin
val sequenceNo = receivedSensorData[0].toUByte()
```

以下のように出力することで想定していた出力が得られているはずです。
```kotlin
println("seqNo: $sequenceNo")
// Output: seqNo: 0
```

## temperature
temperatureも、sequenceNoと同様1 byteなので、以下のようにパースできます。
```kotlin
val temperature = receivedSensorData[1]
```

以下のように出力することで想定していた出力が得られているはずです。
```kotlin
println("temperature: $temperature")
// Output: temperature: -1
```

## humidity
humidityは、データサイズが7 bitっと少しきりの悪いデータサイズとなっているため、今までとは違い必要なデータサイズ分、切り出す必要があります。
まず、考えを簡単にするために以下のようにして、必要なデータだけを取得します。
```kotlin
val tmp = receivedSensorData[2]
```

このとき、`tmp`は`0b01010001`という値のはずです。
左側を先頭ビットとして、この値の先頭ビットから7 bit分取り出せれば、humidityの値になるはずです。
ですので、以下のようにしてデータを取り出します。
```kotlin
// import kotlin.experimental.and が必須
val humidity = ((tmp.toInt() ushr 1).toByte() and 0b01111111.toByte()).toUByte()
```

何をしているかを解説すると、まずビット演算の右シフトを用いて必要な分のデータ量にし、その上で必要なデータ領域分をAND演算を用いてマスキングしています。

以下のように出力することで想定していた出力が得られているはずです。
```kotlin
println("humidity: $humidity")
// Output: humidity: 40
```

## pressure
pressureは、データサイズが11 bitです。
必要なbyteの領域としては、先ほどの`tmp`変数の他に、以下が必要となります。
```kotlin
val tmp2 = receivedSensorData[3]
val tmp3 = receivedSensorData[4]
```

これらから、以下のようにして必要なビットを取り出し、pressureのデータをパースします。
```kotlin
val msb = (tmp and 1.toByte()).toInt() shl 10
val tmp4 = (tmp3.toInt() ushr 6).toByte() and 0b00000011.toByte()
val pressure = ((msb or (tmp2.toInt() shl 2)) or tmp4.toInt()).toUShort()
```

`msb`変数のところでは、最終的に`pressure`変数の最上位ビットをAND演算を用いて取得し、最上位ビットの位置となるように左シフト演算で調整しています。
`tmp4`変数のところでは、`humidity`変数のところでやっていた方法と同様にして、`tmp3`から必要なデータ領域分を取り出しています。
`pressure`変数のところでは、`tmp2`変数のビットの位置を左に2つずらす必要があるため、左シフト演算で調整した後、`msb`変数や`tmp4`変数のビットをOR演算を用いて加え、`UShort`型に変換しています。

以下のように出力することで想定していた出力が得られているはずです。
```kotlin
println("pressure: $pressure")
// Output: pressure: 1080
```

## errCode
errCodeは、`tmp3`変数から以下のようにして取り出します。
```kotlin
val errCode = (tmp3 and 0b00111111.toByte()).toUByte()
```

以下のように出力することで想定していた出力が得られているはずです。
```kotlin
println("errCode: $errCode")
// Output: errCode: 0
```

# data classの生成
今までの内容で生成した変数を用いて、以下のようにして`SensorData`の`data class`を生成できます。
```kotlin
val buildSensorData = SensorData(
    sequenceNo=sequenceNo,
    temperature=temperature,
    humidity=humidity,
    pressure=pressure,
    errCode=errCode
)
```

実際に、以下のように出力することで想定していた出力が得られているはずです。
```kotlin
println(buildSensorData)
// Output: SensorData(sequenceNo=0, temperature=-1, humidity=40, pressure=1080, errCode=0)
```

# まとめ
この記事では、Kotlinでビット単位のデータフォーマットをパースする方法について解説しました。
Kotlinでビット単位の処理を行う際には、ビット演算を多用する必要がありますが、場合によってInt型に変換する必要があるため、変換した後に不要な値が含まれないように気を付ける必要がありそうです。
この記事が、Kotlinでビット単位のデータフォーマットをパースする際の参考になれば幸いです。

# 本記事のコード
- https://github.com/5altNaCl/Parsing-Bitwise-Data-Formats-in-Kotlin/blob/main/ParsingBitwiseDataFormats.kt