Metadata-Version: 2.4
Name: kikaiken-pyserial
Version: 1.0.2
Summary: シリアル通信をシンプルにするライブラリ
Author: mugi2525
License: MIT
Requires-Python: >=3.10
Requires-Dist: pyserial>=3.5
Description-Content-Type: text/markdown

# kikaiken-pyserial

シリアル通信をシンプルにするライブラリです。[pyserial](https://github.com/pyserial/pyserial) のラッパーとして動作します。

## インストール

```bash
uv add kikaiken-pyserial
```

## 基本的な使い方

`SerialPort` を `with` ブロックで開き、`send()` / `read_line()` を呼ぶだけです。

```python
from kikaiken_pyserial import SerialPort

with SerialPort("COM3", baudrate=115200) as ser:
    ser.send("Hello\r\n")
    response = ser.read_line()
    print(response)
```

---

## ポートの確認

接続中のシリアルポートを一覧表示します。どのポート名を指定すればいいかわからないときに使います。

```python
import kikaiken_pyserial

ports = kikaiken_pyserial.list_ports()
print(ports)
# => ["COM3", "COM8"]  (Windows)
# => ["/dev/ttyUSB0", "/dev/ttyACM0"]  (Linux)
```

---

## `SerialPort`

### 接続

```python
SerialPort(
    port: str | None = None,  # ポート名 ("COM3", "/dev/ttyUSB0" など)。省略すると STM32 を自動検出
    baudrate: int = 115200,
    *,
    timeout: float = 1.0,   # 受信タイムアウト（秒）
    encoding: str = "utf-8",
)
```

`with` ブロックで接続・切断を管理します。`with` の外で使うと `RuntimeError` になります。

`port` を省略すると、接続中の STM32 デバイスを USB VID (`0x0483`) で自動検出します。見つからない場合や複数見つかった場合は `RuntimeError` になります。

```python
# STM32 を自動検出して接続
with SerialPort() as ser:
    print(ser.port)  # => "COM3" など（自動検出されたポート名）

# ポートを明示して接続
with SerialPort("COM3", baudrate=9600) as ser:
    print(ser.port)      # => "COM3"
    print(ser.baudrate)  # => 9600
```

### テキスト送信 — `send(data: str)`

文字列をそのまま送信します。**改行コードはユーザーが明示します。**

```python
with SerialPort("COM3") as ser:
    ser.send("ping\r\n")        # \r\n を明示
    ser.send("speed:100\n")     # \n だけでもOK
    ser.send("raw message")     # 改行なしも送れる
```

### テキスト受信 — `read_line() -> str`

`\n` が来るまで待ち、末尾の `\r\n` / `\n` を取り除いた文字列を返します。

```python
with SerialPort("COM3") as ser:
    line = ser.read_line()
    print(line)  # => "OK"  (デバイスが "OK\r\n" を返した場合)
```

タイムアウト内に改行が届かない場合は `TimeoutError` を送出します。

```python
with SerialPort("COM3", timeout=0.5) as ser:
    try:
        line = ser.read_line()
    except TimeoutError:
        print("応答がありませんでした")
```

### バイナリ受信 — `read_bytes(n: int) -> bytes`

ちょうど `n` バイト受信して返します。タイムアウトまでに届かない場合は `TimeoutError` です。

```python
with SerialPort("COM3") as ser:
    header = ser.read_bytes(4)   # 4バイトのヘッダを読む
    print(header.hex())          # => "deadbeef"
```

---

## `BinaryProtocol` — バイナリプロトコルの定義

マイコンとバイナリ形式で通信する場合は `BinaryProtocol` を使います。クラス変数にフィールドを定義すると、`send_bytes()` でそのままシリアルに送れます。

### フィールドの型

| 型 | 説明 | 値の範囲 |
|---|---|---|
| `UInt(n)` | n ビット符号なし整数 | 0 〜 2ⁿ − 1 |
| `Int(n)` | n ビット符号あり整数 | −2ⁿ⁻¹ 〜 2ⁿ⁻¹ − 1 |
| `Bool()` | 1 ビットのフラグ | `True` / `False` |

### 基本的な定義

```python
from kikaiken_pyserial import BinaryProtocol, UInt, Int, Bool

class MotorCmd(BinaryProtocol):
    speed     = UInt(7)   # 0〜127
    direction = Bool()    # True=正転 / False=逆転
    # 合計 8 ビット = 1 バイト
```

フィールドの値は**インスタンス化時にキーワード引数で渡します。** 省略すると `TypeError` になります。

```python
cmd = MotorCmd(speed=100, direction=True)

print(cmd)               # => MotorCmd(speed=100, direction=True)
print(cmd.speed)         # => 100
print(cmd.to_bytes().hex())  # => "c9"
#  speed=100  → 0b1100100
#  direction=1 → 0b1
#  結合(MSBファースト) → 0b11001001 = 0xc9
```

インスタンス化後にフィールドを書き換えようとすると `AttributeError` になります。

```python
cmd.speed = 50   # AttributeError: 'MotorCmd.speed' はインスタンス化後に変更できません
```

### `send_bytes()` で送信

```python
with SerialPort("COM3") as ser:
    ser.send_bytes(MotorCmd(speed=80, direction=False))
```

### 複数バイトのプロトコル

フィールドの合計が 8 ビットを超えると自動的に複数バイトになります。

```python
class ServoCmd(BinaryProtocol):
    servo_id = UInt(4)    # 0〜15
    angle    = UInt(12)   # 0〜4095
    # 合計 16 ビット = 2 バイト

cmd = ServoCmd(servo_id=3, angle=2048)

print(cmd)                   # => ServoCmd(servo_id=3, angle=2048)
print(cmd.to_bytes().hex())  # => "3800"
# servo_id=3 → 0b0011
# angle=2048  → 0b100000000000
# 結合 → 0b0011_100000000000 = 0x3800
```

### 符号あり整数（`Int`）

```python
class PositionCmd(BinaryProtocol):
    x = Int(8)    # -128〜127
    y = Int(8)    # -128〜127
    # 合計 16 ビット = 2 バイト

cmd = PositionCmd(x=-30, y=50)

print(cmd.to_bytes().hex())  # => "e232"
```

### パディング

合計ビット数が 8 の倍数でない場合、**末尾（LSB 側）を自動的に 0 で埋めます。**

```python
class SensorCmd(BinaryProtocol):
    mode   = UInt(3)   # 0〜7
    enable = Bool()    # True/False
    # 合計 4 ビット → 4 ビットパディング → 1 バイト

cmd = SensorCmd(mode=5, enable=True)

print(cmd.to_bytes().hex())  # => "b0"
# mode=5 → 0b101、enable=1 → 0b1
# 結合(MSBファースト) + 4ビットパディング → 0b1011_0000 = 0xb0
```

---

## `kikaiken-pycontroller` と組み合わせる

コントローラの入力をシリアルでマイコンに送る典型的な使い方です。

```python
from pycontroller import Controller
from kikaiken_pyserial import BinaryProtocol, Int, SerialPort, UInt

class DriveCmd(BinaryProtocol):
    left  = Int(8)   # 左モータ速度: -128〜127
    right = Int(8)   # 右モータ速度: -128〜127
    # 合計 16 ビット = 2 バイト

with Controller(0) as ctrl, SerialPort("COM3") as ser:
    while True:
        state = ctrl.read()

        # 左スティックの Y 軸（前後）と X 軸（左右）で差動駆動
        forward = state.axes.axis1  # -1.0〜1.0
        turn    = state.axes.axis0  # -1.0〜1.0

        ser.send_bytes(DriveCmd(
            left=int((forward - turn) * 127),
            right=int((forward + turn) * 127),
        ))
```

テキストで送りたい場合はこうなります。

```python
from pycontroller import Controller
from kikaiken_pyserial import SerialPort

with Controller(0) as ctrl, SerialPort("COM3") as ser:
    while True:
        state = ctrl.read()
        x = state.axes.axis0
        y = state.axes.axis1
        ser.send(f"X:{x:.3f},Y:{y:.3f}\r\n")

        try:
            ack = ser.read_line()
            print("ACK:", ack)
        except TimeoutError:
            pass  # 応答がない場合は無視して続行
```

---

## API リファレンス

### `list_ports() -> list[str]`

接続中のシリアルポートの名前一覧を返します。

---

### `SerialPort`

| メソッド / プロパティ | 説明 |
|---|---|
| `SerialPort(port=None, baudrate=115200, *, timeout=1.0, encoding="utf-8")` | インスタンス生成。`port` 省略時は STM32 を自動検出 |
| `with SerialPort(...) as ser:` | 接続・切断の管理 |
| `ser.send(data: str)` | テキスト送信 |
| `ser.send_bytes(data: BinaryProtocol)` | バイナリ送信 |
| `ser.read_line() -> str` | 1 行受信（タイムアウトで `TimeoutError`） |
| `ser.read_bytes(n: int) -> bytes` | n バイト受信（タイムアウトで `TimeoutError`） |
| `ser.port` | ポート名 |
| `ser.baudrate` | ボーレート |

---

### `BinaryProtocol`

| 要素 | 説明 |
|---|---|
| `UInt(n)` | n ビット符号なし整数フィールド |
| `Int(n)` | n ビット符号あり整数フィールド |
| `Bool()` | 1 ビットフラグフィールド |
| `MyProtocol(field=value, ...)` | インスタンス生成。全フィールドの指定が必須 |
| `to_bytes() -> bytes` | フィールドの値をバイト列に変換（MSB ファースト） |
| `repr(cmd)` | `MyProtocol(field=value, ...)` 形式で内容を表示 |

フィールドは定義した順に MSB から詰められます。合計が 8 の倍数でない場合は末尾を 0 でパディングします。値が範囲外のとき `ValueError`、インスタンス化後に書き換えようとすると `AttributeError` を送出します。
