M5StickC plus のMicropythonで赤外線リモコン

M5シリーズの赤外線リモコンの実装はArduino環境ではたくさんあったが、Micropythonでのやり方は
あまり書いてなかったので自分用も兼ねてメモ

# 環境
* M5StickC plus
* M5 UI Flow v1.7.2

# 結論
`esp32.RMT` モジュールを使う
https://docs.micropython.org/en/latest/library/esp32.html?highlight=rmt#esp32.RMT

# esp32.RMT

公式ドキュメントに記載されている通り、赤外線リモコン用に用意されているモジュール。
ソースの周波数が80MHz固定、送信用機能しかないなど、まだベータ版としての位置づけみたい。

`clock_div`で`RMT.write_pulse`での時間分解能を指定。
分解能は`1 / (source_freq / clock_div)`で計算できる。

`RMT.write_pulses(pulses, start)`で`pulses`にON/OFFの時間の
リストまたはタプルを渡してあげることで、指定したキャリアのパルスが発生する。
`start`は`pulse`の最初がON/OFFのどちらか。

```python
# To use carrier frequency
r = esp32.RMT(channel=0, pin=Pin(18), clock_div=80, carrier_freq=38000, carrier_duty_percent=33)
r  # RMT(channel=0, pin=18, source_freq=80000000, clock_div=80, carrier_freq=38000, carrier_duty_percent=33)

# The channel resolution is 1us (1/(source_freq/clock_div)).
r.write_pulses((1, 20, 2, 40), start=0)  # Send 0 for 1us, 1 for 20us, 0 for 2us, 1 for 40us
```

# 実装と確認
我が家の エアコン(Panasonic) で試してみた。
Panasonicは家製協のフォーマットであるので、下記を参考に実装してM5 UI Flowから書き込み[^1]して動作したことを確認。
https://github.com/r45635/HVAC-IR-Control/blob/master/Protocol/Panasonic%20HVAC%20IR%20Protocol%20specification.pdf

リモコンがどんなコードを送信しているかなどの解析は割愛。
https://github.com/gensyu/micropython_ir_send

[^1]: RMTチャンネル割当て後、deinit()せずに同じチャンネルに割当するとエラー測れるので、書込み毎にリセットする必要あり



```python
# MicroPython esp32 AEHA 赤外線リモコン送信
import time

T = 436
HEADER_HIGH_US = 3400
HEADER_LOW_US = 1750


def reverce_8bit(data_8bit: int) -> int:
    """8bit を反転させる

    Parameters
    ----------
    data_8bit : int
        8bit数値

    Returns
    -------
    int
        ビット反転後の数値
    """

    data_8bit = ((data_8bit & 0b01010101) << 1) | ((data_8bit & 0b10101010) >> 1)
    data_8bit = ((data_8bit & 0b00110011) << 2) | ((data_8bit & 0b11001100) >> 2)
    return (data_8bit & 0b00001111) << 4 | data_8bit >> 4


def cal_parity(customer_code: list) -> int:
    """パリティ計算

    Parameters
    ----------
    customer_code : int
        カスタマーコード

    Returns
    -------
    int
        パリティ計算結果
    """
    parity = 0b0000
    for i in customer_code:
        upper_4bit = i >> 4
        lower_4bit = i & 0b00001111
        parity = parity ^ upper_4bit ^ lower_4bit
    return parity


def cal_sum(data_lsb: list) -> int:
    """チェックサム計算

    Parameters
    ----------
    data_lsb : list
        データ配列

    Returns
    -------
    int
        チェックサム値
    """
    val = 0
    for i in data_lsb:
        val = (val + i) % 256
    return val


def encode_ir_data(customer_code_lsb: list, data_lsb: list) -> str:
    """カスタマーコード、データを0, 1文字列に変換

    Parameters
    ----------
    customer_code : List[int]
        カスタマーコード
    data : List[int]
        送信データ

    Returns
    -------
    str
        0,1文字列
    """
    bit_str = ""
    customer_code_msb = [reverce_8bit(i) for i in customer_code_lsb]
    for d in customer_code_msb:
        bit_str += "{0:08b}".format(d)
    bit_str = bit_str + "{:04b}".format(cal_parity(customer_code_msb))

    data_msb = [reverce_8bit(i) for i in data_lsb]
    check_sum = cal_sum(customer_code_lsb + data_lsb)
    data_msb.append(reverce_8bit(check_sum))
    for i, d in enumerate(data_msb):
        if i == 0:
            bit_str += "{:04b}".format(d)
        else:
            bit_str += "{:08b}".format(d)
    return bit_str


def generate_frame(bit_code: str) -> list:
    """01文字列からON/OFFの時間に変換

    Parameters
    ----------
    bit_code : str
        0,1文字列

    Returns
    -------
    list
        ON/OFF時間の配列
    """
    pulse_width_data = [HEADER_HIGH_US, HEADER_LOW_US]
    for bit in bit_code:
        if bit == "0":
            pulse_width_data.extend([T, T])
        else:
            pulse_width_data.extend([T, 3 * T])
    pulse_width_data.extend([T])
    return pulse_width_data


def send_ir_data(rmt, customer_code, *data):
    if len(data) == 0:
        raise TypeError("data引数がありません")
    for d in data:
        bit_code = encode_ir_data(customer_code, d)
        frame = generate_frame(bit_code)
        rmt.write_pulses(frame, start=1)
        while not (rmt.wait_done()):
            time.sleep_us(T)
        time.sleep_us(8000)

if __name__ == "__main__":
    CUSTOMER_CODE = [0x02, 0x20]
    DATA1 = [0xE0, 0x04, 0x00, 0x00, 0x00]
    DATA2 = [
        0xE0,
        0x04,
        0x00,
        0x41,
        0x30,
        0x80,
        0xAF,
        0x00,
        0x00,
        0x06,
        0x60,
        0x00,
        0x00,
        0x80,
        0x00,
        0x06,
    ]

    ir_led = RMT(0, pin=Pin(9), clock_div=80, carrier_freq=38000)
    send_ir_data(ir_led, CUSTOMER_CODE, DATA1, DATA2)