SANDFISH FACTORY

技術ブログです。python・vuejsを愛でる日々について綴ります

toioをpythonで動かす:バイナリデータ操作

はじめに

前回から「toioをpythonで動かす」を始めました。
toio.jsのソースコードを見ると、BLE操作を行うため、バイナリデータの読み書きを行っています。
今回はjavascriptで書かれているコードをpythonで実現したいと思います。

pythonでバイナリデータを扱う

pythonでバイナリデータを扱う場合はbytesかbytearrayを用います。

組み込み型 — Python 3.8.3 ドキュメント

bytes オブジェクトは不変なオブジェクトで、bytearray オブジェクトは可変なオブジェクトです。
不変と可変を理解するために下のコードを試してみました。

>>> from struct import pack_into
>>> 
>>> data = bytes(b'\x01\x02')
>>> data
b'\x01\x02'
>>> pack_into("B", data, 1, 5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: argument must be read-write bytes-like object, not bytes
>>> 
>>> data = bytearray(b'\x01\02')
>>> data
bytearray(b'\x01\x02')
>>> 
>>> pack_into("B", data, 1, 5)
>>> data
bytearray(b'\x01\x05')

pythonでバイナリデータを操作する場合はstructを用います。

struct --- バイト列をパックされたバイナリデータとして解釈する — Python 3.8.3 ドキュメント

bytesオブジェクトをstruct.pack_intoを用いて書き換えようとするとTypeErrorが発生します。
bytearrayオブジェクトの場合は書き換えが可能で、bytearray(b'\x01\x02')がbytearray(b'\x01\x05')に書き換わっています。
このことからbytesが不変なオブジェクトで、bytearrayが可変なオブジェクトであることがわかると思います。

バイナリデータを読み込む

一例ですが、toio.jsを見てみるとこのような形で実装されています。

const type = buffer.readUInt8(0)
const standardId  = buffer.readUInt32LE(1)
const angle = buffer.readUInt16LE(5)

readUInt8メソッドは、bufferに対して指定されたオフセットの位置から符号なし8ビット整数を読み取っています。

pythonでバイナリデータを読み込む場合は以下のメソッドで対応できます。

  • unpack(format, buffer)
  • unpack_from(format, buffer, offset=0)

formatは書式指定文字を指定します。

8bit符号なし整数はunsigned charの整数なので"B"を指定します。
上記の例ではoffsetを指定しているのでunpack_fromを用います。

from struct import unpack_from
data = bytes(b'\x01\x02\x03\x04\x05\x06\x07')
type_data = unpack_from("B", data, 0)[0]

2バイト、4バイトの読み込みは書式指定文字を変更することで行います。
また、javascriptでのソースコードはリトルエンディアンで読み込みを行っているので、バイトオーダーを指定する文字(<)を指定します。

standardId = unpack_from("<I", data, 1)[0]
angle = unpack_from("<H", data, 5)[0]

バイナリデータを書き込む

書き込みも同じような形で実装されています。

const buffer = Buffer.alloc(3)
buffer.writeUInt8(4, 0)
buffer.writeUInt16LE(5, 1)

ただし、書き込む前に書き込む領域を割り当てる必要があります。
allocメソッドは書き込み領域を割り当て、writeUInt8メソッドは、bufferに対して指定されたオフセットの位置から符号なし8ビット整数、16ビット整数を書き込んでいます。

pythonでバイナリデータを書き込む場合は以下のメソッドで対応できます。

  • pack(format, v1, v2, ...)
  • pack_into(format, buffer, offset, v1, v2, ...)

python用の formatは読み込みと同じように書式指定文字を指定します。
2バイトの書き込みは書式指定文字を指定し、バイトオーダーを指定する文字(<)を指定します。

from struct import pack_into

class Buffer:
  def __init__(self, byte_data: bytearray):
    self._byte_data = byte_data
    self.bytelength = len(byte_data)

  @classmethod
  def alloc(cls, size: int):
    return cls(bytearray(size))

  def write_uint8(self, value: int, offset: int):
    return pack_into("B", self._byte_data, offset, value)

  def write_uint16le(self, value: int, offset: int):
    return pack_into("<H", self._byte_data, offset, value

buffer = Buffer.alloc(3)
buffer.write_uint8(4, 0)
buffer.write_uint16le(12, 1)

バイナリデータを操作するためのBufferクラスを定義し、 alloc、writeメソッドを定義しました。

まとめ

今回、バイナリデータの割り当て、読み込み、書き込みを行うためBufferクラスを定義しました。
toioに対するBLE操作はこのクラスを用いて行います。

class Buffer:
  def __init__(self, byte_data: bytearray):
    self._byte_data = byte_data
    # 8bit符号なし整数(unsigned char)
    self.bytelength = len(byte_data)

  @property
  def byte_data(self):
    return bytearray(self._byte_data)

  @classmethod
  def from_data(cls, data_array: List):
    size = len(data_array)
    byte_data = pack("B" * size, *data_array)
    return cls(bytearray(byte_data))

  def read_uint8(self, offset: int):
    # unpackした結果はtupleになっている
    return unpack_from("B", self._byte_data, offset)[0]

  def read_uint16le(self, offset: int):
    # unpackした結果はtupleになっている
    return unpack_from("<H", self._byte_data, offset)[0]

  def read_uint32le(self, offset: int):
    # unpackした結果はtupleになっている
    return unpack_from("<I", self._byte_data, offset)[0]

  def write_uint8(self, value: int, offset: int):
    return pack_into("B", self._byte_data, offset, value)

  def write_uint16le(self, value: int, offset: int):
    return pack_into("<H", self._byte_data, offset, value)

  def to_str(
    self, 
    encoding: str = "utf-8", 
    start: int = 0, 
    end: Optional[int] = None
  ):

    if end is None:
      end = self.bytelength

    return self._byte_data[start:end].decode(encoding)

  @classmethod
  def alloc(cls, size: int):
    return cls(bytearray(size))

次回はtoioのBLE操作を行いたいと思います。