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.
- kikaiken_pycontroller-0.1.0.dist-info/METADATA +152 -0
- kikaiken_pycontroller-0.1.0.dist-info/RECORD +7 -0
- kikaiken_pycontroller-0.1.0.dist-info/WHEEL +5 -0
- kikaiken_pycontroller-0.1.0.dist-info/top_level.txt +1 -0
- pycontroller/__init__.py +11 -0
- pycontroller/_controller.py +74 -0
- pycontroller/_states.py +216 -0
|
@@ -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 @@
|
|
|
1
|
+
pycontroller
|
pycontroller/__init__.py
ADDED
|
@@ -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
|
+
|
pycontroller/_states.py
ADDED
|
@@ -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})"
|