dglab-controller-python 0.1.0__tar.gz
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.
- dglab_controller_python-0.1.0/LICENSE +21 -0
- dglab_controller_python-0.1.0/PKG-INFO +138 -0
- dglab_controller_python-0.1.0/README.md +117 -0
- dglab_controller_python-0.1.0/dg_lab_devices/__init__.py +8 -0
- dglab_controller_python-0.1.0/dg_lab_devices/base.py +94 -0
- dglab_controller_python-0.1.0/dg_lab_devices/civec.py +33 -0
- dglab_controller_python-0.1.0/dg_lab_devices/common.py +31 -0
- dglab_controller_python-0.1.0/dg_lab_devices/controller.py +59 -0
- dglab_controller_python-0.1.0/dg_lab_devices/pawprints.py +101 -0
- dglab_controller_python-0.1.0/dglab_controller_python.egg-info/PKG-INFO +138 -0
- dglab_controller_python-0.1.0/dglab_controller_python.egg-info/SOURCES.txt +14 -0
- dglab_controller_python-0.1.0/dglab_controller_python.egg-info/dependency_links.txt +1 -0
- dglab_controller_python-0.1.0/dglab_controller_python.egg-info/requires.txt +1 -0
- dglab_controller_python-0.1.0/dglab_controller_python.egg-info/top_level.txt +1 -0
- dglab_controller_python-0.1.0/pyproject.toml +31 -0
- dglab_controller_python-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lindog114514
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dglab-controller-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 用于控制负鼠振动控制器以及读取灵猫和爪印传感器的 Python 库
|
|
5
|
+
Author-email: lindog114514 <kaikaihe467@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lindog114514/DGLAB-controller-python
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: bleak>=0.21.0
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
## DGLAB-controller-python
|
|
23
|
+
|
|
24
|
+
DG-LAB 蓝牙设备的 Python SDK,基于 bleak。
|
|
25
|
+
支持同时连接多个设备、异步事件处理,并通过 async with 自动管理连接。
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
支持的设备
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
灵猫边缘控制传感器(47L124000)
|
|
32
|
+
|
|
33
|
+
负鼠振动控制器 (47L127000)
|
|
34
|
+
|
|
35
|
+
爪印无线按钮传感器 (47L120300)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
安装
|
|
40
|
+
---
|
|
41
|
+
```bash
|
|
42
|
+
pip install DGLAB-controller-python
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
要求 Python 3.10+ 且 bleak ≥ 0.21。
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
快速开始
|
|
49
|
+
---
|
|
50
|
+
灵猫 – 气压上报
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import asyncio
|
|
54
|
+
from dg_lab_devices import Civec
|
|
55
|
+
|
|
56
|
+
async def main():
|
|
57
|
+
async with Civec() as civec:
|
|
58
|
+
@civec.on_pressure
|
|
59
|
+
def on_pressure(kpa: float):
|
|
60
|
+
print(f"气压: {kpa:.2f} kPa")
|
|
61
|
+
|
|
62
|
+
await civec.start_pressure_report(color=0x02)
|
|
63
|
+
await asyncio.sleep(30)
|
|
64
|
+
|
|
65
|
+
asyncio.run(main())
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
负鼠 – 按键与强度
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from dg_lab_devices import Controller
|
|
72
|
+
|
|
73
|
+
async def main():
|
|
74
|
+
async with Controller() as ctrl:
|
|
75
|
+
@ctrl.on_button
|
|
76
|
+
async def on_button(seq, buttons):
|
|
77
|
+
if buttons.get("A"):
|
|
78
|
+
await ctrl.set_strength(a=160)
|
|
79
|
+
|
|
80
|
+
await ctrl.set_led_and_report(enable_button_report=True)
|
|
81
|
+
await asyncio.sleep(60)
|
|
82
|
+
|
|
83
|
+
asyncio.run(main())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
爪印 – 触发模式
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from dg_lab_devices import PawPrints
|
|
90
|
+
|
|
91
|
+
async def main():
|
|
92
|
+
async with PawPrints() as pp:
|
|
93
|
+
@pp.on_trigger
|
|
94
|
+
def on_trigger(color, event_id, param):
|
|
95
|
+
print(f"触发事件 {event_id},参数={param}")
|
|
96
|
+
|
|
97
|
+
await pp.set_random_trigger(
|
|
98
|
+
color=0x01, event_id=5,
|
|
99
|
+
green_min=30, green_max=50,
|
|
100
|
+
reaction_time=10,
|
|
101
|
+
param_inc=20, param_speed=40,
|
|
102
|
+
param_dec=50, param_dec_speed=40
|
|
103
|
+
)
|
|
104
|
+
await asyncio.sleep(300)
|
|
105
|
+
|
|
106
|
+
asyncio.run(main())
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
主要特性
|
|
111
|
+
---
|
|
112
|
+
· **异步事件驱动** – 装饰器订阅设备通知
|
|
113
|
+
|
|
114
|
+
· **自动连接管理** – async with 自动连接/断开
|
|
115
|
+
|
|
116
|
+
· **多设备并发** – 同一事件循环同时运行多个设备
|
|
117
|
+
|
|
118
|
+
· **完整协议支持** – 实现所有 BLE 指令与回调
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
开源许可
|
|
122
|
+
---
|
|
123
|
+
本项目使用 MIT 许可证。
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
联系作者
|
|
127
|
+
---
|
|
128
|
+
. QQ群:870333220 <del>有香香软软的小南娘和技术大佬<del>
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
相关链接
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
. [参考文档](https://github.com/dungeonlab-open/dglab-bluetooth-protocol)
|
|
135
|
+
|
|
136
|
+
· [PyPI](https://pypi.org/project/DGLAB-controller-python/)
|
|
137
|
+
|
|
138
|
+
· [API文档](docs/API.md)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
## DGLAB-controller-python
|
|
2
|
+
|
|
3
|
+
DG-LAB 蓝牙设备的 Python SDK,基于 bleak。
|
|
4
|
+
支持同时连接多个设备、异步事件处理,并通过 async with 自动管理连接。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
支持的设备
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
灵猫边缘控制传感器(47L124000)
|
|
11
|
+
|
|
12
|
+
负鼠振动控制器 (47L127000)
|
|
13
|
+
|
|
14
|
+
爪印无线按钮传感器 (47L120300)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
安装
|
|
19
|
+
---
|
|
20
|
+
```bash
|
|
21
|
+
pip install DGLAB-controller-python
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
要求 Python 3.10+ 且 bleak ≥ 0.21。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
快速开始
|
|
28
|
+
---
|
|
29
|
+
灵猫 – 气压上报
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import asyncio
|
|
33
|
+
from dg_lab_devices import Civec
|
|
34
|
+
|
|
35
|
+
async def main():
|
|
36
|
+
async with Civec() as civec:
|
|
37
|
+
@civec.on_pressure
|
|
38
|
+
def on_pressure(kpa: float):
|
|
39
|
+
print(f"气压: {kpa:.2f} kPa")
|
|
40
|
+
|
|
41
|
+
await civec.start_pressure_report(color=0x02)
|
|
42
|
+
await asyncio.sleep(30)
|
|
43
|
+
|
|
44
|
+
asyncio.run(main())
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
负鼠 – 按键与强度
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from dg_lab_devices import Controller
|
|
51
|
+
|
|
52
|
+
async def main():
|
|
53
|
+
async with Controller() as ctrl:
|
|
54
|
+
@ctrl.on_button
|
|
55
|
+
async def on_button(seq, buttons):
|
|
56
|
+
if buttons.get("A"):
|
|
57
|
+
await ctrl.set_strength(a=160)
|
|
58
|
+
|
|
59
|
+
await ctrl.set_led_and_report(enable_button_report=True)
|
|
60
|
+
await asyncio.sleep(60)
|
|
61
|
+
|
|
62
|
+
asyncio.run(main())
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
爪印 – 触发模式
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from dg_lab_devices import PawPrints
|
|
69
|
+
|
|
70
|
+
async def main():
|
|
71
|
+
async with PawPrints() as pp:
|
|
72
|
+
@pp.on_trigger
|
|
73
|
+
def on_trigger(color, event_id, param):
|
|
74
|
+
print(f"触发事件 {event_id},参数={param}")
|
|
75
|
+
|
|
76
|
+
await pp.set_random_trigger(
|
|
77
|
+
color=0x01, event_id=5,
|
|
78
|
+
green_min=30, green_max=50,
|
|
79
|
+
reaction_time=10,
|
|
80
|
+
param_inc=20, param_speed=40,
|
|
81
|
+
param_dec=50, param_dec_speed=40
|
|
82
|
+
)
|
|
83
|
+
await asyncio.sleep(300)
|
|
84
|
+
|
|
85
|
+
asyncio.run(main())
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
主要特性
|
|
90
|
+
---
|
|
91
|
+
· **异步事件驱动** – 装饰器订阅设备通知
|
|
92
|
+
|
|
93
|
+
· **自动连接管理** – async with 自动连接/断开
|
|
94
|
+
|
|
95
|
+
· **多设备并发** – 同一事件循环同时运行多个设备
|
|
96
|
+
|
|
97
|
+
· **完整协议支持** – 实现所有 BLE 指令与回调
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
开源许可
|
|
101
|
+
---
|
|
102
|
+
本项目使用 MIT 许可证。
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
联系作者
|
|
106
|
+
---
|
|
107
|
+
. QQ群:870333220 <del>有香香软软的小南娘和技术大佬<del>
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
相关链接
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
. [参考文档](https://github.com/dungeonlab-open/dglab-bluetooth-protocol)
|
|
114
|
+
|
|
115
|
+
· [PyPI](https://pypi.org/project/DGLAB-controller-python/)
|
|
116
|
+
|
|
117
|
+
· [API文档](docs/API.md)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""设备基类:BLE连接、事件注册/发射、通用命令"""
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from bleak import BleakClient, BleakScanner
|
|
6
|
+
from bleak.exc import BleakError # 新增
|
|
7
|
+
from .common import CHAR_WRITE, CHAR_NOTIFY, CHAR_BATTERY, DeviceError
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
class BaseDevice(ABC):
|
|
12
|
+
"""所有DG-LAB蓝牙设备的抽象基类"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, address: str | None = None, name: str = ""):
|
|
15
|
+
self.address = address
|
|
16
|
+
self.name = name
|
|
17
|
+
self.client: BleakClient | None = None
|
|
18
|
+
self._event_handlers: dict[str, list] = {}
|
|
19
|
+
|
|
20
|
+
# ---------- 异步上下文管理器 ----------
|
|
21
|
+
async def __aenter__(self):
|
|
22
|
+
await self.connect()
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
26
|
+
await self.disconnect()
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
# ---------- 事件装饰器注册 ----------
|
|
30
|
+
def _register_event(self, event_name: str, func):
|
|
31
|
+
self._event_handlers.setdefault(event_name, []).append(func)
|
|
32
|
+
return func
|
|
33
|
+
|
|
34
|
+
async def _emit_event(self, event_name: str, *args):
|
|
35
|
+
for handler in self._event_handlers.get(event_name, []):
|
|
36
|
+
try:
|
|
37
|
+
if asyncio.iscoroutinefunction(handler):
|
|
38
|
+
await handler(*args)
|
|
39
|
+
else:
|
|
40
|
+
handler(*args)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.error(f"事件 {event_name} 回调异常: {e}")
|
|
43
|
+
|
|
44
|
+
# ---------- 连接管理 ----------
|
|
45
|
+
async def connect(self) -> None:
|
|
46
|
+
try:
|
|
47
|
+
if self.address is None:
|
|
48
|
+
device = await BleakScanner.find_device_by_name(self.name, timeout=5.0)
|
|
49
|
+
if device is None:
|
|
50
|
+
raise DeviceError(f"未找到设备: {self.name}")
|
|
51
|
+
self.address = device.address
|
|
52
|
+
self.client = BleakClient(self.address)
|
|
53
|
+
await self.client.connect()
|
|
54
|
+
except BleakError as e:
|
|
55
|
+
msg = str(e).lower()
|
|
56
|
+
if any(keyword in msg for keyword in ("bluetooth", "adapter")) and \
|
|
57
|
+
any(keyword in msg for keyword in ("off", "disabled", "not available", "not found")):
|
|
58
|
+
raise DeviceError("蓝牙未开启,请打开蓝牙后重试") from e
|
|
59
|
+
raise DeviceError(f"连接失败: {e}") from e
|
|
60
|
+
except Exception as e:
|
|
61
|
+
raise DeviceError(f"设备连接异常: {e}") from e
|
|
62
|
+
|
|
63
|
+
# 连接成功后启动通知
|
|
64
|
+
await self.client.start_notify(CHAR_NOTIFY, self._on_main_notify)
|
|
65
|
+
try:
|
|
66
|
+
await self.client.start_notify(CHAR_BATTERY, self._on_battery_notify)
|
|
67
|
+
except Exception:
|
|
68
|
+
logger.debug("设备不支持电量通知,使用主动读取")
|
|
69
|
+
logger.info(f"已连接 {self.name} [{self.address}]")
|
|
70
|
+
|
|
71
|
+
async def disconnect(self) -> None:
|
|
72
|
+
if self.client and self.client.is_connected:
|
|
73
|
+
await self.client.disconnect()
|
|
74
|
+
logger.info(f"已断开 {self.name}")
|
|
75
|
+
self.client = None
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
async def _on_main_notify(self, sender, data: bytearray):
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
async def _on_battery_notify(self, sender, data: bytearray):
|
|
82
|
+
if data:
|
|
83
|
+
await self._emit_event("battery", data[0])
|
|
84
|
+
|
|
85
|
+
async def _write_command(self, cmd: bytes):
|
|
86
|
+
if not self.client or not self.client.is_connected:
|
|
87
|
+
raise DeviceError("设备未连接")
|
|
88
|
+
await self.client.write_gatt_char(CHAR_WRITE, cmd, response=False)
|
|
89
|
+
|
|
90
|
+
async def get_battery(self) -> int:
|
|
91
|
+
if not self.client:
|
|
92
|
+
raise DeviceError("未连接")
|
|
93
|
+
data = await self.client.read_gatt_char(CHAR_BATTERY)
|
|
94
|
+
return data[0] if data else 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""灵猫边缘控制传感器 (Civec)"""
|
|
2
|
+
from .base import BaseDevice
|
|
3
|
+
import struct
|
|
4
|
+
|
|
5
|
+
class Civec(BaseDevice):
|
|
6
|
+
def __init__(self, address: str = None, name: str = "47L124000"):
|
|
7
|
+
super().__init__(address, name)
|
|
8
|
+
|
|
9
|
+
def on_pressure(self, func):
|
|
10
|
+
return self._register_event("pressure", func)
|
|
11
|
+
|
|
12
|
+
def on_battery(self, func):
|
|
13
|
+
return self._register_event("battery", func)
|
|
14
|
+
|
|
15
|
+
async def _on_main_notify(self, sender, data: bytearray):
|
|
16
|
+
if len(data) >= 17 and data[0] == 0xD0:
|
|
17
|
+
raw = struct.unpack_from('<h', data, 8)[0]
|
|
18
|
+
await self._emit_event("pressure", raw / 100.0)
|
|
19
|
+
|
|
20
|
+
async def start_pressure_report(self, color: int = 0x01):
|
|
21
|
+
await self._write_command(bytes([0x50, color, 0xD0]) + b'\x00' * 14)
|
|
22
|
+
|
|
23
|
+
async def stop_pressure_report(self, color: int = 0x01):
|
|
24
|
+
await self._write_command(bytes([0x50, color, 0x00]) + b'\x00' * 14)
|
|
25
|
+
|
|
26
|
+
async def reset_pressure(self):
|
|
27
|
+
await self._write_command(bytes([0x66]) + b'\x00' * 9 + b'\x00\x02\x00')
|
|
28
|
+
|
|
29
|
+
async def flip_screen(self, current_rotate: int = 1) -> int:
|
|
30
|
+
new_rotate = 0x03 if current_rotate == 1 else 0x01
|
|
31
|
+
cmd = bytes([0x66]) + b'\x00' * 9 + bytes([new_rotate, 0x00, 0x00])
|
|
32
|
+
await self._write_command(cmd)
|
|
33
|
+
return new_rotate
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""公共常量与工具"""
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
_BASE_UUID_SUFFIX = "-0000-1000-8000-00805f9b34fb"
|
|
5
|
+
|
|
6
|
+
def uuid16(short_id: int) -> uuid.UUID:
|
|
7
|
+
"""将16位短ID转换为完整的128位蓝牙UUID"""
|
|
8
|
+
return uuid.UUID(f"0000{short_id:04x}{_BASE_UUID_SUFFIX}")
|
|
9
|
+
|
|
10
|
+
# 标准蓝牙服务/特征
|
|
11
|
+
SERVICE_DEVICE_INFO = uuid16(0x180A)
|
|
12
|
+
CHAR_BATTERY = uuid16(0x1500)
|
|
13
|
+
SERVICE_MAIN = uuid16(0x180C)
|
|
14
|
+
CHAR_WRITE = uuid16(0x150A)
|
|
15
|
+
CHAR_NOTIFY = uuid16(0x150B)
|
|
16
|
+
|
|
17
|
+
# 颜色常量
|
|
18
|
+
COLORS = {
|
|
19
|
+
0x00: "熄灭",
|
|
20
|
+
0x01: "黄色",
|
|
21
|
+
0x02: "红色",
|
|
22
|
+
0x03: "紫色",
|
|
23
|
+
0x04: "蓝色",
|
|
24
|
+
0x05: "青色",
|
|
25
|
+
0x06: "绿色",
|
|
26
|
+
0x07: "白色",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class DeviceError(Exception):
|
|
30
|
+
"""设备相关异常"""
|
|
31
|
+
pass
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""负鼠振动控制器 (Controller)"""
|
|
2
|
+
from .base import BaseDevice
|
|
3
|
+
|
|
4
|
+
class Controller(BaseDevice):
|
|
5
|
+
def __init__(self, address: str = None, name: str = "47L127000"):
|
|
6
|
+
super().__init__(address, name)
|
|
7
|
+
self._button_map = [
|
|
8
|
+
"SEL_1", "SEL_2", "HOME", None, None, None, None, None,
|
|
9
|
+
"Up", "Down", "Left", "Right", "B", "A", "G", "D"
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
def on_button(self, func):
|
|
13
|
+
return self._register_event("button", func)
|
|
14
|
+
|
|
15
|
+
def on_strength_change(self, func):
|
|
16
|
+
return self._register_event("strength", func)
|
|
17
|
+
|
|
18
|
+
def on_battery(self, func):
|
|
19
|
+
return self._register_event("battery", func)
|
|
20
|
+
|
|
21
|
+
async def _on_main_notify(self, sender, data: bytearray):
|
|
22
|
+
if len(data) >= 3 and data[0] == 0xB3:
|
|
23
|
+
await self._emit_event("strength", data[1], data[2])
|
|
24
|
+
elif len(data) >= 4 and data[0] == 0xD0:
|
|
25
|
+
seq = data[1]
|
|
26
|
+
state = (data[2] << 8) | data[3]
|
|
27
|
+
buttons = {}
|
|
28
|
+
for i, name in enumerate(self._button_map):
|
|
29
|
+
if name:
|
|
30
|
+
buttons[name] = bool(state & (1 << (15 - i)))
|
|
31
|
+
await self._emit_event("button", seq, buttons)
|
|
32
|
+
|
|
33
|
+
async def set_led_and_report(self, color: int = 0x01, enable_button_report: bool = True):
|
|
34
|
+
mode = 0x01 if enable_button_report else 0x00
|
|
35
|
+
await self._write_command(bytes([0x50, color, mode]))
|
|
36
|
+
|
|
37
|
+
async def set_waveform(self, a_wave: list[int], b_wave: list[int]):
|
|
38
|
+
if len(a_wave) != 4 or len(b_wave) != 4:
|
|
39
|
+
raise ValueError("波形数据必须为4字节")
|
|
40
|
+
for v in a_wave + b_wave:
|
|
41
|
+
if not (0 <= v <= 100):
|
|
42
|
+
raise ValueError("波形强度超出0-100")
|
|
43
|
+
cmd = bytes([0xB0]) + b'\x00' * 7 + bytes(a_wave) + b'\x00' * 4 + bytes(b_wave)
|
|
44
|
+
await self._write_command(cmd)
|
|
45
|
+
|
|
46
|
+
async def set_strength(self, a: int | None = None, b: int | None = None):
|
|
47
|
+
def _byte(v):
|
|
48
|
+
return 0xFF if v is None else v
|
|
49
|
+
cmd = bytes([0xB3, _byte(a), _byte(b)])
|
|
50
|
+
await self._write_command(cmd)
|
|
51
|
+
# 同步屏幕显示
|
|
52
|
+
await self._sync_screen(a, b)
|
|
53
|
+
|
|
54
|
+
async def _sync_screen(self, a: int | None, b: int | None):
|
|
55
|
+
prefix = bytes.fromhex("FFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0809")
|
|
56
|
+
a_byte = a if a is not None else 0xFF
|
|
57
|
+
b_byte = b if b is not None else 0xFF
|
|
58
|
+
cmd = bytes([0xB2]) + prefix + bytes([a_byte, b_byte])
|
|
59
|
+
await self._write_command(cmd)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""爪印无线按钮传感器 V1.1 (PawPrints)"""
|
|
2
|
+
import struct
|
|
3
|
+
from .base import BaseDevice
|
|
4
|
+
|
|
5
|
+
class PawPrints(BaseDevice):
|
|
6
|
+
def __init__(self, address: str = None, name: str = "47L120300"):
|
|
7
|
+
super().__init__(address, name)
|
|
8
|
+
|
|
9
|
+
def on_status(self, func):
|
|
10
|
+
return self._register_event("status", func)
|
|
11
|
+
|
|
12
|
+
def on_trigger(self, func):
|
|
13
|
+
return self._register_event("trigger", func)
|
|
14
|
+
|
|
15
|
+
def on_untrack(self, func):
|
|
16
|
+
return self._register_event("untrack", func)
|
|
17
|
+
|
|
18
|
+
def on_param_change(self, func):
|
|
19
|
+
return self._register_event("param_change", func)
|
|
20
|
+
|
|
21
|
+
def on_physical(self, func):
|
|
22
|
+
return self._register_event("physical", func)
|
|
23
|
+
|
|
24
|
+
def on_auto_detect(self, func):
|
|
25
|
+
return self._register_event("auto_detect", func)
|
|
26
|
+
|
|
27
|
+
async def _on_main_notify(self, sender, data: bytearray):
|
|
28
|
+
head = data[0]
|
|
29
|
+
if head == 0x51 and len(data) >= 4:
|
|
30
|
+
await self._emit_event("status", data[1], data[2], data[3])
|
|
31
|
+
elif head == 0x5A and len(data) >= 4:
|
|
32
|
+
await self._emit_event("trigger", data[1], data[2], data[3])
|
|
33
|
+
elif head == 0x5B and len(data) >= 3:
|
|
34
|
+
await self._emit_event("untrack", data[1], data[2])
|
|
35
|
+
elif head == 0x5C and len(data) >= 4:
|
|
36
|
+
await self._emit_event("param_change", data[1], data[2], data[3])
|
|
37
|
+
elif head == 0xD0 and len(data) >= 9:
|
|
38
|
+
await self._emit_event("physical", data[1], data[2], data[3],
|
|
39
|
+
data[4], data[5], data[6], data[7], data[8])
|
|
40
|
+
elif head == 0xF1 and len(data) >= 14:
|
|
41
|
+
x_range = struct.unpack_from('<hh', data, 2)
|
|
42
|
+
y_range = struct.unpack_from('<hh', data, 6)
|
|
43
|
+
z_range = struct.unpack_from('<hh', data, 10)
|
|
44
|
+
await self._emit_event("auto_detect", x_range, y_range, z_range)
|
|
45
|
+
|
|
46
|
+
async def _write_50(self, payload: bytes):
|
|
47
|
+
await self._write_command(bytes([0x50]) + payload)
|
|
48
|
+
|
|
49
|
+
async def set_trigger_mode_none(self, color: int = 0x01):
|
|
50
|
+
await self._write_50(bytes([color, 0x00]) + b'\x00' * 14)
|
|
51
|
+
|
|
52
|
+
async def set_random_trigger(self, color: int, event_id: int,
|
|
53
|
+
green_min: int, green_max: int,
|
|
54
|
+
reaction_time: int,
|
|
55
|
+
param_inc: int, param_speed: int,
|
|
56
|
+
param_dec: int, param_dec_speed: int):
|
|
57
|
+
payload = struct.pack('<BBHHHHBBB', color, 0x03, event_id,
|
|
58
|
+
green_min, green_max, reaction_time,
|
|
59
|
+
param_inc, param_speed, param_dec, param_dec_speed)
|
|
60
|
+
payload += b'\x00' * 3
|
|
61
|
+
await self._write_command(bytes([0x50]) + payload)
|
|
62
|
+
|
|
63
|
+
async def set_probability_trigger(self, color: int, events_probs: list[tuple[int, int]], cooldown: int):
|
|
64
|
+
data = bytearray([0x50, color, 0x04])
|
|
65
|
+
for i in range(6):
|
|
66
|
+
eid, prob = events_probs[i] if i < len(events_probs) else (0, 0)
|
|
67
|
+
data.append(eid)
|
|
68
|
+
data.append(prob)
|
|
69
|
+
data.append((cooldown >> 8) & 0xFF)
|
|
70
|
+
data.append(cooldown & 0xFF)
|
|
71
|
+
await self._write_command(bytes(data))
|
|
72
|
+
|
|
73
|
+
async def set_external_voltage_trigger(self, color: int, event_id: int,
|
|
74
|
+
pull_up: int, vol_min: int, vol_max: int, map_range: int):
|
|
75
|
+
payload = bytes([color, 0x0F, event_id, pull_up, vol_min, vol_max, map_range]) + b'\x00' * 9
|
|
76
|
+
await self._write_50(payload)
|
|
77
|
+
|
|
78
|
+
async def set_press_motion_trigger(self, color: int, press_event_id: int,
|
|
79
|
+
settings_byte: int, press_inc_speed: int,
|
|
80
|
+
press_dec_val: int, press_dec_speed: int,
|
|
81
|
+
press_inc_val: int, motion_event_id: int,
|
|
82
|
+
motion_config: bytes, map_range: int):
|
|
83
|
+
payload = bytes([color, 0x05, press_event_id, settings_byte,
|
|
84
|
+
press_inc_speed, press_dec_val, press_dec_speed, press_inc_val,
|
|
85
|
+
motion_event_id]) + motion_config + bytes([map_range])
|
|
86
|
+
await self._write_50(payload)
|
|
87
|
+
|
|
88
|
+
async def set_physical_data_mode(self, color: int = 0x01):
|
|
89
|
+
await self._write_50(bytes([color, 0xD0]) + b'\x00' * 14)
|
|
90
|
+
|
|
91
|
+
async def reset_param(self):
|
|
92
|
+
await self._write_command(b'\x5F')
|
|
93
|
+
|
|
94
|
+
async def auto_detect_angle(self):
|
|
95
|
+
await self._write_command(b'\x60')
|
|
96
|
+
|
|
97
|
+
async def set_led(self, color: int):
|
|
98
|
+
await self._write_command(bytes([0x70, color]))
|
|
99
|
+
|
|
100
|
+
async def set_led_blink(self, color1: int, color2: int, speed: int):
|
|
101
|
+
await self._write_command(bytes([0x70, color1, color2, speed]))
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dglab-controller-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 用于控制负鼠振动控制器以及读取灵猫和爪印传感器的 Python 库
|
|
5
|
+
Author-email: lindog114514 <kaikaihe467@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lindog114514/DGLAB-controller-python
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: bleak>=0.21.0
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
## DGLAB-controller-python
|
|
23
|
+
|
|
24
|
+
DG-LAB 蓝牙设备的 Python SDK,基于 bleak。
|
|
25
|
+
支持同时连接多个设备、异步事件处理,并通过 async with 自动管理连接。
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
支持的设备
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
灵猫边缘控制传感器(47L124000)
|
|
32
|
+
|
|
33
|
+
负鼠振动控制器 (47L127000)
|
|
34
|
+
|
|
35
|
+
爪印无线按钮传感器 (47L120300)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
安装
|
|
40
|
+
---
|
|
41
|
+
```bash
|
|
42
|
+
pip install DGLAB-controller-python
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
要求 Python 3.10+ 且 bleak ≥ 0.21。
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
快速开始
|
|
49
|
+
---
|
|
50
|
+
灵猫 – 气压上报
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import asyncio
|
|
54
|
+
from dg_lab_devices import Civec
|
|
55
|
+
|
|
56
|
+
async def main():
|
|
57
|
+
async with Civec() as civec:
|
|
58
|
+
@civec.on_pressure
|
|
59
|
+
def on_pressure(kpa: float):
|
|
60
|
+
print(f"气压: {kpa:.2f} kPa")
|
|
61
|
+
|
|
62
|
+
await civec.start_pressure_report(color=0x02)
|
|
63
|
+
await asyncio.sleep(30)
|
|
64
|
+
|
|
65
|
+
asyncio.run(main())
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
负鼠 – 按键与强度
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from dg_lab_devices import Controller
|
|
72
|
+
|
|
73
|
+
async def main():
|
|
74
|
+
async with Controller() as ctrl:
|
|
75
|
+
@ctrl.on_button
|
|
76
|
+
async def on_button(seq, buttons):
|
|
77
|
+
if buttons.get("A"):
|
|
78
|
+
await ctrl.set_strength(a=160)
|
|
79
|
+
|
|
80
|
+
await ctrl.set_led_and_report(enable_button_report=True)
|
|
81
|
+
await asyncio.sleep(60)
|
|
82
|
+
|
|
83
|
+
asyncio.run(main())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
爪印 – 触发模式
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from dg_lab_devices import PawPrints
|
|
90
|
+
|
|
91
|
+
async def main():
|
|
92
|
+
async with PawPrints() as pp:
|
|
93
|
+
@pp.on_trigger
|
|
94
|
+
def on_trigger(color, event_id, param):
|
|
95
|
+
print(f"触发事件 {event_id},参数={param}")
|
|
96
|
+
|
|
97
|
+
await pp.set_random_trigger(
|
|
98
|
+
color=0x01, event_id=5,
|
|
99
|
+
green_min=30, green_max=50,
|
|
100
|
+
reaction_time=10,
|
|
101
|
+
param_inc=20, param_speed=40,
|
|
102
|
+
param_dec=50, param_dec_speed=40
|
|
103
|
+
)
|
|
104
|
+
await asyncio.sleep(300)
|
|
105
|
+
|
|
106
|
+
asyncio.run(main())
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
主要特性
|
|
111
|
+
---
|
|
112
|
+
· **异步事件驱动** – 装饰器订阅设备通知
|
|
113
|
+
|
|
114
|
+
· **自动连接管理** – async with 自动连接/断开
|
|
115
|
+
|
|
116
|
+
· **多设备并发** – 同一事件循环同时运行多个设备
|
|
117
|
+
|
|
118
|
+
· **完整协议支持** – 实现所有 BLE 指令与回调
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
开源许可
|
|
122
|
+
---
|
|
123
|
+
本项目使用 MIT 许可证。
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
联系作者
|
|
127
|
+
---
|
|
128
|
+
. QQ群:870333220 <del>有香香软软的小南娘和技术大佬<del>
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
相关链接
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
. [参考文档](https://github.com/dungeonlab-open/dglab-bluetooth-protocol)
|
|
135
|
+
|
|
136
|
+
· [PyPI](https://pypi.org/project/DGLAB-controller-python/)
|
|
137
|
+
|
|
138
|
+
· [API文档](docs/API.md)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
dg_lab_devices/__init__.py
|
|
5
|
+
dg_lab_devices/base.py
|
|
6
|
+
dg_lab_devices/civec.py
|
|
7
|
+
dg_lab_devices/common.py
|
|
8
|
+
dg_lab_devices/controller.py
|
|
9
|
+
dg_lab_devices/pawprints.py
|
|
10
|
+
dglab_controller_python.egg-info/PKG-INFO
|
|
11
|
+
dglab_controller_python.egg-info/SOURCES.txt
|
|
12
|
+
dglab_controller_python.egg-info/dependency_links.txt
|
|
13
|
+
dglab_controller_python.egg-info/requires.txt
|
|
14
|
+
dglab_controller_python.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bleak>=0.21.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dg_lab_devices
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dglab-controller-python"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "用于控制负鼠振动控制器以及读取灵猫和爪印传感器的 Python 库"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{name = "lindog114514", email = "kaikaihe467@gmail.com"}]
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"bleak>=0.21.0",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/lindog114514/DGLAB-controller-python"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
include = ["dg_lab_devices*"]
|