difonlib 0.2.2a1__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.
difonlib/__init__.py ADDED
File without changes
difonlib/bt_utils.py ADDED
@@ -0,0 +1,553 @@
1
+ #!/usr/bin/env python
2
+
3
+ import os
4
+ import re
5
+ import pexpect
6
+ import sys
7
+ import time
8
+ from typing import Optional
9
+ from ctypes import LittleEndianStructure, c_uint32
10
+ from difonlib.utils import logdbg
11
+ from difonlib.input_devs import get_connected_input_devices
12
+
13
+
14
+ dbg = logdbg
15
+
16
+ MAJOR_CLASSES = {
17
+ 0x00: "Miscellaneous",
18
+ 0x01: "Computer",
19
+ 0x02: "Phone",
20
+ 0x03: "LAN/Network Access Point",
21
+ 0x04: "Audio/Video",
22
+ 0x05: "HID Device",
23
+ 0x06: "Imaging",
24
+ 0x07: "Wearable",
25
+ 0x08: "Toy",
26
+ 0x09: "Health",
27
+ 0x1F: "Uncategorized",
28
+ }
29
+
30
+
31
+ # === Class of Device (CoD)===
32
+ class CoD(LittleEndianStructure):
33
+ _fields_ = [
34
+ ("FormatType", c_uint32, 2), # 2 бита
35
+ ("MinorDevClass", c_uint32, 6), # 6 бит
36
+ ("MajorDevClass", c_uint32, 5), # 5 бит
37
+ ("ServiceClass", c_uint32, 11), # 11 бит
38
+ ("Reserved", c_uint32, 8), # оставшиеся до 32
39
+ ]
40
+
41
+
42
+ def bt_parse_cod(cod_val: str) -> str:
43
+ """Разобрать Class of Device (CoD) в словарь с описанием"""
44
+ cod32 = c_uint32(int(cod_val, 0))
45
+ fields = CoD.from_buffer_copy(cod32)
46
+ major = fields.MajorDevClass
47
+ return MAJOR_CLASSES.get(major, f"0x{major:02X}")
48
+
49
+
50
+ def bt_scan() -> list:
51
+ # px = pexpect.spawn("hcitool scan", encoding="utf-8")
52
+ devs = []
53
+ # inquery remote devices
54
+ scan_devs = os.popen("hcitool inq").readlines()
55
+ if len(scan_devs) > 1:
56
+ for _dev in scan_devs:
57
+ if "Inquiring" in _dev:
58
+ continue
59
+ # devs.append(re.sub("\t", " ", dev).strip())
60
+ # print(f" -- dev: '{dev.strip()}'")
61
+ m, _, c = _dev.strip().split("\t")
62
+ dev = dict()
63
+ dev["mac"] = m
64
+ dev["name"] = os.popen(f"hcitool name {m}").read().strip()
65
+ dev_class = c.split(":")[1].strip()
66
+ dev["class"] = bt_parse_cod(dev_class)
67
+ devs.append(dev)
68
+ return devs
69
+
70
+
71
+ def bt_scan_hid_devs() -> list:
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:
81
+ # px = pexpect.spawn("hcitool scan", encoding="utf-8")
82
+ devs = []
83
+ _devs = os.popen("hcitool scan").readlines()
84
+ if len(_devs) > 1:
85
+ for _dev in _devs:
86
+ if "Scanning" in _dev:
87
+ continue
88
+ # devs.append(re.sub("\t", " ", dev).strip())
89
+ # print(f" -- dev: '{dev.strip()}'")
90
+ mac, name = re.sub("\t", " ", _dev).strip().split(" ", 1)
91
+ dev = {}
92
+ dev["mac"] = mac
93
+ dev["name"] = name
94
+ devs.append(dev)
95
+ return devs
96
+
97
+
98
+ def btctl_add(mac_address: str, timeout: int = 10) -> Optional[dict]:
99
+ """
100
+ Подключает HID или любое Bluetooth устройство по MAC через bluetoothctl.
101
+ Возвращает dev_info, если подключение успешно, {} иначе.
102
+ """
103
+ # prompt = r"\[bluetoothctl\]>"
104
+ prompt = r"\[.*\][#>]"
105
+ dev_info = {}
106
+ response = ""
107
+ btctl = None
108
+ try:
109
+ btctl = pexpect.spawn("bluetoothctl", encoding="utf-8")
110
+ # btctl.logfile_read = sys.stdout
111
+ # btctl.logfile = open("bt_debug.log", "w") # весь вывод в файл
112
+ btctl.expect(prompt) # ждём приглашения
113
+
114
+ ### RESTART BT ADAPTER
115
+ btctl.sendline("power off")
116
+ btctl.expect("PowerState: off", timeout=3)
117
+ btctl.expect(prompt)
118
+ time.sleep(1)
119
+
120
+ btctl.sendline("power on")
121
+ # btctl.expect("PowerState: on", timeout=3)
122
+ btctl.expect("Powered: yes", timeout=3)
123
+ btctl.expect(prompt)
124
+ time.sleep(1)
125
+ dbg("Adapter restarted")
126
+
127
+ ### REMOVE DEVICE (RECONNECT)
128
+ btctl.sendline(f"remove {mac_address}")
129
+ btctl.expect(prompt, timeout=3)
130
+
131
+ btctl.sendline("scan on")
132
+ btctl.expect("Discovery started", timeout=3)
133
+ btctl.expect(prompt)
134
+ time.sleep(1)
135
+
136
+ dbg("Scan On")
137
+
138
+ responses = [
139
+ "Pairing successful",
140
+ "Connection successful",
141
+ # "Discovery started",
142
+ "Failed to start discovery",
143
+ f"Device {mac_address} not available",
144
+ "Failed to connect",
145
+ f"Attempting to pair with {mac_address}",
146
+ f"Changing {mac_address} trust succeeded",
147
+ ]
148
+
149
+ ### PAIRING
150
+ dbg(" === PAIRING ===")
151
+ while response != "Pairing successful":
152
+ btctl.sendline(f"pair {mac_address}")
153
+ btctl.expect(list(responses))
154
+ response = btctl.after
155
+ dbg(f"{response}")
156
+ time.sleep(1)
157
+ if response == f"Device {mac_address} not available":
158
+ timeout -= 1
159
+ if timeout == 0:
160
+ return {"Connected": response}
161
+
162
+ ### TRUST
163
+ dbg(" === TRUSTING ===")
164
+ while response != f"Changing {mac_address} trust succeeded":
165
+ btctl.sendline(f"trust {mac_address}")
166
+ btctl.expect(list(responses))
167
+ response = btctl.after
168
+ dbg(f" {response}")
169
+ time.sleep(1)
170
+
171
+ ### CONNECTION
172
+ dbg(" === CONNECTION ===")
173
+ while response != "Connection successful":
174
+ btctl.sendline(f"connect {mac_address}")
175
+ btctl.expect(list(responses))
176
+ response = btctl.after
177
+ dbg(f" {response}")
178
+ time.sleep(1)
179
+
180
+ ### SCAN OFF
181
+ btctl.sendline("scan off")
182
+ # btctl.expect("Discovery stopped", timeout=5)
183
+ btctl.expect(prompt, timeout=5)
184
+ dbg(" === Scan OFF ===") # //Dima
185
+ # time.sleep(0.1)
186
+ _info = ""
187
+ while "Connected" not in _info:
188
+ btctl.sendline(f"info {mac_address}")
189
+ btctl.expect(f"Device {mac_address}", timeout=5)
190
+ btctl.expect(prompt, timeout=5)
191
+ if btctl.before is None:
192
+ dbg("info........") # //Dima
193
+ time.sleep(1)
194
+ continue
195
+ _info = btctl.before.strip()
196
+
197
+ btctl.sendline("quit")
198
+ btctl.close()
199
+ # print("=== INFO OUTPUT ===")
200
+ # print(info)
201
+ # print("===================")
202
+ if not _info:
203
+ return None
204
+ info = _info.splitlines()
205
+ info[0] = "Device: " + mac_address
206
+ i = 0
207
+ for line in info:
208
+ if ":" in line:
209
+ k, v = line.split(":", 1)
210
+ field_name = k.strip()
211
+ if field_name in dev_info:
212
+ i += 1
213
+ field_name += f"-{i}"
214
+ dev_info[field_name] = v.strip()
215
+
216
+ except Exception as e:
217
+ print(f" =!= Error: {e}")
218
+ return None
219
+
220
+ return dev_info
221
+
222
+
223
+ def btctl_get_dev_info(mac_address: str, timeout: int = 5) -> Optional[dict]:
224
+ prompt = r"\[.*\][#>]"
225
+ dev_info = {}
226
+ try:
227
+ btctl = pexpect.spawn("bluetoothctl", encoding="utf-8")
228
+ # btctl.logfile_read = sys.stdout
229
+ # btctl.logfile = open("bt_debug.log", "w") # весь вывод в файл
230
+ btctl.expect(prompt) # ждём приглашения
231
+
232
+ dbg("------------") # //Dima
233
+
234
+ _info = ""
235
+ while "Connected" not in _info:
236
+ btctl.sendline(f"info {mac_address}")
237
+ btctl.expect(f"Device {mac_address}", timeout=5)
238
+ btctl.expect(prompt, timeout=5)
239
+ if btctl.before is None:
240
+ dbg("before") # //Dima
241
+ continue
242
+ timeout -= 1
243
+ if timeout == 0:
244
+ break
245
+ _info = btctl.before.strip()
246
+ dbg("info....") # //Dima
247
+ time.sleep(1)
248
+
249
+ btctl.sendline("quit")
250
+ btctl.close()
251
+ if timeout == 0:
252
+ return None
253
+
254
+ # print("=== INFO OUTPUT ===")
255
+ # print(info)
256
+ # print("===================")
257
+ if not _info:
258
+ return None
259
+ info = _info.splitlines()
260
+ info[0] = "Device: " + mac_address
261
+ i = 0
262
+ for line in info:
263
+ if ":" in line:
264
+ k, v = line.split(":", 1)
265
+ field_name = k.strip()
266
+ if field_name in dev_info:
267
+ i += 1
268
+ field_name += f"-{i}"
269
+ dev_info[field_name] = v.strip()
270
+
271
+ except Exception as e:
272
+ print(f" =!= Error: {e}")
273
+ return None
274
+ return dev_info
275
+
276
+
277
+ def btctl_dev_remove(mac_address: str) -> bool:
278
+ """Return True if no errors, False in other case"""
279
+ # prompt = r"\[bluetoothctl\]>"
280
+ prompt = r"\[.*\][#>]"
281
+ # dev_info = {}
282
+ # response = ""
283
+ try:
284
+ btctl = pexpect.spawn("bluetoothctl", encoding="utf-8")
285
+ # btctl.logfile_read = sys.stdout
286
+ # btctl.logfile = open("bt_debug.log", "w") # весь вывод в файл
287
+ btctl.expect(prompt) # ждём приглашения
288
+
289
+ ### REMOVE DEVICE (RECONNECT)
290
+ btctl.sendline(f"remove {mac_address}")
291
+ btctl.expect(prompt, timeout=3)
292
+ return True
293
+ except Exception as e:
294
+ print(f" =!= Error: {e}")
295
+ return False
296
+
297
+
298
+ def bt_hid_conn_devs() -> list:
299
+ """Return list of connected bluetooth HID devices"""
300
+ all_devs = get_connected_input_devices()
301
+ bt_hid_devs = []
302
+ for dev in all_devs:
303
+ if "/uhid/" not in dev["Sysfs"]:
304
+ continue
305
+ bt_dev = {}
306
+ bt_dev["name"] = dev["Name"]
307
+ bt_dev["mac"] = dev["Uniq"]
308
+ bt_dev["event"] = re.findall(r"event\d+", dev["Handlers"])[0]
309
+ # bt_dev["btns"] = dev["B"]["KEY"]
310
+ bt_hid_devs.append(bt_dev)
311
+ return bt_hid_devs
312
+
313
+
314
+ # # Пример использования:
315
+ # if __name__ == "__main__":
316
+ # mac = "XX:XX:XX:XX:XX:XX" # замени на MAC твоего устройства
317
+ # btctl_add(mac)
318
+
319
+
320
+ if __name__ == "__main__":
321
+
322
+ dev_mac = "41:42:68:D8:DA:39"
323
+
324
+ from difonlib.utils import print_dicts_list
325
+
326
+ print(" ======== ALL connected devices ==========")
327
+ all_connected_devs = get_connected_input_devices()
328
+ print_dicts_list(all_connected_devs)
329
+
330
+ print(" ======== HID connected devices ==========")
331
+ conn_devs = bt_hid_conn_devs()
332
+ print_dicts_list(conn_devs)
333
+
334
+ # available_bt_devs = bt_scan()
335
+ # print_lists(available_bt_devs) # //Dima
336
+
337
+ sys.exit(0)
338
+
339
+ # btctl_dev_remove(dev_mac)
340
+ # input(f" ====== After Remove {dev_mac}")
341
+ # devs = bt_scan()
342
+ # for dev in devs:
343
+ # print(f"dev: {dev}")
344
+ # input("+++++++++++++++++++++++++++++++++")
345
+ # sys.exit(1)
346
+
347
+ # dev = btctl_add(dev_mac, dbg=verbose)
348
+ # if not dev:
349
+ # sys.exit(1)
350
+ # dbg(f"--------------------------") # //Dima
351
+ # pprint(dev)
352
+ # dbg(f"--------------------------") # //Dima
353
+ # dev_status = dev.get("Connected")
354
+ # if dev_status == "yes":
355
+ # print(f"Device {dev['Device']} - connected!")
356
+ # else:
357
+ # print(f" =!= {dev_status}")
358
+
359
+ # dev_info = btctl_get_dev_info(dev_mac)
360
+ # if dev_info:
361
+ # pprint(dev_info)
362
+
363
+ # [dima@archryzen bluetooth_lib]$ ./bt_utils.py
364
+ # dev: 08:E9:F6:4E:99:9B n/a
365
+ # dev: D0:49:7C:DF:47:0E OnePlus Nord2 5G
366
+ # dev: 20:50:E7:58:02:6D n/a
367
+ # dev: 41:42:68:D8:DA:39 BT006
368
+ # dev: 64:57:25:7B:50:70 Smart TV Pro
369
+ # dev: 08:EB:ED:1F:AC:70 Mi Portable BT Speaker 16W
370
+
371
+
372
+ #############################################################################
373
+
374
+ #########################################################
375
+ ### Как определить, что HID device является клавиатурой??
376
+ #########################################################
377
+ # I: Bus=0003 Vendor=1e4e Product=0110 Version=0201
378
+ # N: Name="USB2.0 Camera: USB2.0 Camera"
379
+ # P: Phys=usb-0000:04:00.3-2/button
380
+ # S: Sysfs=/devices/pci0000:00/0000:00:08.1/0000:04:00.3/usb1/1-2/1-2:1.0/input/input20
381
+ # U: Uniq=
382
+ # H: Handlers=kbd event17
383
+ # B: PROP=0
384
+ # B: EV=3
385
+ # B: KEY=100000 0 0 0
386
+
387
+ #############################################################################
388
+ # dima@dima-mpd ~]$ cat /proc/bus/input/devices
389
+ # I: Bus=0019 Vendor=0000 Product=0001 Version=0000
390
+ # N: Name="Power Button"
391
+ # P: Phys=PNP0C0C/button/input0
392
+ # S: Sysfs=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0C:00/input/input0
393
+ # U: Uniq=
394
+ # H: Handlers=kbd event0
395
+ # B: PROP=0
396
+ # B: EV=3
397
+ # B: KEY=8000 10000000000000 0
398
+
399
+ # I: Bus=0019 Vendor=0000 Product=0001 Version=0000
400
+ # N: Name="Power Button"
401
+ # P: Phys=LNXPWRBN/button/input0
402
+ # S: Sysfs=/devices/LNXSYSTM:00/LNXPWRBN:00/input/input1
403
+ # U: Uniq=
404
+ # H: Handlers=kbd event1
405
+ # B: PROP=0
406
+ # B: EV=3
407
+ # B: KEY=8000 10000000000000 0
408
+
409
+ # I: Bus=0003 Vendor=8089 Product=0003 Version=0111
410
+ # N: Name="SayoDevice SayoDevice M3K RGB"
411
+ # P: Phys=usb-0000:00:15.0-4/input0
412
+ # S: Sysfs=/devices/pci0000:00/0000:00:15.0/usb1/1-4/1-4:1.0/0003:8089:0003.0002/input/input2
413
+ # U: Uniq=0061BC18586F
414
+ # H: Handlers=sysrq kbd leds event2
415
+ # B: PROP=0
416
+ # B: EV=120013
417
+ # B: KEY=1000000000007 ff9f207ac14057ff febeffdfffefffff fffffffffffffffe
418
+ # B: MSC=10
419
+ # B: LED=1f
420
+
421
+ # I: Bus=0019 Vendor=0000 Product=0006 Version=0000
422
+ # N: Name="Video Bus"
423
+ # P: Phys=LNXVIDEO/video/input0
424
+ # S: Sysfs=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/LNXVIDEO:00/input/input3
425
+ # U: Uniq=
426
+ # H: Handlers=kbd event3
427
+ # B: PROP=0
428
+ # B: EV=3
429
+ # B: KEY=3e000b00000000 0 0 0
430
+
431
+ # I: Bus=0003 Vendor=8089 Product=0003 Version=0111
432
+ # N: Name="SayoDevice SayoDevice M3K RGB Mouse"
433
+ # P: Phys=usb-0000:00:15.0-4/input1
434
+ # S: Sysfs=/devices/pci0000:00/0000:00:15.0/usb1/1-4/1-4:1.1/0003:8089:0003.0003/input/input5
435
+ # U: Uniq=0061BC18586F
436
+ # H: Handlers=event4 mouse0
437
+ # B: PROP=0
438
+ # B: EV=17
439
+ # B: KEY=ff0000 0 0 0 0
440
+ # B: REL=903
441
+ # B: MSC=10
442
+
443
+ # I: Bus=0003 Vendor=8089 Product=0003 Version=0111
444
+ # N: Name="SayoDevice SayoDevice M3K RGB Consumer Control"
445
+ # P: Phys=usb-0000:00:15.0-4/input1
446
+ # S: Sysfs=/devices/pci0000:00/0000:00:15.0/usb1/1-4/1-4:1.1/0003:8089:0003.0003/input/input6
447
+ # U: Uniq=0061BC18586F
448
+ # H: Handlers=kbd event5
449
+ # B: PROP=0
450
+ # B: EV=1f
451
+ # B: KEY=3f00033fff 0 0 483ffff17aff32d bfd4444600000000 1 130ff38b17d000 677bfad9415fed 19ed68000004400 10000002
452
+ # B: REL=1040
453
+ # B: ABS=100000000
454
+ # B: MSC=10
455
+
456
+ # I: Bus=0000 Vendor=0000 Product=0000 Version=0000
457
+ # N: Name="sof-essx8336 Headset"
458
+ # P: Phys=ALSA
459
+ # S: Sysfs=/devices/pci0000:00/0000:00:0e.0/sof-essx8336/sound/card1/input7
460
+ # U: Uniq=
461
+ # H: Handlers=kbd event6
462
+ # B: PROP=0
463
+ # B: EV=23
464
+ # B: KEY=1000000000 0 0
465
+ # B: SW=14
466
+
467
+ # I: Bus=0000 Vendor=0000 Product=0000 Version=0000
468
+ # N: Name="sof-essx8336 HDMI/DP,pcm=5"
469
+ # P: Phys=ALSA
470
+ # S: Sysfs=/devices/pci0000:00/0000:00:0e.0/sof-essx8336/sound/card1/input8
471
+ # U: Uniq=
472
+ # H: Handlers=event7
473
+ # B: PROP=0
474
+ # B: EV=21
475
+ # B: SW=140
476
+
477
+ # I: Bus=0000 Vendor=0000 Product=0000 Version=0000
478
+ # N: Name="sof-essx8336 HDMI/DP,pcm=6"
479
+ # P: Phys=ALSA
480
+ # S: Sysfs=/devices/pci0000:00/0000:00:0e.0/sof-essx8336/sound/card1/input9
481
+ # U: Uniq=
482
+ # H: Handlers=event8
483
+ # B: PROP=0
484
+ # B: EV=21
485
+ # B: SW=140
486
+
487
+ # I: Bus=0000 Vendor=0000 Product=0000 Version=0000
488
+ # N: Name="sof-essx8336 HDMI/DP,pcm=7"
489
+ # P: Phys=ALSA
490
+ # S: Sysfs=/devices/pci0000:00/0000:00:0e.0/sof-essx8336/sound/card1/input10
491
+ # U: Uniq=
492
+ # H: Handlers=event9
493
+ # B: PROP=0
494
+ # B: EV=21
495
+ # B: SW=140
496
+
497
+ # I: Bus=0005 Vendor=05ac Product=022c Version=011b
498
+ # N: Name="MINI_KEYBOARD"
499
+ # P: Phys=34:6f:24:62:0b:0e
500
+ # S: Sysfs=/devices/virtual/misc/uhid/0005:05AC:022C.0004/input/input11
501
+ # U: Uniq=97:ec:92:2e:2c:e0
502
+ # H: Handlers=sysrq kbd leds event10 mouse1
503
+ # B: PROP=0
504
+ # B: EV=12001f
505
+ # B: KEY=3f00033fff 0 0 483ffff17aff32d bfd4444600000000 70001 130ff38b17d000 677bfad9415fed e19effdf01cfffff fffffffffffffffe
506
+ # B: REL=1943
507
+ # B: ABS=100000000
508
+ # B: MSC=10
509
+ # B: LED=1f
510
+
511
+ # I: Bus=0005 Vendor=05ac Product=022c Version=011b
512
+ # N: Name="MINI_KEYBOARD"
513
+ # P: Phys=34:6f:24:62:0b:0e
514
+ # S: Sysfs=/devices/virtual/misc/uhid/0005:05AC:022C.0005/input/input12
515
+ # U: Uniq=2d:bb:bc:fa:10:f5
516
+ # H: Handlers=sysrq kbd leds event11 mouse2
517
+ # B: PROP=0
518
+ # B: EV=12001f
519
+ # B: KEY=3f00033fff 0 0 483ffff17aff32d bfd4444600000000 70001 130ff38b17d000 677bfad9415fed e19effdf01cfffff fffffffffffffffe
520
+ # B: REL=1943
521
+ # B: ABS=100000000
522
+ # B: MSC=10
523
+ # B: LED=1f
524
+
525
+ # I: Bus=0005 Vendor=05ac Product=022c Version=011b
526
+ # N: Name="MINI_KEYBOARD"
527
+ # P: Phys=34:6f:24:62:0b:0e
528
+ # S: Sysfs=/devices/virtual/misc/uhid/0005:05AC:022C.0006/input/input13
529
+ # U: Uniq=8a:bd:f7:49:11:6f
530
+ # H: Handlers=sysrq kbd leds event12 mouse3
531
+ # B: PROP=0
532
+ # B: EV=12001f
533
+ # B: KEY=3f00033fff 0 0 483ffff17aff32d bfd4444600000000 70001 130ff38b17d000 677bfad9415fed e19effdf01cfffff fffffffffffffffe
534
+ # B: REL=1943
535
+ # B: ABS=100000000
536
+ # B: MSC=10
537
+ # B: LED=1f
538
+
539
+ # I: Bus=0005 Vendor=05ac Product=022c Version=011b
540
+ # N: Name="MINI_KEYBOARD"
541
+ # P: Phys=34:6f:24:62:0b:0e
542
+ # S: Sysfs=/devices/virtual/misc/uhid/0005:05AC:022C.0007/input/input14
543
+ # U: Uniq=b9:4b:6d:31:bb:97
544
+ # H: Handlers=sysrq kbd leds event13 mouse4
545
+ # B: PROP=0
546
+ # B: EV=12001f
547
+ # B: KEY=3f00033fff 0 0 483ffff17aff32d bfd4444600000000 70001 130ff38b17d000 677bfad9415fed e19effdf01cfffff fffffffffffffffe
548
+ # B: REL=1943
549
+ # B: ABS=100000000
550
+ # B: MSC=10
551
+ # B: LED=1f
552
+
553
+ # [dima@dima-mpd ~]$