kikaiken-pycontroller 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: kikaiken-pycontroller
3
+ Version: 0.1.0
4
+ Summary: USB・Bluetooth接続のコントローラ入力状態を取得するシンプルなライブラリ
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: pygame>=2.6.1
8
+
9
+ # pycontroller
10
+
11
+ USB・Bluetooth接続のコントローラ入力状態を取得するシンプルなライブラリです。現在の入力状態のみを扱う、ステートレスな設計になっています。
12
+ pygame をバックエンドとして使用しますが、pygame 固有の初期化処理はライブラリ内に隠蔽されています。
13
+
14
+ ## インストール
15
+
16
+ ```bash
17
+ uv add kikaiken-pycontroller
18
+ ```
19
+
20
+ ## 基本的な使い方
21
+
22
+ `Controller` を `with` ブロックで開き、ループ内で `read()` を呼ぶだけです。
23
+
24
+ ```python
25
+ from pycontroller import Controller
26
+
27
+ with Controller(index=0) as ctrl:
28
+ while True:
29
+ state = ctrl.read()
30
+ print(state.axes.axis0) # 左スティック X 軸: -1.0 〜 1.0
31
+ print(state.buttons.button0) # ボタン0: True / False
32
+ print(state.hats.hat0) # 十字キー: (x, y)
33
+ ```
34
+
35
+ ## API リファレンス
36
+
37
+ ### コントローラの検索・接続
38
+
39
+ 接続前にどのコントローラが繋がっているか確認できます。
40
+
41
+ ```python
42
+ import pycontroller
43
+
44
+ # 接続中のコントローラ名一覧
45
+ pycontroller.list_controllers()
46
+ # => ["Xbox Wireless Controller", "DualSense Wireless Controller"]
47
+
48
+ # 接続台数を確認したい場合
49
+ len(pycontroller.list_controllers()) # => 2
50
+ ```
51
+
52
+ ### `Controller`
53
+
54
+ `with` ブロックで接続・切断を管理します。複数のコントローラを同時に開くこともできます。
55
+
56
+ ```python
57
+ # 1台目
58
+ with Controller(index=0) as ctrl:
59
+ print(ctrl.name) # => "Xbox Wireless Controller"
60
+ state = ctrl.read()
61
+
62
+ # 2台同時
63
+ with Controller(0) as ctrl0, Controller(1) as ctrl1:
64
+ state0 = ctrl0.read()
65
+ state1 = ctrl1.read()
66
+ ```
67
+
68
+ `index` が接続台数以上の場合は `IndexError` を送出します。
69
+
70
+ ---
71
+
72
+ ### `ControllerState` — `ctrl.read()` の戻り値
73
+
74
+ `read()` を呼ぶたびに最新の入力状態が得られます。3つの属性を持ちます。
75
+
76
+ ```python
77
+ state = ctrl.read()
78
+
79
+ state.axes # アナログ軸 → AxesState
80
+ state.buttons # ボタン → ButtonsState
81
+ state.hats # 十字キー → HatsState
82
+ ```
83
+
84
+ ---
85
+
86
+ ### `AxesState` — アナログ軸
87
+
88
+ 値の範囲は `-1.0` 〜 `1.0` です。`axis0` 〜 `axis7` のプロパティ、またはインデックス・イテレーションでアクセスできます。
89
+
90
+ ```python
91
+ axes = state.axes
92
+
93
+ # プロパティ・インデックス (どちらも0始まり)
94
+ axes.axis0 # => 0.003
95
+ axes.axis1 # => -1.0
96
+ axes[0] # => 0.003
97
+
98
+ # イテレーション・件数
99
+ len(axes) # => 6
100
+ list(axes) # => [0.003, -1.0, 0.0, 0.0, -1.0, -1.0]
101
+ ```
102
+
103
+ コントローラが持たない番号にアクセスすると `IndexError` を送出します。
104
+
105
+ ---
106
+
107
+ ### `ButtonsState` — ボタン
108
+
109
+ 値は `True`(押下中) / `False`(未押下) です。`button0` 〜 `button19` のプロパティ、またはインデックス・イテレーションでアクセスできます。
110
+
111
+ ```python
112
+ buttons = state.buttons
113
+
114
+ # プロパティ・インデックス (どちらも0始まり)
115
+ buttons.button0 # => True
116
+ buttons.button1 # => False
117
+ buttons[0] # => True
118
+
119
+ # イテレーション・件数
120
+ len(buttons) # => 14
121
+ list(buttons) # => [True, False, False, ...]
122
+ ```
123
+
124
+ コントローラが持たない番号にアクセスすると `IndexError` を送出します。
125
+
126
+ ---
127
+
128
+ ### `HatsState` — 十字キー(ハット)
129
+
130
+ 値は `(x, y)` の tuple で、各要素は `-1`、`0`、`1` のいずれかです。`hat0` 〜 `hat3` のプロパティ、またはインデックス・イテレーションでアクセスできます。
131
+
132
+ ```python
133
+ hats = state.hats
134
+
135
+ # プロパティ・インデックス (どちらも0始まり)
136
+ hats.hat0 # => (0, 1) ← 上
137
+ hats[0] # => (0, 1)
138
+
139
+ # イテレーション・件数
140
+ len(hats) # => 1
141
+ list(hats) # => [(0, 1)]
142
+ ```
143
+
144
+ | `(x, y)` | 方向 |
145
+ |-----------|------|
146
+ | `(0, 0)` | ニュートラル |
147
+ | `(1, 0)` | 右 |
148
+ | `(-1, 0)` | 左 |
149
+ | `(0, 1)` | 上 |
150
+ | `(0, -1)` | 下 |
151
+
152
+ コントローラが持たない番号にアクセスすると `IndexError` を送出します。
@@ -0,0 +1,7 @@
1
+ pycontroller/__init__.py,sha256=GgsmNuoJRwt8JTdKJKk35A1yv6XNSXZ2XVUbERngR_4,285
2
+ pycontroller/_controller.py,sha256=UwMPsbT0Hxgr5hv9rQzgjfASimgudXrRSh-i201NnqU,2333
3
+ pycontroller/_states.py,sha256=591nte518pSpJNZGwrrVTi7uPaF7gs1c9PxhwBtKeBs,4912
4
+ kikaiken_pycontroller-0.1.0.dist-info/METADATA,sha256=kPu8Nd_wMRniQHRvYEBBodeDgCAxkxkPZxQkGAxBO_w,4496
5
+ kikaiken_pycontroller-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ kikaiken_pycontroller-0.1.0.dist-info/top_level.txt,sha256=waznDYpbhwQXkADVm05smvKghEHKbCmKypLP7TXZPE0,13
7
+ kikaiken_pycontroller-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pycontroller
@@ -0,0 +1,11 @@
1
+ from pycontroller._controller import Controller, list_controllers
2
+ from pycontroller._states import AxesState, ButtonsState, ControllerState, HatsState
3
+
4
+ __all__ = [
5
+ "AxesState",
6
+ "ButtonsState",
7
+ "Controller",
8
+ "ControllerState",
9
+ "HatsState",
10
+ "list_controllers",
11
+ ]
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ import pygame
6
+
7
+ from pycontroller._states import AxesState, ButtonsState, ControllerState, HatsState
8
+
9
+ _open_count = 0
10
+
11
+
12
+ def _ensure_init() -> None:
13
+ os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
14
+ os.environ.setdefault("SDL_AUDIODRIVER", "dummy")
15
+ pygame.init()
16
+
17
+
18
+ def list_controllers() -> list[str]:
19
+ if _open_count == 0:
20
+ _ensure_init()
21
+ names = [pygame.joystick.Joystick(i).get_name() for i in range(pygame.joystick.get_count())]
22
+ if _open_count == 0:
23
+ pygame.quit()
24
+ return names
25
+
26
+
27
+ class Controller:
28
+ def __init__(self, index: int = 0) -> None:
29
+ self._index = index
30
+ self._joystick: pygame.joystick.JoystickType | None = None
31
+
32
+ def __enter__(self) -> Controller:
33
+ global _open_count
34
+ if _open_count == 0:
35
+ _ensure_init()
36
+ _open_count += 1
37
+ count = pygame.joystick.get_count()
38
+ if self._index >= count:
39
+ _open_count -= 1
40
+ if _open_count == 0:
41
+ pygame.quit()
42
+ raise IndexError(
43
+ f"コントローラ(index={self._index})が見つかりません "
44
+ f"(接続中のコントローラ数: {count})"
45
+ )
46
+ self._joystick = pygame.joystick.Joystick(self._index)
47
+ return self
48
+
49
+ def __exit__(self, *_: object) -> None:
50
+ global _open_count
51
+ if self._joystick is not None:
52
+ self._joystick.quit()
53
+ self._joystick = None
54
+ _open_count -= 1
55
+ if _open_count == 0:
56
+ pygame.quit()
57
+
58
+ def read(self) -> ControllerState:
59
+ if self._joystick is None:
60
+ raise RuntimeError("Controller は 'with' ブロック内で使用してください")
61
+ pygame.event.pump()
62
+ j = self._joystick
63
+ return ControllerState(
64
+ axes=AxesState(tuple(j.get_axis(i) for i in range(j.get_numaxes()))),
65
+ buttons=ButtonsState(tuple(bool(j.get_button(i)) for i in range(j.get_numbuttons()))),
66
+ hats=HatsState(tuple(j.get_hat(i) for i in range(j.get_numhats()))),
67
+ )
68
+
69
+ @property
70
+ def name(self) -> str:
71
+ if self._joystick is None:
72
+ raise RuntimeError("Controller は 'with' ブロック内で使用してください")
73
+ return self._joystick.get_name()
74
+
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+
5
+
6
+ class AxesState:
7
+ def __init__(self, data: tuple[float, ...]) -> None:
8
+ self._data = data
9
+
10
+ def _get(self, index: int) -> float:
11
+ if index >= len(self._data):
12
+ raise IndexError(
13
+ f"軸{index}は存在しません "
14
+ f"(このコントローラの軸数: {len(self._data)})"
15
+ )
16
+ return self._data[index]
17
+
18
+ def __len__(self) -> int:
19
+ return len(self._data)
20
+
21
+ def __iter__(self) -> Iterator[float]:
22
+ return iter(self._data)
23
+
24
+ def __getitem__(self, index: int) -> float:
25
+ return self._data[index]
26
+
27
+ def __repr__(self) -> str:
28
+ return f"AxesState({self._data})"
29
+
30
+ @property
31
+ def axis0(self) -> float:
32
+ return self._get(0)
33
+
34
+ @property
35
+ def axis1(self) -> float:
36
+ return self._get(1)
37
+
38
+ @property
39
+ def axis2(self) -> float:
40
+ return self._get(2)
41
+
42
+ @property
43
+ def axis3(self) -> float:
44
+ return self._get(3)
45
+
46
+ @property
47
+ def axis4(self) -> float:
48
+ return self._get(4)
49
+
50
+ @property
51
+ def axis5(self) -> float:
52
+ return self._get(5)
53
+
54
+ @property
55
+ def axis6(self) -> float:
56
+ return self._get(6)
57
+
58
+ @property
59
+ def axis7(self) -> float:
60
+ return self._get(7)
61
+
62
+
63
+ class ButtonsState:
64
+ def __init__(self, data: tuple[bool, ...]) -> None:
65
+ self._data = data
66
+
67
+ def _get(self, index: int) -> bool:
68
+ if index >= len(self._data):
69
+ raise IndexError(
70
+ f"ボタン{index}は存在しません "
71
+ f"(このコントローラのボタン数: {len(self._data)})"
72
+ )
73
+ return self._data[index]
74
+
75
+ def __len__(self) -> int:
76
+ return len(self._data)
77
+
78
+ def __iter__(self) -> Iterator[bool]:
79
+ return iter(self._data)
80
+
81
+ def __getitem__(self, index: int) -> bool:
82
+ return self._data[index]
83
+
84
+ def __repr__(self) -> str:
85
+ return f"ButtonsState({self._data})"
86
+
87
+ @property
88
+ def button0(self) -> bool:
89
+ return self._get(0)
90
+
91
+ @property
92
+ def button1(self) -> bool:
93
+ return self._get(1)
94
+
95
+ @property
96
+ def button2(self) -> bool:
97
+ return self._get(2)
98
+
99
+ @property
100
+ def button3(self) -> bool:
101
+ return self._get(3)
102
+
103
+ @property
104
+ def button4(self) -> bool:
105
+ return self._get(4)
106
+
107
+ @property
108
+ def button5(self) -> bool:
109
+ return self._get(5)
110
+
111
+ @property
112
+ def button6(self) -> bool:
113
+ return self._get(6)
114
+
115
+ @property
116
+ def button7(self) -> bool:
117
+ return self._get(7)
118
+
119
+ @property
120
+ def button8(self) -> bool:
121
+ return self._get(8)
122
+
123
+ @property
124
+ def button9(self) -> bool:
125
+ return self._get(9)
126
+
127
+ @property
128
+ def button10(self) -> bool:
129
+ return self._get(10)
130
+
131
+ @property
132
+ def button11(self) -> bool:
133
+ return self._get(11)
134
+
135
+ @property
136
+ def button12(self) -> bool:
137
+ return self._get(12)
138
+
139
+ @property
140
+ def button13(self) -> bool:
141
+ return self._get(13)
142
+
143
+ @property
144
+ def button14(self) -> bool:
145
+ return self._get(14)
146
+
147
+ @property
148
+ def button15(self) -> bool:
149
+ return self._get(15)
150
+
151
+ @property
152
+ def button16(self) -> bool:
153
+ return self._get(16)
154
+
155
+ @property
156
+ def button17(self) -> bool:
157
+ return self._get(17)
158
+
159
+ @property
160
+ def button18(self) -> bool:
161
+ return self._get(18)
162
+
163
+ @property
164
+ def button19(self) -> bool:
165
+ return self._get(19)
166
+
167
+
168
+ class HatsState:
169
+ def __init__(self, data: tuple[tuple[int, int], ...]) -> None:
170
+ self._data = data
171
+
172
+ def _get(self, index: int) -> tuple[int, int]:
173
+ if index >= len(self._data):
174
+ raise IndexError(
175
+ f"ハット{index}は存在しません "
176
+ f"(このコントローラのハット数: {len(self._data)})"
177
+ )
178
+ return self._data[index]
179
+
180
+ def __len__(self) -> int:
181
+ return len(self._data)
182
+
183
+ def __iter__(self) -> Iterator[tuple[int, int]]:
184
+ return iter(self._data)
185
+
186
+ def __getitem__(self, index: int) -> tuple[int, int]:
187
+ return self._data[index]
188
+
189
+ def __repr__(self) -> str:
190
+ return f"HatsState({self._data})"
191
+
192
+ @property
193
+ def hat0(self) -> tuple[int, int]:
194
+ return self._get(0)
195
+
196
+ @property
197
+ def hat1(self) -> tuple[int, int]:
198
+ return self._get(1)
199
+
200
+ @property
201
+ def hat2(self) -> tuple[int, int]:
202
+ return self._get(2)
203
+
204
+ @property
205
+ def hat3(self) -> tuple[int, int]:
206
+ return self._get(3)
207
+
208
+
209
+ class ControllerState:
210
+ def __init__(self, axes: AxesState, buttons: ButtonsState, hats: HatsState) -> None:
211
+ self.axes = axes
212
+ self.buttons = buttons
213
+ self.hats = hats
214
+
215
+ def __repr__(self) -> str:
216
+ return f"ControllerState(axes={self.axes!r}, buttons={self.buttons!r}, hats={self.hats!r})"