toioをpythonで動かす:バイナリデータ操作
はじめに
前回から「toioをpythonで動かす」を始めました。
toio.jsのソースコードを見ると、BLE操作を行うため、バイナリデータの読み書きを行っています。
今回はjavascriptで書かれているコードをpythonで実現したいと思います。
pythonでバイナリデータを扱う
pythonでバイナリデータを扱う場合はbytesかbytearrayを用います。
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操作を行いたいと思います。