入力: ゲームパッド

### 背景
pygameを使うとゲームパッドの入力を使用できるようです。

https://www.pygame.org/wiki/GettingStarted

ここではdualsenseを使ってSphero boltを操作してみます。

### Scratch
Sphero IDEにはゲームパッドを使用する機能は見つかりませんでした。
（もしかするとbluetoothなどでゲームパッドつなげたらドライブモードで使えるのかも）

### spheroV2

chatGPT先生と相談しながら作成しました。
12/21と同様に、bluetoothの通信とpygameの並列化が衝突するようだったので、入力内容をqueingして逐次spheroを操作する方式にしました。

```
import pygame
import sys
import time
import threading
from queue import Queue
from spherov2 import scanner
from spherov2.sphero_edu import SpheroEduAPI
from spherov2.types import Color

# コントローラの入力処理スレッド
def controller_input_process(queue):
    pygame.init()
    pygame.joystick.init()

    if pygame.joystick.get_count() == 0:
        print("No controller connected.")
        sys.exit()

    controller = pygame.joystick.Joystick(0)
    controller.init()
    print(f"Controller connected: {controller.get_name()}")

    # 直前の値を保存する変数
    last_speed = None
    last_heading_change = None
    speed_threshold = 0.1  # スピードの閾値
    heading_threshold = 0.2  # 方向変更の閾値

    try:
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    queue.put(("exit", None))
                    return

            # R2 トリガー値を取得
            r2_trigger = controller.get_axis(5)  # R2 トリガー
            if r2_trigger > speed_threshold:
                speed = r2_trigger * 30  # 最大速度を30に制限
            else:
                speed = 0  # トリガーが離されると停止

            # 左スティックの X 軸値を取得
            left_stick_x = controller.get_axis(0)
            if abs(left_stick_x) > heading_threshold:
                heading_change = left_stick_x * 10  # 感度を調整
            else:
                heading_change = 0

            # 速度と方向のどちらかが閾値を超えていて、直前の値と異なる場合のみキューに追加
            if (abs(speed - (last_speed or 0)) > speed_threshold or
                    abs(heading_change - (last_heading_change or 0)) > heading_threshold):
                queue.put(("move", (speed, heading_change)))
                last_speed = speed
                last_heading_change = heading_change

                # デバッグ用出力
                print(f"R2 (raw): {r2_trigger:.2f}, Speed: {speed:.1f}, Left Stick X: {left_stick_x:.2f}, Heading Change: {heading_change:.2f}")

            # フレームレート調整
            time.sleep(0.03)

    except KeyboardInterrupt:
        print("\nController process interrupted by user.")
        queue.put(("exit", None))
    except Exception as e:
        print(f"An error occurred in controller process: {e}")

# Sphero Bolt の制御スレッド
def sphero_control_process(queue):
    print("Connecting to Sphero Bolt...")
    toy = scanner.find_BOLT()
    if toy is None:
        print("Failed to connect to Sphero Bolt.")
        return

    with SpheroEduAPI(toy) as droid:
        print("Connected to Bolt.")
        droid.reset_aim()
        droid.set_stabilization(True)
        droid.set_front_led(Color(r=5, g=5, b=5))  # LED を白に設定

        speed = 0
        heading = 0

        try:
            while True:
                # キューから操作内容を取得
                if not queue.empty():
                    action, data = queue.get()
                    if action == "exit":
                        break
                    elif action == "move":
                        speed, heading_change = data
                        heading += heading_change
                        heading %= 360  # 方向は 0〜360 度に制限

                        # Sphero Bolt を制御
                        droid.set_speed(int(speed))
                        droid.set_heading(int(heading))

                        # デバッグ用出力
                        print(f"Speed: {speed:.1f}, Heading Change: {heading_change:.2f}, Heading: {heading:.1f}")

                # フレームレート調整
                time.sleep(0.03)

        except KeyboardInterrupt:
            print("\nSphero control process interrupted by user.")
        except Exception as e:
            print(f"An error occurred in sphero control process: {e}")

# メインスクリプト
if __name__ == "__main__":
    queue = Queue()

    # コントローラの入力処理スレッドを起動
    controller_thread = threading.Thread(target=controller_input_process, args=(queue,))
    controller_thread.daemon = True
    controller_thread.start()

    # Sphero Bolt の制御スレッドを起動
    sphero_thread = threading.Thread(target=sphero_control_process, args=(queue,))
    sphero_thread.daemon = True
    sphero_thread.start()

    try:
        # メインスレッドでスレッドが終了するのを待つ
        while controller_thread.is_alive() and sphero_thread.is_alive():
            time.sleep(0.03)
    except KeyboardInterrupt:
        print("\nMain program interrupted by user.")
    finally:
        print("Exiting...")
        pygame.quit()

```

実行結果はこちらのようになりました。

残念ながらyawの変更がカクカクになったので、要修正です。

終了後に接続したままになったので、終了の工夫が必要です。
```
git\spherov2.py\spherov2\test>python BoltTest_ds.py
pygame 2.6.1 (SDL 2.28.4, Python 3.12.7)
Hello from the pygame community. https://www.pygame.org/contribute.html
Connecting to Sphero Bolt...
Controller connected: DualSense Wireless Controller
Connected to Bolt.
R2 (raw): 0.51, Speed: 15.4, Left Stick X: 0.04, Heading Change: 0.00
R2 (raw): 1.00, Speed: 30.0, Left Stick X: 0.04, Heading Change: 0.00
R2 (raw): 0.87, Speed: 26.2, Left Stick X: 0.04, Heading Change: 0.00
Speed: 15.4, Heading Change: 0.00, Heading: 0.0
R2 (raw): -0.24, Speed: 0.0, Left Stick X: 0.04, Heading Change: 0.00
Speed: 30.0, Heading Change: 0.00, Heading: 0.0
Speed: 26.2, Heading Change: 0.00, Heading: 0.0
Speed: 0.0, Heading Change: 0.00, Heading: 0.0
R2 (raw): 0.34, Speed: 10.2, Left Stick X: 0.07, Heading Change: 0.00
R2 (raw): 1.00, Speed: 30.0, Left Stick X: 0.06, Heading Change: 0.00
Speed: 10.2, Heading Change: 0.00, Heading: 0.0
...
R2 (raw): -1.00, Speed: 0.0, Left Stick X: -0.89, Heading Change: -8.90
R2 (raw): -1.00, Speed: 0.0, Left Stick X: 0.15, Heading Change: 0.00
Speed: 0.0, Heading Change: -8.90, Heading: 117.4
Speed: 0.0, Heading Change: 0.00, Heading: 117.4

Main program interrupted by user.
Exiting...
```

### まとめ
pygameを使い、dualsenseを使ってspheroを操作をするプログラミングを実験しました。
左右のパッドで左右のタイヤを動かすようにするとか、一つのパッドで前後左右に動かすとか、工夫すると面白そうです。