[続] PythonでPLCのレジスタアクセスを試す

「[PythonでPLCのレジスタアクセスを試す](https://qiita.com/satosisotas/items/78814da5756de76f1a77)」では、SLMPによる基本的なワードアクセスをPythonで試しました。
今回は、バイナリコードでのビットデバイスのリード・ライトをするときのデータ操作をPythonで実装してみます。

## ビットデバイスのアクセス方法

SLMPでは、ビットデバイスへの連続アクセス方法として2種類用意されています。

- ビットデバイス（連続したデバイス番号）から1点単位で値を読み書き
- ビットデバイス（連続したデバイス番号）から16点単位で値を読み書き

1点単位でのアクセスの場合は、下記のように4ビットで1点のBCDコード表現となっています。
下記は読み出し時の交信例です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493279/8553c7e4-6026-e310-ec89-4c52862f90d7.png)

16点単位でのアクセスでは、1ビットで1点の2値表現となっています。
下記は読み出し時の交信例です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493279/1753a13d-8280-99e9-d944-11eb81360288.png)

当然ながら、連続で大量に読み書きするには16点単位の方が有利です。

使用する通信コマンドはワードアクセス時と変わらないのですが、上記のようにビットデータがパックされますので、Pythonでリストなどの配列データとして扱うにはデータ変換が必要になってきます。

これが結構面倒ですし、特に16点単位の場合は、普通に1ビットづつ取り出そうとするとPythonでは処理速度への影響が懸念されます。
ここでは、numpyのビット操作を使って実装してみます。

## 1点単位のアクセス

### 読み出し

ワードアクセスと同様に、Readコマンド(`0401h`)で読み出します。ただしサブコマンドは `0001h` と異なっています。

受信したデータ配列が `[0x00, 0x01, 0x00, 0x11, ...]` のように抽出されたとすると、`[0, 0, 0, 1, 0, 0, 1, 1, ...]` のように展開する必要があります。

下記サンプルコードは、BCDコード配列を展開します。
引数の`data`および戻り値はいずれも numpy の ndarray (dtype=uint8) となります。

numpyを使うと配列データを一括で演算できるため、Python構文でのループ処理が不要となり、特にデータ量が多い場合には高速処理が期待できます。

```python
import numpy as np

# ex. data = np.array([0x12, 0x34, 0x56], 'u1')

def decode_bcd(data):
    """
    Decode 4bit BCD array
      [0x12,0x34,...] --> [1,2,3,4,...]
    """
    binArrayH = (data >> 4) & 0x0F
    binArrayL = data & 0x0F

    binArray = np.empty(data.size * 2, 'u1')
    binArray[::2] = binArrayH
    binArray[1::2] = binArrayL

    return binArray
```

### 書き込み

読み出しと逆のパターンです。BCDコードにパックします。
元データが奇数個の場合は最後の4bitは未使用なので、0で埋めます。

```python
def encode_bcd(data):
    """
    Encode 4bit BCD array
      [1,2,3,4,...] --> [0x12,0x34,...]
      入力が奇数個の場合は最後の4bitを0埋め
    """
    binArrayH = (data[::2] & 0x0F) << 4
    binArrayL = data[1::2] & 0x0F
    binArray = np.zeros_like(binArrayH)

    if data.size % 2 == 0:
        binArray = binArrayH | binArrayL
    else:
        binArray[:-1] = binArrayH[:-1] | binArrayL
        binArray[-1] = binArrayH[-1]

    return binArray
```

## 16点単位のアクセス

### 読み出し

ワードアクセスと同様に、Readコマンド(`0401h`)で読み出し、サブコマンドも同じです。
中身はワードデータだから通信コマンドとしては同じということですね。

先ほどの交信例の図にあったように、受信した4バイトのデータ `[34h, 12h, 02h, 00h]` は、

    [<M107>, ..., <M100>, <M115>, ..., <M108>, <M123>, ..., <M116>, <M131>, ..., <M124>]
                        ^                    ^                    ^

のように、ビット展開して16*4=32点のデータ配列に展開することになります。

ここでややこしいのが、**1バイトにつきアドレスの順序が逆になって格納されている**というところです。
つまり上記の例では、最初の1バイト`34h`のLSBが、読み出しコマンド送信時の先頭アドレスの値で、MSBに向かってアドレスがインクリメントされていったデータの値ということになります。
そして、2バイト目の`12h`については、同様に今度はLSBが「先頭アドレス+8」の値で・・・ということになります。

下記サンプルコードは、numpyのunpackbitを使用して各ビットデータを1次元配列に展開します。
[numpy.packbits](https://docs.scipy.org/doc/numpy/reference/generated/numpy.packbits.html), [numpy.unpackbits](https://docs.scipy.org/doc/numpy/reference/generated/numpy.unpackbits.html) は、10進数⇔2進数配列の変換をしてくれる関数です。

LSBとMSBの順序を逆にしなければいけないところを、一旦2次元配列化してビット展開した後にスライス指定で列方向の順序を逆にしているところがミソです。

```python
def unpack_bits(data):
    """
    LSBから順に格納されているビット列を配列に展開する
    [<M107 ... M100>, <M115 ... M108>] --> [<M100>, ... ,<M107>, <M108>, ... ,<M115>]
    """

    # unpackbits後のデータ順を反転させるために、疑似的に2次元配列として、
    # 1バイトごとにビットデータが格納されるようにする
    #   ex. [1,2,3] --> [[1],[2],[3]]
    byteArray2D = data.reshape((data.size, 1))

    # ビットデータを展開
    #   ex. [[1],[2],[3]] --> [[0,0,0,0,0,0,0,1],[0,0,0,0,0,0,1,0],[0,0,0,0,0,0,1,1]]
    byteArray2D_bin = np.unpackbits(byteArray2D, axis=1)
            
    # 列方向の順序を反転後、1次元配列に戻す
    return byteArray2D_bin[:, ::-1].flatten()
```

### 書き込み

読み出しと逆のパターンです。0/1の1次元配列データをバイト列にパックします。

```python
def pack_bits(data):
    """
    ビットデータの配列をLSBから順に格納されたバイト列にパックする
    [<M100>, ... ,<M107>, <M108>, ... ,<M115>]  --> [<M107 ... M100>, <M115 ... M108>]
    """
    # データ数が8の倍数になるようにする
    size8 = -(-data.size // 8) * 8

    # 最後サイズが足りないところを0埋め
    byteArray_bin = np.zeros(size8, 'u1')
    byteArray_bin[:size8] = data

    # 8bitごとにデータ順を反転させるために2次元配列に変換
    byteArray2D_bin = byteArray_bin.reshape((size8//8, 8))

    # ビットデータをパック
    return np.packbits(byteArray2D_bin[:, ::-1])
```


## 参考文献

- 三菱電機 SLMPリファレンスマニュアル