0
0

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

Last updated at Posted at 2023-12-06

はじめに

少し前に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]

このとき、tmp0b01010001という値のはずです。
左側を先頭ビットとして、この値の先頭ビットから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の生成

今までの内容で生成した変数を用いて、以下のようにしてSensorDatadata 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でビット単位のデータフォーマットをパースする際の参考になれば幸いです。

本記事のコード

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0