mirror of
https://github.com/morgan9e/UxPlay
synced 2026-04-14 00:04:13 +09:00
260 lines
9.2 KiB
Python
260 lines
9.2 KiB
Python
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
# adapted from https://github.com/bluez/bluez/blob/master/test/example-advertisement
|
|
#----------------------------------------------------------------
|
|
# HCI_Linux (uses sudo hciconfig): module for a standalone python-3.6 or later AirPlay Service-Discovery Bluetooth LE beacon for UxPlay
|
|
|
|
# this requires that users can run "sudo -n hciconfig" without giving a password:
|
|
# (1) (as root) create a group like "hciusers"
|
|
# (2a) Linux: use visudo to create a file /etc/sudoers.d/hciusers containing a line
|
|
# %hciusers ALL=(ALL) NOPASSWD: /usr/bin/hcitool, /usr/bin/hciconfig
|
|
# (2b) FreeBSD: use visudo to create /usr/local/etc/sudoers.d/hciusers with the line
|
|
# %hciusers ALL=(ALL) NOPASSWD: /usr/sbin/hccontrol
|
|
# (3) add the users who will run uxplay-beacon.py to the group hciusers
|
|
|
|
|
|
import subprocess
|
|
import time
|
|
import re
|
|
import subprocess
|
|
import platform
|
|
from typing import Optional
|
|
from typing import Literal
|
|
|
|
#global variables
|
|
hci = None
|
|
advertised_port = None
|
|
advertised_address = None
|
|
|
|
os_name = platform.system()
|
|
linux = os_name == 'Linux'
|
|
freebsd = os_name == 'FreeBSD'
|
|
if not linux and not freebsd:
|
|
print(f'{os_name} is not supported by the HCI module')
|
|
raise SystemExit(1)
|
|
|
|
#help text
|
|
help_text1 = '''
|
|
This HCI module requires users of the module to have elevated privileges that
|
|
allow execution of a low-level Bluetooth HCI command using passwordless "sudo":
|
|
(1) As System Administrator, create a group such as "hciusers")
|
|
'''
|
|
if linux:
|
|
help_text2 = '''
|
|
(2) use visudo to create a file /etc/sudoers.d/hciusers containing the line:
|
|
%hciusers ALL=(ALL) NOPASSWD: /usr/bin/hciconfig, /usr/bin/hcitool
|
|
'''
|
|
elif freebsd:
|
|
disclaimer = '''
|
|
|
|
***********************************************************************
|
|
* FreeBSD: this module currently requires a patch to FreeBSD's *
|
|
* hccontrol utility, that will hopefully be accepted into the FreeBSD *
|
|
* source tree. It is available at the UxPlay github site Wiki: *
|
|
* https://github.com/FDH2/UxPlay/wiki/hccontrol-patch-for-FreeBSD-15.0*
|
|
***********************************************************************
|
|
wget https://github.com/user-attachments/files/26074904/hccontrol_FreeBSD_15_0_patch.txt
|
|
|
|
'''
|
|
print(disclaimer)
|
|
|
|
help_text2 = '''
|
|
(2) use visudo to create a file /usr/local/etc/sudoers.d/hciusers containing the line:
|
|
%hciusers ALL=(ALL) NOPASSWD: /usr/sbin/hccontrol
|
|
'''
|
|
help_text3 = '''
|
|
(3) add users of uxplay_beacon_module_HCI.py to the group "hciusers"
|
|
'''
|
|
help_text = help_text1 + help_text2 + help_text3
|
|
|
|
sudo = ['sudo', '-n']
|
|
if linux:
|
|
ogf = "0x08"
|
|
def le_cmd(hcicmd, args):
|
|
cmd = sudo + ['hcitool', '-i', hci, 'cmd', ogf, hcicmd] + args
|
|
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
elif freebsd:
|
|
def le_cmd(hcicmd, args):
|
|
cmd = sudo + ['hccontrol', '-n', hci, hcicmd] + args
|
|
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
|
|
def setup_beacon(ipv4_str: str, port: int, advmin: int, advmax: int, index: Literal[None]) -> bool:
|
|
global advertised_port
|
|
global advertised_address
|
|
advertised_port = None
|
|
advertised_address = None
|
|
|
|
# setup Advertising Parameters
|
|
if linux:
|
|
# convert into units of 5/8 msec.
|
|
advmin = (advmin * 8) // 5
|
|
advmax = (advmax * 8) // 5
|
|
min1 = f'{advmin %256 :#04x}'
|
|
min2 = f'{advmin //256 :#04x}'
|
|
max1 = f'{advmax % 256 :#04x}'
|
|
max2 = f'{advmax // 256 :#04x}'
|
|
args = [min1, min2, max1, max2, '0x03', '0x00', '0x00'] + ['0x00'] * 6 + ['0x07', '0x00']
|
|
hcicmd = "0x0006"
|
|
elif freebsd:
|
|
min = f'-m {advmin}'
|
|
max = f'-M {advmax}'
|
|
args = [min, max, 't = 3']
|
|
hcicmd = 'le_set_advertising_param'
|
|
try:
|
|
result = le_cmd(hcicmd, args)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f'beacon_on error (set_advertisng_parameters):', e.stderr, e.stdout)
|
|
return False
|
|
|
|
# setup Advertising Data
|
|
adv_head = ['0xff', '0x4c', '0x00', '0x09', '0x08', '0x13', '0x30']
|
|
adv_int = [int(hex_str, 16) for hex_str in adv_head]
|
|
ip = list(map(int, ipv4_str.split('.')))
|
|
prt = [port // 256, port % 256]
|
|
adv_int = adv_int + ip + prt
|
|
|
|
if linux:
|
|
adv_len = len(adv_int)
|
|
adv_int = [adv_len + 1, adv_len ] + adv_int
|
|
args = [f'{i:#04x}' for i in adv_int]
|
|
args += ['0x00'] * (31 - len(adv_int))
|
|
hcicmd = '0x0008'
|
|
elif freebsd:
|
|
adv = ','.join(f'{byte:02x}' for byte in adv_int)
|
|
args = ['-b', adv]
|
|
hcicmd = 'le_set_advertising_data'
|
|
try:
|
|
le_cmd(hcicmd, args)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f'beacon_on error (set_advertisng_parameters):', e.stderr, e.stdout)
|
|
return False
|
|
advertised_port = port
|
|
advertised_address = ipv4_str
|
|
return True
|
|
|
|
def beacon_on() -> Optional[int]:
|
|
if linux:
|
|
hcicmd = '0x000a'
|
|
args = ['0x01']
|
|
elif freebsd:
|
|
hcicmd = 'le_set_advertising_enable'
|
|
args = ['enable']
|
|
try:
|
|
le_cmd(hcicmd, args)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f'beacon_on error:', e.stderr, e.stdout)
|
|
global advertised_port
|
|
global advertised_address
|
|
advertised_port = None
|
|
advertised_address = None
|
|
return None
|
|
print(f'AirPlay Service-Discovery beacon transmission started')
|
|
return advertised_port
|
|
|
|
def beacon_off():
|
|
if linux:
|
|
hcicmd = '0x000a'
|
|
args = ['0x00']
|
|
elif freebsd:
|
|
hcicmd = 'le_set_advertising_enable'
|
|
args = ['disable']
|
|
le_cmd(hcicmd, args)
|
|
print(f'AirPlay Service-Discovery beacon transmission ended')
|
|
advertised_address = None
|
|
advertised_port = None
|
|
|
|
LMP = ["1.0b","1.1", "1.2", "2.0+EDR", "2.1+EDR", "3.0+HS"]
|
|
LMP += ["4.0","4.1", "4.2", "5.0", "5.1", "5.2", "5.3", "5.4", "6.0", "6.1"]
|
|
def get_bluetooth_version(device_name):
|
|
if linux:
|
|
cmd ='hciconfig'
|
|
args = [cmd, device_name, '-a']
|
|
regexp = r"LMP Version: .*?\(0x([0-9a-fA-F])\)"
|
|
elif freebsd:
|
|
cmd = 'hccontrol'
|
|
args = [cmd, '-n', device_name, 'Read_Local_Version_Information']
|
|
regexp = r"LMP version: .*?\[(0x[0-9a-fA-F]+)\]"
|
|
try:
|
|
# Run hciconfig -a for the specific device
|
|
result = subprocess.check_output(args, stderr=subprocess.STDOUT, text=True)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Error running {cmd} for {device_name}: {e.output}")
|
|
return None
|
|
except FileNotFoundError:
|
|
print(f"Error: {cmd} command not found")
|
|
return None
|
|
# Regex to find "LMP Version: X.Y (0xZ)"
|
|
lmp_version_match = re.search(regexp, result)
|
|
if lmp_version_match:
|
|
version_hex = lmp_version_match.group(1)
|
|
return int(version_hex,16)
|
|
return None
|
|
|
|
def list_devices_by_version(min_version):
|
|
if linux:
|
|
cmd = 'hcitool'
|
|
args = [cmd]
|
|
args.append('dev')
|
|
regexp = r"(hci\d+)"
|
|
elif freebsd:
|
|
cmd = 'hccontrol'
|
|
args = [cmd]
|
|
args.append('Read_Node_List')
|
|
regexp = r"(^ubt\d+hci)"
|
|
try:
|
|
# Run hciconfig to list all devices
|
|
devices_list_output = subprocess.check_output(args, stderr=subprocess.STDOUT, text=True)
|
|
print(devices_list_output)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Error running {cmd}: {e.output}")
|
|
return None
|
|
except FileNotFoundError:
|
|
print(f"Error: {cmd} command not found")
|
|
return None
|
|
# Regex to find device names (e.g., hci0, hci1)
|
|
device_names = re.findall(regexp, devices_list_output, re.MULTILINE)
|
|
found_devices = []
|
|
for device_name in device_names:
|
|
version_decimal = get_bluetooth_version(device_name)
|
|
if version_decimal is None or version_decimal < min_version:
|
|
continue
|
|
bt_version = LMP[version_decimal]
|
|
device = [device_name, bt_version]
|
|
found_devices.append(device)
|
|
return found_devices
|
|
|
|
from typing import Optional
|
|
def find_device(hci_in: Optional[str]) -> Optional[str]:
|
|
global hci
|
|
list = list_devices_by_version(min_version=6)
|
|
if list is None or len(list) == 0:
|
|
return None
|
|
hci = None
|
|
if hci_in is not None:
|
|
for item in list:
|
|
if item[0] == hci_in:
|
|
hci = hci_in
|
|
return hci
|
|
count = 0
|
|
for index, item in enumerate(list, start = 1):
|
|
count += 1
|
|
print(f'=== detected HCI device {count}. {item[0]}: Bluetooth v{item[1]}')
|
|
if count == 1:
|
|
hci = item[0]
|
|
if count > 1:
|
|
print(f'warning: {count} HCI devices were found, the first found will be used')
|
|
print(f'(to override this choice, specify "--device=..." in optional arguments)')
|
|
if linux:
|
|
cmd = sudo + ['hciconfig', hci, 'reset']
|
|
elif freebsd:
|
|
cmd = sudo + ['hccontrol', '-n', hci, 'Reset']
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f'hci reset error:', e.stderr, e.stdout)
|
|
print(help_text)
|
|
print('cannot continue: SystemExit(1)')
|
|
raise SystemExit(1)
|
|
return hci
|
|
|
|
print('loaded uxplay_beacon_module_HCI')
|