はじめに
少し前に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 |
+-+-+-+-+-+-+-+-+
細かい制約
パラメータ名 | データサイズ | 取りうる値 |
---|---|---|
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 |
今回は、例として以下のパラメータの値を受け取ることとします。
SequenceNo: 0
temperature: -1
humidity: 40
pressure: 1080
errCode: 0
これをKotlinのByteArray
型で表現したときは、以下のようになることとします。
val receivedSensorData = byteArrayOf(
0b00000000,
0b11111111.toByte(),
0b01010001,
0b00001110.toByte(),
0b00000000
)
この受信したデータをパースして、以下のKotlinのdata class
を生成することをゴールとします。
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
型にキャストしています。
val sequenceNo = receivedSensorData[0].toUByte()
以下のように出力することで想定していた出力が得られているはずです。
println("seqNo: $sequenceNo")
// Output: seqNo: 0
temperature
temperatureも、sequenceNoと同様1 byteなので、以下のようにパースできます。
val temperature = receivedSensorData[1]
以下のように出力することで想定していた出力が得られているはずです。
println("temperature: $temperature")
// Output: temperature: -1
humidity
humidityは、データサイズが7 bitっと少しきりの悪いデータサイズとなっているため、今までとは違い必要なデータサイズ分、切り出す必要があります。
まず、考えを簡単にするために以下のようにして、必要なデータだけを取得します。
val tmp = receivedSensorData[2]
このとき、tmp
は0b01010001
という値のはずです。
左側を先頭ビットとして、この値の先頭ビットから7 bit分取り出せれば、humidityの値になるはずです。
ですので、以下のようにしてデータを取り出します。
// import kotlin.experimental.and が必須
val humidity = ((tmp.toInt() ushr 1).toByte() and 0b01111111.toByte()).toUByte()
何をしているかを解説すると、まずビット演算の右シフトを用いて必要な分のデータ量にし、その上で必要なデータ領域分をAND演算を用いてマスキングしています。
以下のように出力することで想定していた出力が得られているはずです。
println("humidity: $humidity")
// Output: humidity: 40
pressure
pressureは、データサイズが11 bitです。
必要なbyteの領域としては、先ほどのtmp
変数の他に、以下が必要となります。
val tmp2 = receivedSensorData[3]
val tmp3 = receivedSensorData[4]
これらから、以下のようにして必要なビットを取り出し、pressureのデータをパースします。
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
型に変換しています。
以下のように出力することで想定していた出力が得られているはずです。
println("pressure: $pressure")
// Output: pressure: 1080
errCode
errCodeは、tmp3
変数から以下のようにして取り出します。
val errCode = (tmp3 and 0b00111111.toByte()).toUByte()
以下のように出力することで想定していた出力が得られているはずです。
println("errCode: $errCode")
// Output: errCode: 0
data classの生成
今までの内容で生成した変数を用いて、以下のようにしてSensorData
のdata class
を生成できます。
val buildSensorData = SensorData(
sequenceNo=sequenceNo,
temperature=temperature,
humidity=humidity,
pressure=pressure,
errCode=errCode
)
実際に、以下のように出力することで想定していた出力が得られているはずです。
println(buildSensorData)
// Output: SensorData(sequenceNo=0, temperature=-1, humidity=40, pressure=1080, errCode=0)
まとめ
この記事では、Kotlinでビット単位のデータフォーマットをパースする方法について解説しました。
Kotlinでビット単位の処理を行う際には、ビット演算を多用する必要がありますが、場合によってInt型に変換する必要があるため、変換した後に不要な値が含まれないように気を付ける必要がありそうです。
この記事が、Kotlinでビット単位のデータフォーマットをパースする際の参考になれば幸いです。