difonlib 0.2.3__tar.gz → 0.2.4__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.
- {difonlib-0.2.3 → difonlib-0.2.4}/PKG-INFO +5 -4
- {difonlib-0.2.3 → difonlib-0.2.4}/pyproject.toml +8 -4
- difonlib-0.2.4/src/difonlib/bt_scanner.py +277 -0
- {difonlib-0.2.3 → difonlib-0.2.4}/src/difonlib/bt_utils.py +128 -23
- {difonlib-0.2.3 → difonlib-0.2.4}/src/difonlib/input_devs.py +1 -0
- {difonlib-0.2.3 → difonlib-0.2.4}/src/difonlib/utils.py +20 -8
- {difonlib-0.2.3 → difonlib-0.2.4}/src/difonlib.egg-info/PKG-INFO +5 -4
- {difonlib-0.2.3 → difonlib-0.2.4}/src/difonlib.egg-info/SOURCES.txt +1 -0
- difonlib-0.2.4/src/difonlib.egg-info/requires.txt +10 -0
- difonlib-0.2.3/src/difonlib.egg-info/requires.txt +0 -9
- {difonlib-0.2.3 → difonlib-0.2.4}/README.md +0 -0
- {difonlib-0.2.3 → difonlib-0.2.4}/setup.cfg +0 -0
- {difonlib-0.2.3 → difonlib-0.2.4}/src/difonlib/__init__.py +0 -0
- {difonlib-0.2.3 → difonlib-0.2.4}/src/difonlib/ng_lib.py +0 -0
- {difonlib-0.2.3 → difonlib-0.2.4}/src/difonlib/py.typed +0 -0
- {difonlib-0.2.3 → difonlib-0.2.4}/src/difonlib/remctrl.py +0 -0
- {difonlib-0.2.3 → difonlib-0.2.4}/src/difonlib/tuya_devs.py +0 -0
- {difonlib-0.2.3 → difonlib-0.2.4}/src/difonlib.egg-info/dependency_links.txt +0 -0
- {difonlib-0.2.3 → difonlib-0.2.4}/src/difonlib.egg-info/top_level.txt +0 -0
- {difonlib-0.2.3 → difonlib-0.2.4}/tests/test_bt_utils.py +0 -0
- {difonlib-0.2.3 → difonlib-0.2.4}/tests/test_utils.py +0 -0
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: difonlib
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: python libraries
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: bleak>=3.0.1
|
|
7
8
|
Requires-Dist: evdev>=1.9.2
|
|
9
|
+
Requires-Dist: hid>=1.0.9
|
|
8
10
|
Requires-Dist: nicegui>=3.2.0
|
|
9
11
|
Requires-Dist: pexpect>=4.9.0
|
|
12
|
+
Requires-Dist: pydbus>=0.6.0
|
|
13
|
+
Requires-Dist: pygobject>=3.56.2
|
|
10
14
|
Requires-Dist: pyyaml>=6.0.3
|
|
11
15
|
Requires-Dist: tinytuya>=1.17.4
|
|
12
|
-
Requires-Dist: types-pexpect>=4.9.0.20250916
|
|
13
|
-
Requires-Dist: types-PyYAML>=6.0.12.20250915
|
|
14
|
-
Requires-Dist: types-xmltodict>=1.0.1.20250920
|
|
15
16
|
Requires-Dist: xmltodict>=1.0.2
|
|
16
17
|
|
|
17
18
|
# Python libraries
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "difonlib"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "python libraries"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
dependencies = [
|
|
8
|
+
"bleak>=3.0.1",
|
|
8
9
|
"evdev>=1.9.2",
|
|
10
|
+
"hid>=1.0.9",
|
|
9
11
|
"nicegui>=3.2.0",
|
|
10
12
|
"pexpect>=4.9.0",
|
|
13
|
+
"pydbus>=0.6.0",
|
|
14
|
+
"pygobject>=3.56.2",
|
|
11
15
|
"pyyaml>=6.0.3",
|
|
12
16
|
"tinytuya>=1.17.4",
|
|
13
|
-
"types-pexpect>=4.9.0.20250916",
|
|
14
|
-
"types-PyYAML>=6.0.12.20250915",
|
|
15
|
-
"types-xmltodict>=1.0.1.20250920",
|
|
16
17
|
"xmltodict>=1.0.2",
|
|
17
18
|
]
|
|
18
19
|
|
|
@@ -47,6 +48,9 @@ dev = [
|
|
|
47
48
|
"black>=24.8.0",
|
|
48
49
|
"mypy>=1.11.0",
|
|
49
50
|
"twine>=5.1.1",
|
|
51
|
+
"types-pexpect>=4.9.0.20250916",
|
|
52
|
+
"types-PyYAML>=6.0.12.20250915",
|
|
53
|
+
"types-xmltodict>=1.0.1.20250920",
|
|
50
54
|
]
|
|
51
55
|
|
|
52
56
|
[tool.ruff]
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bluetooth HID Device Scanner
|
|
3
|
+
Requires: pip install bleak hid
|
|
4
|
+
On Linux may also need: sudo apt install libhidapi-hidraw0 libhidapi-libusb0
|
|
5
|
+
Run with: python bt_hid_scanner.py
|
|
6
|
+
https://claude.ai/share/54cf3394-15f2-4ca9-8730-b70bf2973646
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import hid
|
|
11
|
+
import pydbus
|
|
12
|
+
from bleak import BleakScanner
|
|
13
|
+
from bleak.backends.device import BLEDevice
|
|
14
|
+
from bleak.backends.scanner import AdvertisementData
|
|
15
|
+
from ctypes import LittleEndianStructure, c_uint32
|
|
16
|
+
|
|
17
|
+
# HID Usage Page 0x01 = Generic Desktop
|
|
18
|
+
# HID Usages: 0x02=Mouse, 0x04=Joystick, 0x05=Gamepad, 0x06=Keyboard, 0x08=Multi-axis
|
|
19
|
+
HID_USAGE_NAMES = {
|
|
20
|
+
0x01: "Pointer",
|
|
21
|
+
0x02: "Mouse",
|
|
22
|
+
0x04: "Joystick",
|
|
23
|
+
0x05: "Gamepad",
|
|
24
|
+
0x06: "Keyboard",
|
|
25
|
+
0x07: "Keypad",
|
|
26
|
+
0x08: "Multi-axis Controller",
|
|
27
|
+
0x09: "Tablet PC Controls",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Bluetooth HID Service UUID
|
|
32
|
+
HID_SERVICE_UUID = "00001812-0000-1000-8000-00805f9b34fb"
|
|
33
|
+
|
|
34
|
+
MAJOR_CLASSES = {
|
|
35
|
+
0x00: "Miscellaneous",
|
|
36
|
+
0x01: "Computer",
|
|
37
|
+
0x02: "Phone",
|
|
38
|
+
0x03: "LAN/Network Access Point",
|
|
39
|
+
0x04: "Audio/Video",
|
|
40
|
+
0x05: "HID Device",
|
|
41
|
+
0x06: "Imaging",
|
|
42
|
+
0x07: "Wearable",
|
|
43
|
+
0x08: "Toy",
|
|
44
|
+
0x09: "Health",
|
|
45
|
+
0x1F: "Uncategorized",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# === Class of Device (CoD)===
|
|
50
|
+
class CoD(LittleEndianStructure):
|
|
51
|
+
_fields_ = [
|
|
52
|
+
("FormatType", c_uint32, 2), # 2 бита
|
|
53
|
+
("MinorDevClass", c_uint32, 6), # 6 бит
|
|
54
|
+
("MajorDevClass", c_uint32, 5), # 5 бит
|
|
55
|
+
("ServiceClass", c_uint32, 11), # 11 бит
|
|
56
|
+
("Reserved", c_uint32, 8), # оставшиеся до 32
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def bt_parse_cod(cod_val: int) -> str:
|
|
61
|
+
"""Разобрать Class of Device (CoD) в словарь с описанием"""
|
|
62
|
+
# cod32 = c_uint32(int(cod_val, 0))
|
|
63
|
+
cod32 = c_uint32(cod_val)
|
|
64
|
+
fields = CoD.from_buffer_copy(cod32)
|
|
65
|
+
major = fields.MajorDevClass
|
|
66
|
+
return MAJOR_CLASSES.get(major, f"0x{major:02X}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def adapter_restart(adapter_path: str = "/org/bluez/hci0", duration: float = 0.5) -> None:
|
|
70
|
+
bus = pydbus.SystemBus()
|
|
71
|
+
adapter = bus.get("org.bluez", adapter_path)
|
|
72
|
+
adapter.StartDiscovery()
|
|
73
|
+
await asyncio.sleep(duration)
|
|
74
|
+
try:
|
|
75
|
+
adapter.StopDiscovery()
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
await asyncio.sleep(0.5)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ─── USB HID Devices (connected right now) ────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def scan_usb_hid_devices() -> list[dict]:
|
|
85
|
+
"""Return a list of currently connected USB HID devices."""
|
|
86
|
+
devices = []
|
|
87
|
+
seen = set()
|
|
88
|
+
|
|
89
|
+
for info in hid.enumerate():
|
|
90
|
+
key = (info["vendor_id"], info["product_id"], info["usage_page"], info["usage"])
|
|
91
|
+
if key in seen:
|
|
92
|
+
continue
|
|
93
|
+
seen.add(key)
|
|
94
|
+
|
|
95
|
+
devices.append(
|
|
96
|
+
{
|
|
97
|
+
"name": info.get("product_string") or "Unknown",
|
|
98
|
+
"manufacturer": info.get("manufacturer_string") or "Unknown",
|
|
99
|
+
"vid": f"0x{info['vendor_id']:04X}",
|
|
100
|
+
"pid": f"0x{info['product_id']:04X}",
|
|
101
|
+
"usage_page": f"0x{info['usage_page']:04X}",
|
|
102
|
+
"usage": HID_USAGE_NAMES.get(info["usage"], f"0x{info['usage']:04X}"),
|
|
103
|
+
"serial": info.get("serial_number") or "—",
|
|
104
|
+
"path": info.get("path", b"").decode(errors="replace"),
|
|
105
|
+
"interface": info.get("interface_number", -1),
|
|
106
|
+
"transport": "USB / HID",
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return devices
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ─── Bluetooth LE HID Devices (nearby) ────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class BluetoothScanner:
|
|
117
|
+
def __init__(self, duration: float = 5.0, dev_type: str = ""):
|
|
118
|
+
self.duration = duration
|
|
119
|
+
self.found: dict[str, dict] = {} # addr -> device info
|
|
120
|
+
self.dev_type = dev_type
|
|
121
|
+
self.available_types = [v for v in MAJOR_CLASSES.values()]
|
|
122
|
+
|
|
123
|
+
def _on_device(self, device: BLEDevice, adv: AdvertisementData) -> None:
|
|
124
|
+
# print(f"device: {device}")
|
|
125
|
+
# print(f"Class: {device.details['Class']}")
|
|
126
|
+
# print(f"adv: {adv}")
|
|
127
|
+
service_uuids = [u.lower() for u in (adv.service_uuids or [])]
|
|
128
|
+
ble_hid_dev = HID_SERVICE_UUID in service_uuids
|
|
129
|
+
dclass = device.details.get("props", {}).get("Class")
|
|
130
|
+
# print(f" = DETAILS: {device.details}")
|
|
131
|
+
dev_class = None
|
|
132
|
+
if ble_hid_dev:
|
|
133
|
+
dev_class = "HID Device"
|
|
134
|
+
elif dclass:
|
|
135
|
+
dev_class = bt_parse_cod(dclass)
|
|
136
|
+
|
|
137
|
+
addr = device.address
|
|
138
|
+
if addr in self.found:
|
|
139
|
+
# update RSSI
|
|
140
|
+
self.found[addr]["rssi"] = adv.rssi
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
self.found[addr] = {
|
|
144
|
+
"name": device.name or "Unknown",
|
|
145
|
+
"address": addr,
|
|
146
|
+
"dev_class": dev_class,
|
|
147
|
+
"rssi": adv.rssi,
|
|
148
|
+
"services": service_uuids,
|
|
149
|
+
"tx_power": adv.tx_power,
|
|
150
|
+
}
|
|
151
|
+
# print(
|
|
152
|
+
# f" [+] Found: {device.name or 'Unknown':30s} {addr} RSSI {adv.rssi} dBm"
|
|
153
|
+
# )
|
|
154
|
+
# print(f" service_uuids: {service_uuids}")
|
|
155
|
+
|
|
156
|
+
def _filter(self, devs: list[dict], dev_type: str) -> list[dict]:
|
|
157
|
+
_devs = []
|
|
158
|
+
for dev in devs:
|
|
159
|
+
if dev["dev_class"] == dev_type:
|
|
160
|
+
_devs.append(dev)
|
|
161
|
+
return _devs
|
|
162
|
+
|
|
163
|
+
async def run(self) -> list[dict]:
|
|
164
|
+
print(f" Scanning BLE for {self.duration}s ...")
|
|
165
|
+
async with BleakScanner(detection_callback=self._on_device):
|
|
166
|
+
await asyncio.sleep(self.duration)
|
|
167
|
+
if self.dev_type in self.available_types:
|
|
168
|
+
return self._filter(list(self.found.values()), self.dev_type)
|
|
169
|
+
elif self.dev_type != "":
|
|
170
|
+
print(" =!= Device Type ERROR")
|
|
171
|
+
print(f" Type '{self.dev_type}' is not available.")
|
|
172
|
+
print(f" Available types: {self.available_types}")
|
|
173
|
+
return list(self.found.values())
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# def get_bt_hid_devs(devs:list[dict]) -> list[dict]:
|
|
177
|
+
# hid_devs = []
|
|
178
|
+
# for dev in devs:
|
|
179
|
+
# if HID_SERVICE_UUID in dev["services"] or dev['dev_class'] == "HID Device":
|
|
180
|
+
# hid_devs.append(dev)
|
|
181
|
+
# return hid_devs
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ─── Pretty printing ───────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def print_separator(char: str = "─", width: int = 60) -> None:
|
|
188
|
+
print(char * width)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def print_usb_devices(devices: list[dict]) -> None:
|
|
192
|
+
print_separator("═")
|
|
193
|
+
print(f" USB HID DEVICES ({len(devices)} found)")
|
|
194
|
+
print_separator("═")
|
|
195
|
+
|
|
196
|
+
if not devices:
|
|
197
|
+
print(" No USB HID devices found.\n")
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
for i, d in enumerate(devices, 1):
|
|
201
|
+
print(f"\n [{i}] {d['name']}")
|
|
202
|
+
print(f" Manufacturer : {d['manufacturer']}")
|
|
203
|
+
print(f" VID / PID : {d['vid']} / {d['pid']}")
|
|
204
|
+
print(f" Usage : {d['usage']} (page {d['usage_page']})")
|
|
205
|
+
print(f" Serial : {d['serial']}")
|
|
206
|
+
print(f" Interface : {d['interface']}")
|
|
207
|
+
print(f" Path : {d['path']}")
|
|
208
|
+
|
|
209
|
+
print()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def print_bt_devices(devices: list[dict], type: str = "DEVICES") -> None:
|
|
213
|
+
print_separator("═")
|
|
214
|
+
print(f" BLUETOOTH AVAILABLE {type} ({len(devices)} found)")
|
|
215
|
+
print_separator("═")
|
|
216
|
+
|
|
217
|
+
if not devices:
|
|
218
|
+
print(" No devices found.\n")
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
for i, d in enumerate(devices, 1):
|
|
222
|
+
tx = f"{d['tx_power']} dBm" if d["tx_power"] is not None else "—"
|
|
223
|
+
print(f"\n [{i}] {d['name']}")
|
|
224
|
+
print(f" Address : {d['address']}")
|
|
225
|
+
print(f" Services : {d['services']}")
|
|
226
|
+
print(f" Device Class : {d['dev_class']}")
|
|
227
|
+
print(f" RSSI : {d['rssi']} dBm")
|
|
228
|
+
print(f" TX Power : {tx}")
|
|
229
|
+
# print(f" Transport : {d['transport']}")
|
|
230
|
+
|
|
231
|
+
print()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ─── Main ──────────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
async def main() -> None:
|
|
238
|
+
print("\n╔══════════════════════════════════════════════════════════╗")
|
|
239
|
+
print("║ Bluetooth Device Scanner ║")
|
|
240
|
+
print("╚══════════════════════════════════════════════════════════╝\n")
|
|
241
|
+
|
|
242
|
+
# # 1. USB HID
|
|
243
|
+
# print("Scanning USB HID devices ...")
|
|
244
|
+
# usb_devices = scan_usb_hid_devices()
|
|
245
|
+
# print_usb_devices(usb_devices)
|
|
246
|
+
|
|
247
|
+
# # Сначала — inquiry, чтобы BlueZ узнал о Classic устройствах
|
|
248
|
+
# print("Triggering Classic BT inquiry via bluetoothctl...")
|
|
249
|
+
# await trigger_classic_inquiry(duration=2)
|
|
250
|
+
|
|
251
|
+
await adapter_restart()
|
|
252
|
+
|
|
253
|
+
# 2. BLE HID
|
|
254
|
+
try:
|
|
255
|
+
# bt_scanner = BluetoothScanner(duration=6.0, dev_type="Audio/Video")
|
|
256
|
+
dev_type = "HID Device"
|
|
257
|
+
# dev_type = ""
|
|
258
|
+
bt_scanner = BluetoothScanner(duration=6.0, dev_type=dev_type)
|
|
259
|
+
bt_devices = await bt_scanner.run()
|
|
260
|
+
print_bt_devices(bt_devices, type=dev_type)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
print(f" Bluetooth device scan failed: {e}")
|
|
263
|
+
print(" Make sure Bluetooth is enabled and bleak is installed.\n")
|
|
264
|
+
bt_devices = []
|
|
265
|
+
|
|
266
|
+
# # 3. Summary
|
|
267
|
+
# print_separator("═")
|
|
268
|
+
# total = len(usb_devices) + len(ble_devices)
|
|
269
|
+
# print(
|
|
270
|
+
# f" SUMMARY: {len(usb_devices)} USB HID + {len(ble_devices)} BLE HID = {total} total devices"
|
|
271
|
+
# )
|
|
272
|
+
# print_separator("═")
|
|
273
|
+
# print(f"bt_devices: {bt_devices}")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
if __name__ == "__main__":
|
|
277
|
+
asyncio.run(main())
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
3
5
|
import os
|
|
4
6
|
import re
|
|
5
7
|
import pexpect
|
|
6
|
-
import sys
|
|
7
|
-
import time
|
|
8
8
|
from typing import Optional
|
|
9
9
|
from ctypes import LittleEndianStructure, c_uint32
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
10
13
|
from difonlib.utils import logdbg
|
|
11
14
|
from difonlib.input_devs import get_connected_input_devices
|
|
12
15
|
|
|
16
|
+
import asyncio
|
|
17
|
+
import pydbus
|
|
18
|
+
from bleak import BleakScanner
|
|
19
|
+
from bleak.backends.device import BLEDevice
|
|
20
|
+
from bleak.backends.scanner import AdvertisementData
|
|
13
21
|
|
|
14
22
|
dbg = logdbg
|
|
23
|
+
# dbg = print
|
|
15
24
|
|
|
16
25
|
MAJOR_CLASSES = {
|
|
17
26
|
0x00: "Miscellaneous",
|
|
@@ -39,15 +48,81 @@ class CoD(LittleEndianStructure):
|
|
|
39
48
|
]
|
|
40
49
|
|
|
41
50
|
|
|
42
|
-
def bt_parse_cod(cod_val:
|
|
51
|
+
def bt_parse_cod(cod_val: Any) -> str:
|
|
43
52
|
"""Разобрать Class of Device (CoD) в словарь с описанием"""
|
|
44
|
-
cod32 = c_uint32(int(cod_val
|
|
53
|
+
cod32 = c_uint32(int(cod_val))
|
|
45
54
|
fields = CoD.from_buffer_copy(cod32)
|
|
46
55
|
major = fields.MajorDevClass
|
|
47
56
|
return MAJOR_CLASSES.get(major, f"0x{major:02X}")
|
|
48
57
|
|
|
49
58
|
|
|
50
|
-
|
|
59
|
+
# Bluetooth HID Service UUID
|
|
60
|
+
HID_SERVICE_UUID = "00001812-0000-1000-8000-00805f9b34fb"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BluetoothScanner:
|
|
64
|
+
def __init__(self, duration: float = 5.0, dev_type: str = ""):
|
|
65
|
+
self.duration = duration
|
|
66
|
+
self.found: dict[str, dict] = {} # addr -> device info
|
|
67
|
+
self.dev_type = dev_type
|
|
68
|
+
self.available_types = [v for v in MAJOR_CLASSES.values()]
|
|
69
|
+
|
|
70
|
+
def _on_device(self, device: BLEDevice, adv: AdvertisementData) -> None:
|
|
71
|
+
# print(f"device: {device}")
|
|
72
|
+
# print(f"Class: {device.details['Class']}")
|
|
73
|
+
# print(f"adv: {adv}")
|
|
74
|
+
service_uuids = [u.lower() for u in (adv.service_uuids or [])]
|
|
75
|
+
ble_hid_dev = HID_SERVICE_UUID in service_uuids
|
|
76
|
+
dclass = device.details.get("props", {}).get("Class")
|
|
77
|
+
# print(f" = DETAILS: {device.details}")
|
|
78
|
+
dev_class = None
|
|
79
|
+
if ble_hid_dev:
|
|
80
|
+
dev_class = "HID Device"
|
|
81
|
+
elif dclass:
|
|
82
|
+
dev_class = bt_parse_cod(dclass)
|
|
83
|
+
|
|
84
|
+
addr = device.address
|
|
85
|
+
if addr in self.found:
|
|
86
|
+
# update RSSI
|
|
87
|
+
self.found[addr]["rssi"] = adv.rssi
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
self.found[addr] = {
|
|
91
|
+
"name": device.name or "Unknown",
|
|
92
|
+
"address": addr,
|
|
93
|
+
"dev_class": dev_class,
|
|
94
|
+
"rssi": adv.rssi,
|
|
95
|
+
"services": service_uuids,
|
|
96
|
+
"tx_power": adv.tx_power,
|
|
97
|
+
}
|
|
98
|
+
print(".", end="", flush=True)
|
|
99
|
+
# print(
|
|
100
|
+
# f" [+] Found: {device.name or 'Unknown':30s} {addr} RSSI {adv.rssi} dBm"
|
|
101
|
+
# )
|
|
102
|
+
# print(f" service_uuids: {service_uuids}")
|
|
103
|
+
|
|
104
|
+
def _filter(self, devs: list[dict], dev_type: str) -> list[dict]:
|
|
105
|
+
_devs = []
|
|
106
|
+
for dev in devs:
|
|
107
|
+
if dev["dev_class"] == dev_type:
|
|
108
|
+
_devs.append(dev)
|
|
109
|
+
return _devs
|
|
110
|
+
|
|
111
|
+
async def run(self) -> list[dict]:
|
|
112
|
+
print(f" Scanning for bluetooth devices {self.duration}s ...", end="")
|
|
113
|
+
async with BleakScanner(detection_callback=self._on_device):
|
|
114
|
+
await asyncio.sleep(self.duration)
|
|
115
|
+
print(" END\n")
|
|
116
|
+
if self.dev_type in self.available_types:
|
|
117
|
+
return self._filter(list(self.found.values()), self.dev_type)
|
|
118
|
+
elif self.dev_type != "":
|
|
119
|
+
print(" =!= Device Type ERROR")
|
|
120
|
+
print(f" Type '{self.dev_type}' is not available.")
|
|
121
|
+
print(f" Available types: {self.available_types}")
|
|
122
|
+
return list(self.found.values())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def bt_scan_hcitool_inq() -> list:
|
|
51
126
|
# px = pexpect.spawn("hcitool scan", encoding="utf-8")
|
|
52
127
|
devs = []
|
|
53
128
|
# inquery remote devices
|
|
@@ -68,16 +143,7 @@ def bt_scan() -> list:
|
|
|
68
143
|
return devs
|
|
69
144
|
|
|
70
145
|
|
|
71
|
-
def
|
|
72
|
-
devs = bt_scan()
|
|
73
|
-
hid_devs = []
|
|
74
|
-
for dev in devs:
|
|
75
|
-
if dev["class"] == "HID Device":
|
|
76
|
-
hid_devs.append(dev)
|
|
77
|
-
return hid_devs
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def bt_scan2() -> list:
|
|
146
|
+
def bt_scan_hcitool_scan() -> list:
|
|
81
147
|
# px = pexpect.spawn("hcitool scan", encoding="utf-8")
|
|
82
148
|
devs = []
|
|
83
149
|
_devs = os.popen("hcitool scan").readlines()
|
|
@@ -95,6 +161,40 @@ def bt_scan2() -> list:
|
|
|
95
161
|
return devs
|
|
96
162
|
|
|
97
163
|
|
|
164
|
+
def bt_scan_hid_devs() -> list:
|
|
165
|
+
devs = bt_scan_hcitool_inq()
|
|
166
|
+
hid_devs = []
|
|
167
|
+
for dev in devs:
|
|
168
|
+
if dev["class"] == "HID Device":
|
|
169
|
+
hid_devs.append(dev)
|
|
170
|
+
return hid_devs
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def bt_scan_devs( # universally version for classic and ble devices
|
|
174
|
+
dev_type: str = "HID Device",
|
|
175
|
+
inquiry_warmup: float = 4.0, # время для Classic Inquiry
|
|
176
|
+
scan_duration: float = 6.0, # время bleak сканирования
|
|
177
|
+
adapter_path: str = "/org/bluez/hci0",
|
|
178
|
+
) -> list[dict]:
|
|
179
|
+
bus = pydbus.SystemBus()
|
|
180
|
+
adapter = bus.get("org.bluez", adapter_path)
|
|
181
|
+
adapter.SetDiscoveryFilter({})
|
|
182
|
+
adapter.StartDiscovery()
|
|
183
|
+
await asyncio.sleep(inquiry_warmup)
|
|
184
|
+
try:
|
|
185
|
+
bt_scanner = BluetoothScanner(duration=scan_duration, dev_type=dev_type)
|
|
186
|
+
bt_devices = await bt_scanner.run()
|
|
187
|
+
except Exception as e:
|
|
188
|
+
print(f" Bluetooth scan failed: {e}")
|
|
189
|
+
bt_devices = []
|
|
190
|
+
finally:
|
|
191
|
+
try:
|
|
192
|
+
adapter.StopDiscovery()
|
|
193
|
+
except Exception as e:
|
|
194
|
+
print(f" StopDiscovery() error: {e}")
|
|
195
|
+
return bt_devices
|
|
196
|
+
|
|
197
|
+
|
|
98
198
|
def btctl_add(mac_address: str, timeout: int = 10) -> Optional[dict]:
|
|
99
199
|
"""
|
|
100
200
|
Подключает HID или любое Bluetooth устройство по MAC через bluetoothctl.
|
|
@@ -319,18 +419,23 @@ def bt_hid_conn_devs() -> list:
|
|
|
319
419
|
|
|
320
420
|
if __name__ == "__main__":
|
|
321
421
|
|
|
322
|
-
dev_mac = "41:42:68:D8:DA:39"
|
|
422
|
+
# dev_mac = "41:42:68:D8:DA:39"
|
|
423
|
+
|
|
424
|
+
# from difonlib.utils import print_dicts_list
|
|
323
425
|
|
|
324
|
-
|
|
426
|
+
# print(" ======== ALL connected devices ==========")
|
|
427
|
+
# all_connected_devs = get_connected_input_devices()
|
|
428
|
+
# print_dicts_list(all_connected_devs)
|
|
325
429
|
|
|
326
|
-
print(" ========
|
|
327
|
-
|
|
328
|
-
print_dicts_list(
|
|
430
|
+
# print(" ======== HID connected devices ==========")
|
|
431
|
+
# conn_devs = bt_hid_conn_devs()
|
|
432
|
+
# print_dicts_list(conn_devs)
|
|
329
433
|
|
|
330
|
-
|
|
331
|
-
conn_devs = bt_hid_conn_devs()
|
|
332
|
-
print_dicts_list(conn_devs)
|
|
434
|
+
# input("ssssssssssssssssssss")
|
|
333
435
|
|
|
436
|
+
# devs = asyncio.run(bt_scan_devs(dev_type=""))
|
|
437
|
+
devs = asyncio.run(bt_scan_devs())
|
|
438
|
+
print(f"devs: {devs}")
|
|
334
439
|
# available_bt_devs = bt_scan()
|
|
335
440
|
# print_lists(available_bt_devs) # //Dima
|
|
336
441
|
|
|
@@ -39,11 +39,23 @@ MSG_COLOR = f"\x1b[{RESET};38;5;{45}m"
|
|
|
39
39
|
# DEBUG 10
|
|
40
40
|
# NOTSET 0
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
|
|
43
|
+
# Глобальные логгеры библиотек — заглушить
|
|
44
|
+
logging.getLogger().setLevel(logging.WARNING) # root logger
|
|
45
|
+
|
|
46
|
+
# Свой именованный логгер
|
|
47
|
+
logger = logging.getLogger("DifonLibLogger")
|
|
48
|
+
logger.setLevel(logging.DEBUG)
|
|
49
|
+
|
|
50
|
+
# Handler только на свой логгер
|
|
51
|
+
handler = logging.StreamHandler()
|
|
52
|
+
handler.setFormatter(
|
|
53
|
+
logging.Formatter(f"{MSG_COLOR}[%(filename)s:%(lineno)d]: %(message)s{COLOR_OFF}")
|
|
45
54
|
)
|
|
46
|
-
|
|
55
|
+
logger.addHandler(handler)
|
|
56
|
+
logger.propagate = False # не пускать в root logger
|
|
57
|
+
|
|
58
|
+
logdbg = logger.debug
|
|
47
59
|
|
|
48
60
|
|
|
49
61
|
class UtilsError(Exception):
|
|
@@ -182,14 +194,14 @@ class YamlConfig:
|
|
|
182
194
|
def load(self) -> None:
|
|
183
195
|
if not self.config_path.exists():
|
|
184
196
|
self.save()
|
|
185
|
-
with self.config_path.open() as
|
|
186
|
-
self.config = yaml.safe_load(
|
|
197
|
+
with self.config_path.open() as fcfg:
|
|
198
|
+
self.config = yaml.safe_load(fcfg) or {}
|
|
187
199
|
|
|
188
200
|
def save(self) -> None:
|
|
189
|
-
with self.config_path.open("w") as
|
|
201
|
+
with self.config_path.open("w") as fcfg:
|
|
190
202
|
yaml.safe_dump(
|
|
191
203
|
self.config,
|
|
192
|
-
|
|
204
|
+
fcfg,
|
|
193
205
|
sort_keys=False,
|
|
194
206
|
allow_unicode=True,
|
|
195
207
|
)
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: difonlib
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: python libraries
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: bleak>=3.0.1
|
|
7
8
|
Requires-Dist: evdev>=1.9.2
|
|
9
|
+
Requires-Dist: hid>=1.0.9
|
|
8
10
|
Requires-Dist: nicegui>=3.2.0
|
|
9
11
|
Requires-Dist: pexpect>=4.9.0
|
|
12
|
+
Requires-Dist: pydbus>=0.6.0
|
|
13
|
+
Requires-Dist: pygobject>=3.56.2
|
|
10
14
|
Requires-Dist: pyyaml>=6.0.3
|
|
11
15
|
Requires-Dist: tinytuya>=1.17.4
|
|
12
|
-
Requires-Dist: types-pexpect>=4.9.0.20250916
|
|
13
|
-
Requires-Dist: types-PyYAML>=6.0.12.20250915
|
|
14
|
-
Requires-Dist: types-xmltodict>=1.0.1.20250920
|
|
15
16
|
Requires-Dist: xmltodict>=1.0.2
|
|
16
17
|
|
|
17
18
|
# Python libraries
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|