updated uxplay-beacon python files

This commit is contained in:
F. Duncanh
2026-03-19 03:51:53 -04:00
parent 3d71c13136
commit 3b0b0dfe23
5 changed files with 205 additions and 149 deletions

View File

@@ -76,10 +76,15 @@ def start_beacon():
setup_beacon(ipv4_str, port, advmin, advmax, index) setup_beacon(ipv4_str, port, advmin, advmax, index)
advertised_port = beacon_on() advertised_port = beacon_on()
beacon_is_running = advertised_port is not None beacon_is_running = advertised_port is not None
if not beacon_is_running: count = 1
print(f'second attempt to start beacon:') while not beacon_is_running:
print(f'Failed attempt {count} to start beacon:')
advertised_port = beacon_on() advertised_port = beacon_on()
beacon_is_running = advertised_port is not None beacon_is_running = advertised_port is not None
count += 1
if count > 5:
print(f'Giving up, check Bluetooth adapter')
raise SystemExit(1)
def stop_beacon(): def stop_beacon():
global beacon_is_running global beacon_is_running
@@ -498,6 +503,14 @@ if __name__ == '__main__':
advminmax = f'[advmin:advmax]={advmin}:{advmax}' advminmax = f'[advmin:advmax]={advmin}:{advmax}'
if ble_type == bluez: if ble_type == bluez:
indx = f'index {index}' indx = f'index {index}'
test = None
if ble_type == winrt or ble_type == bluez:
# initial test to see if Bluetooth is available
setup_beacon(ipv4_str, 1, advmin, advmax, index)
test = beacon_on()
beacon_off()
if test is not None:
print(f"test passed")
print(f'AirPlay Service-Discovery Bluetooth LE beacon: BLE file {path} {advminmax} {indx}') print(f'AirPlay Service-Discovery Bluetooth LE beacon: BLE file {path} {advminmax} {indx}')
print(f'Advertising IP address {ipv4_str}') print(f'Advertising IP address {ipv4_str}')
print(f'(Press Ctrl+C to exit)') print(f'(Press Ctrl+C to exit)')

View File

@@ -12,6 +12,7 @@
import time import time
import os import os
import ipaddress
try: try:
import serial import serial
@@ -21,6 +22,7 @@ except ImportError as e:
print(f'install pyserial') print(f'install pyserial')
raise SystemExit(1) raise SystemExit(1)
#global variables
advertised_port = None advertised_port = None
advertised_address = None advertised_address = None
serial_port = None serial_port = None
@@ -51,17 +53,16 @@ def check_adv_intrvl(min, max):
from typing import Literal from typing import Literal
def setup_beacon(ipv4_str: str, port: int, advmin: int, advmax: int, index: Literal[None]) ->bool: def setup_beacon(ipv4_str: str, port: int, advmin: int, advmax: int, index: Literal[None]) ->bool:
if index is not None:
raise ValuError('uxplay_beacon_module_BleuIO called with value of index: not None')
global advertised_port global advertised_port
global advertised_address global advertised_address
global airplay_advertisement global airplay_advertisement
global advertisement_parameters global advertisement_parameters
if index is not None:
raise ValuError('uxplay_beacon_module_BleuIO called with value of index: not None')
check_adv_intrvl(advmin, advmax) check_adv_intrvl(advmin, advmax)
# set up advertising message: # set up advertising message:
assert port > 0 assert port > 0
assert port <= 65535 assert port <= 65535
import ipaddress
ipv4_address = ipaddress.ip_address(ipv4_str) ipv4_address = ipaddress.ip_address(ipv4_str)
port_bytes = port.to_bytes(2, 'big') port_bytes = port.to_bytes(2, 'big')
data = bytearray([0xff, 0x4c, 0x00]) # ( 3 bytes) type manufacturer_specific 0xff, manufacturer id Apple 0x004c data = bytearray([0xff, 0x4c, 0x00]) # ( 3 bytes) type manufacturer_specific 0xff, manufacturer id Apple 0x004c
@@ -78,10 +79,7 @@ def setup_beacon(ipv4_str: str, port: int, advmin: int, advmax: int, index: Lite
return True return True
def beacon_on() ->bool: def beacon_on() ->bool:
global airplay_advertisement
global advertisement_parameters
global advertised_port global advertised_port
global serial_port
ser = None ser = None
try: try:
print(f'Connecting to BleuIO dongle on {serial_port} ....') print(f'Connecting to BleuIO dongle on {serial_port} ....')
@@ -110,7 +108,6 @@ def beacon_off():
global airplay_advertisement global airplay_advertisement
global advertised_port global advertised_port
global advertised_address global advertised_address
global serial_port
ser = None ser = None
# Stop advertising # Stop advertising
try: try:
@@ -122,7 +119,6 @@ def beacon_off():
advertised_Port = None advertised_Port = None
advertised_address = None advertised_address = None
advertisement_parameters = None advertisement_parameters = None
resullt = True
except serial.SerialException as e: except serial.SerialException as e:
print(f"beacon_off: Serial port error: {e}") print(f"beacon_off: Serial port error: {e}")
except Exception as e: except Exception as e:
@@ -135,19 +131,22 @@ from typing import Optional
def find_device(serial_port_in: Optional[str]) ->Optional[str]: def find_device(serial_port_in: Optional[str]) ->Optional[str]:
global serial_port global serial_port
serial_ports = list(list_ports.comports()) serial_ports = list(list_ports.comports())
count = 0
serial_port_found = False serial_port_found = False
serial_port = None serial_port = None
TARGET_VID = 0x2DCF # used by BleuIO and BleuIO Pro TARGET_VID = '0x2DCF' # used by BleuIO and BleuIO Pro
target_vid = int(TARGET_VID,16)
if serial_port_in is not None: if serial_port_in is not None:
for p in serial_ports: for p in serial_ports:
if p.vid is None: if getattr(p, 'vid', None) == target_vid or TARGET_VID in p.hwid:
continue if p.device == serial_port_in:
if p.vid == TARGET_VID and p.device == serial_port_in:
serial_port = serial_port_in serial_port = serial_port_in
break
if serial_port is None: if serial_port is None:
count = 0
for p in serial_ports: for p in serial_ports:
if p.vid is not None and p.vid == TARGET_VID: if getattr(p, 'vid', None) == target_vid or TARGET_VID in p.hwid:
count+=1 count+=1
if count == 1: if count == 1:
serial_port = p.device serial_port = p.device
@@ -155,6 +154,7 @@ def find_device(serial_port_in: Optional[str]) ->Optional[str]:
if count>1: if count>1:
print(f'warning: {count} BleueIO devices were found, the first found will be used') print(f'warning: {count} BleueIO devices were found, the first found will be used')
print(f'(to override this choice, specify "--device =..." in optional arguments)') print(f'(to override this choice, specify "--device =..." in optional arguments)')
if serial_port is None: if serial_port is None:
return serial_port return serial_port
@@ -166,10 +166,10 @@ def find_device(serial_port_in: Optional[str]) ->Optional[str]:
except Exception as e: except Exception as e:
print(f"beacon_on: Serial port error: {e}") print(f"beacon_on: Serial port error: {e}")
text=''' text='''
The user does not have sufficient privilegs to access this serial port: The user does not have sufficient privileges to access this serial port:
On Linux, the system administrator should add the user to the "dialout" group On Linux, the user should be added to the "dialout" or "uucp" group
On BSD systems, the necesary group is usually the "dialer" group. On BSD systems, the necesary group is usually the "dialer" group.
This can be checked with ''' The correct group can be found using '''
print(text, f'"ls -l {serial_port}"') print(text, f'"ls -l {serial_port}"')
raise SystemExit(1) raise SystemExit(1)
return serial_port return serial_port

View File

@@ -14,6 +14,11 @@ except ImportError as e:
print(f"install the python3 dbus package") print(f"install the python3 dbus package")
raise SystemExit(1) raise SystemExit(1)
import os
import ipaddress
from typing import Optional
#global variables
ad_manager = None ad_manager = None
airplay_advertisement = None airplay_advertisement = None
advertised_port = None advertised_port = None
@@ -96,7 +101,9 @@ class AirPlay_Service_Discovery_Advertisement(dbus.service.Object):
out_signature='') out_signature='')
def Release(self): def Release(self):
print(f'{self.path}: Released!') print(f'{self.path}: D-Bus Released! (Bluetooth USB adapter removed?)')
print(f'Stopping ...')
os._exit(1)
class AirPlayAdvertisement(AirPlay_Service_Discovery_Advertisement): class AirPlayAdvertisement(AirPlay_Service_Discovery_Advertisement):
@@ -105,7 +112,6 @@ class AirPlayAdvertisement(AirPlay_Service_Discovery_Advertisement):
assert port > 0 assert port > 0
assert port <= 65535 assert port <= 65535
mfg_data = bytearray([0x09, 0x08, 0x13, 0x30]) # Apple Data Unit type 9 (Airplay), length 8, flags 0001 0011, seed 30 mfg_data = bytearray([0x09, 0x08, 0x13, 0x30]) # Apple Data Unit type 9 (Airplay), length 8, flags 0001 0011, seed 30
import ipaddress
ipv4_address = ipaddress.ip_address(ipv4_str) ipv4_address = ipaddress.ip_address(ipv4_str)
ipv4 = bytearray(ipv4_address.packed) ipv4 = bytearray(ipv4_address.packed)
mfg_data.extend(ipv4) mfg_data.extend(ipv4)
@@ -119,7 +125,6 @@ def register_ad_cb():
print(f'AirPlay Service_Discovery Advertisement ({advertised_address}:{advertised_port}) registered') print(f'AirPlay Service_Discovery Advertisement ({advertised_address}:{advertised_port}) registered')
def register_ad_error_cb(error): def register_ad_error_cb(error):
print(f'register_ad: {error}')
global ad_manager global ad_manager
global advertised_port global advertised_port
global advertised_address global advertised_address
@@ -128,15 +133,22 @@ def register_ad_error_cb(error):
advertised_address = None advertised_address = None
def find_adapter(bus): def find_adapter(bus):
try:
remote_om = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, '/'), remote_om = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, '/'),
DBUS_OM_IFACE) DBUS_OM_IFACE)
except dbus.exceptions.DBusException as e:
if e.get_dbus_name() == 'org.freedesktop.DBus.Error.ServiceUnknown':
print("Error: Bluetooth D-Bus service not running on host.")
print(f'Stopping ...')
os._exit(1)
objects = remote_om.GetManagedObjects() objects = remote_om.GetManagedObjects()
for o, props in objects.items(): for o, props in objects.items():
if LE_ADVERTISING_MANAGER_IFACE in props: if LE_ADVERTISING_MANAGER_IFACE in props:
return o return o
return None print(f'Error: Bluetooth adapter not found')
print(f'Stopping ...')
os._exit(1)
from typing import Optional
def setup_beacon(ipv4_str :str, port :int, advmin :int, advmax :int, index :int ) ->int: def setup_beacon(ipv4_str :str, port :int, advmin :int, advmax :int, index :int ) ->int:
global ad_manager global ad_manager
global airplay_advertisement global airplay_advertisement
@@ -147,9 +159,6 @@ def setup_beacon(ipv4_str :str, port :int, advmin :int, advmax :int, index :int
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus() bus = dbus.SystemBus()
adapter = find_adapter(bus) adapter = find_adapter(bus)
if not adapter:
print(f'LEAdvertisingManager1 interface not found')
return False
adapter_props = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, adapter), adapter_props = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, adapter),
"org.freedesktop.DBus.Properties") "org.freedesktop.DBus.Properties")
adapter_props.Set("org.bluez.Adapter1", "Powered", dbus.Boolean(1)) adapter_props.Set("org.bluez.Adapter1", "Powered", dbus.Boolean(1))
@@ -160,6 +169,13 @@ def setup_beacon(ipv4_str :str, port :int, advmin :int, advmax :int, index :int
def beacon_on() ->Optional[int]: def beacon_on() ->Optional[int]:
global airplay_advertisement global airplay_advertisement
global advertised_port
global ad_manager
if advertised_port == 1:
# this value is used when testing for Bluetooth Service
ad_manager = None
advertised_port = None
return None
ad_manager.RegisterAdvertisement(airplay_advertisement.get_path(), {}, ad_manager.RegisterAdvertisement(airplay_advertisement.get_path(), {},
reply_handler=register_ad_cb, reply_handler=register_ad_cb,
error_handler=register_ad_error_cb) error_handler=register_ad_error_cb)

View File

@@ -3,12 +3,13 @@
#---------------------------------------------------------------- #----------------------------------------------------------------
# HCI_Linux (uses sudo hciconfig): module for a standalone python-3.6 or later AirPlay Service-Discovery Bluetooth LE beacon for UxPlay # 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 hciconfig" with giving a password: # this requires that users can run "sudo -n hciconfig" without giving a password:
# (1) (as root) create a group like "hciusers" # (1) (as root) create a group like "hciusers"
# (2) use visudo to make an entry in /etc/sudoers: # (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 # %hciusers ALL=(ALL) NOPASSWD: /usr/bin/hcitool, /usr/bin/hciconfig
# (or or use visudo /etc/sudoers.d/hciusers to create a file /etc/sudoers.d/hciusers with this line in it) # (2b) FreeBSD: use visudo to create /usr/local/etc/sudoers.d/hciusers with the line
# (3) add the user who will run uxplay-beacon.py to the group hciusers # %hciusers ALL=(ALL) NOPASSWD: /usr/sbin/hccontrol
# (3) add the users who will run uxplay-beacon.py to the group hciusers
import subprocess import subprocess
@@ -19,12 +20,15 @@ import platform
from typing import Optional from typing import Optional
from typing import Literal from typing import Literal
#global variables
hci = None
advertised_port = None
advertised_address = None
os_name = platform.system() os_name = platform.system()
if os_name == 'Darwin':
os_name = 'macOS'
linux = os_name == 'Linux' linux = os_name == 'Linux'
bsd = 'BSD' in os_name freebsd = os_name == 'FreeBSD'
if not linux and not bsd: if not linux and not freebsd:
print(f'{os_name} is not supported by the HCI module') print(f'{os_name} is not supported by the HCI module')
raise SystemExit(1) raise SystemExit(1)
@@ -39,7 +43,20 @@ if linux:
(2) use visudo to create a file /etc/sudoers.d/hciusers containing the line: (2) use visudo to create a file /etc/sudoers.d/hciusers containing the line:
%hciusers ALL=(ALL) NOPASSWD: /usr/bin/hciconfig, /usr/bin/hcitool %hciusers ALL=(ALL) NOPASSWD: /usr/bin/hciconfig, /usr/bin/hcitool
''' '''
elif bsd: 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 = ''' help_text2 = '''
(2) use visudo to create a file /usr/local/etc/sudoers.d/hciusers containing the line: (2) use visudo to create a file /usr/local/etc/sudoers.d/hciusers containing the line:
%hciusers ALL=(ALL) NOPASSWD: /usr/sbin/hccontrol %hciusers ALL=(ALL) NOPASSWD: /usr/sbin/hccontrol
@@ -49,43 +66,43 @@ help_text3 = '''
''' '''
help_text = help_text1 + help_text2 + help_text3 help_text = help_text1 + help_text2 + help_text3
hci = None sudo = ['sudo', '-n']
LMP_version_map = ["1.0b","1.1", "1.2", "2.0+EDR", "2.1+EDR", "3.0+HS", "4.0", "4.1", "4.2", "5.0", "5.1", "5.2", "5.3", "5.4", "6.0", "6.1"] if linux:
ogf = "0x08"
def le_cmd(hcicmd, args):
advertised_port = None cmd = sudo + ['hcitool', '-i', hci, 'cmd', ogf, hcicmd] + args
advertised_address = None 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: def setup_beacon(ipv4_str: str, port: int, advmin: int, advmax: int, index: Literal[None]) -> bool:
global hci
global advertised_port global advertised_port
global advertised_address global advertised_address
advertised_port = None advertised_port = None
advertised_address = None advertised_address = None
# setup Advertising Parameters
if linux:
# convert into units of 5/8 msec. # convert into units of 5/8 msec.
advmin = (advmin * 8) // 5 advmin = (advmin * 8) // 5
advmax = (advmax * 8) // 5 advmax = (advmax * 8) // 5
# setup Advertising Parameters
if linux:
min1 = f'{advmin %256 :#04x}' min1 = f'{advmin %256 :#04x}'
min2 = f'{advmin //256 :#04x}' min2 = f'{advmin //256 :#04x}'
max1 = f'{advmax % 256 :#04x}' max1 = f'{advmax % 256 :#04x}'
max2 = f'{advmax // 256 :#04x}' max2 = f'{advmax // 256 :#04x}'
ogf = "0x08" args = [min1, min2, max1, max2, '0x03', '0x00', '0x00'] + ['0x00'] * 6 + ['0x07', '0x00']
ocf = "0x0006" hcicmd = "0x0006"
cmd = ["sudo", '-n', "hcitool", "-i", hci, "cmd", ogf, ocf, min1, min2, max1, max2, '0x03', '0x00', '0x00'] + ['0x00'] * 6 + ['0x07', '0x00'] elif freebsd:
elif bsd: min = f'-m {advmin}'
min = f'{advmin :04x}' max = f'-M {advmax}'
max = f'{advmax :04x}' args = [min, max, 't = 3']
cmd = ["sudo", "-n", "hccontrol", "-n", hci, "le_set_advertising_param", min, max, '03', '00', '00', '000000000000', '07','00'] hcicmd = 'le_set_advertising_param'
try: try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True) result = le_cmd(hcicmd, args)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print("Error:", e.stderr, e.stdout) print(f'beacon_on error (set_advertisng_parameters):', e.stderr, e.stdout)
return False return False
# setup Advertising Data # setup Advertising Data
@@ -94,85 +111,76 @@ def setup_beacon(ipv4_str: str, port: int, advmin: int, advmax: int, index: Lite
ip = list(map(int, ipv4_str.split('.'))) ip = list(map(int, ipv4_str.split('.')))
prt = [port // 256, port % 256] prt = [port // 256, port % 256]
adv_int = adv_int + ip + prt adv_int = adv_int + ip + prt
if linux:
adv_len = len(adv_int) adv_len = len(adv_int)
adv_int = [adv_len + 1, adv_len ] + adv_int adv_int = [adv_len + 1, adv_len ] + adv_int
if linux: args = [f'{i:#04x}' for i in adv_int]
ogf = '0x08' args += ['0x00'] * (31 - len(adv_int))
ocf = '0x0008' hcicmd = '0x0008'
cmd = ['sudo', '-n', 'hcitool', '-i', hci, 'cmd', ogf, ocf] elif freebsd:
cmd = cmd + [f'{i:#04x}' for i in adv_int] adv = ','.join(f'{byte:02x}' for byte in adv_int)
cmd = cmd + ['0x00'] * 17 args = ['-b', adv]
elif bsd: hcicmd = 'le_set_advertising_data'
cmd = ['sudo', '-n', 'hccontrol', '-n', hci, 'le_set_advertising_data']
cmd = cmd + [f'{i:02x}' for i in adv_int]
try: try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True) le_cmd(hcicmd, args)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print("Error:", e.stderr, e.stdout) print(f'beacon_on error (set_advertisng_parameters):', e.stderr, e.stdout)
return False return False
advertised_port = port advertised_port = port
advertised_address = ipv4_str advertised_address = ipv4_str
return True return True
def beacon_on() -> Optional[int]: def beacon_on() -> Optional[int]:
global advertised_port
global advertised_address
if linux: if linux:
ogf = '0x08' hcicmd = '0x000a'
ocf = '0x000a' args = ['0x01']
cmd = ['sudo', '-n', 'hcitool', '-i', hci, 'cmd', ogf, ocf, '0x01'] elif freebsd:
elif bsd: hcicmd = 'le_set_advertising_enable'
cmd = ['sudo', '-n', 'hccontrol', '-n', hci, 'le_set_advertising_enable', 'enable'] args = ['enable']
try: try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True) le_cmd(hcicmd, args)
print(f'Started Bluetooth LE Service Discovery beacon {advertised_address}:{advertised_port}')
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f'beacon_on error:', e.stderr, e.stdout) print(f'beacon_on error:', e.stderr, e.stdout)
global advertised_port
global advertised_address
advertised_port = None advertised_port = None
advertised_address = None advertised_address = None
finally: return None
print(f'AirPlay Service-Discovery beacon transmission started')
return advertised_port return advertised_port
def beacon_off(): def beacon_off():
if linux: if linux:
ogf = '0x08' hcicmd = '0x000a'
ocf = '0x000a' args = ['0x00']
cmd = ['sudo', '-n', 'hcitool', '-i', hci, 'cmd', ogf, ocf, '0x00'] elif freebsd:
elif bsd: hcicmd = 'le_set_advertising_enable'
cmd = ['sudo', '-n', 'hccontrol', '-n', hci, 'le_set_advertising_enable', 'disable'] args = ['disable']
try: le_cmd(hcicmd, args)
result = subprocess.run(cmd, capture_output=True, text=True, check=True) print(f'AirPlay Service-Discovery beacon transmission ended')
print(f'Stopped Bluetooth LE Service Discovery beacon')
except subprocess.CalledProcessError as e:
print("Error (beacon_off):", e.stderr, e.stdout)
advertised_address = None advertised_address = None
advertised_port = 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): def get_bluetooth_version(device_name):
"""
Runs 'hciconfig -a <device_name>' and extracts the LMP version.
"""
if linux: if linux:
cmd = f'hciconfig' cmd ='hciconfig'
opt1 = f'' args = [cmd, device_name, '-a']
opt2 = f'-a'
regexp = r"LMP Version: .*?\(0x([0-9a-fA-F])\)" regexp = r"LMP Version: .*?\(0x([0-9a-fA-F])\)"
elif bsd: elif freebsd:
cmd = f'hccontrol' cmd = 'hccontrol'
opt1 = f'-n' args = [cmd, '-n', device_name, 'Read_Local_Version_Information']
opt2 = f'Read_Local_Version_Information'
regexp = r"LMP version: .*?\[(0x[0-9a-fA-F]+)\]" regexp = r"LMP version: .*?\[(0x[0-9a-fA-F]+)\]"
try: try:
# Run hciconfig -a for the specific device # Run hciconfig -a for the specific device
result = subprocess.check_output([cmd, opt1, device_name, opt2], stderr=subprocess.STDOUT, text=True) result = subprocess.check_output(args, stderr=subprocess.STDOUT, text=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Error running {cmd} for {device_name}: {e.output}") print(f"Error running {cmd} for {device_name}: {e.output}")
return None return None
except FileNotFoundError: except FileNotFoundError:
print("Error: {cmd} command not found") print(f"Error: {cmd} command not found")
return None return None
# Regex to find "LMP Version: X.Y (0xZ)" # Regex to find "LMP Version: X.Y (0xZ)"
lmp_version_match = re.search(regexp, result) lmp_version_match = re.search(regexp, result)
@@ -183,22 +191,24 @@ def get_bluetooth_version(device_name):
def list_devices_by_version(min_version): def list_devices_by_version(min_version):
if linux: if linux:
cmd = f'hcitool' cmd = 'hcitool'
opt = f'dev' args = [cmd]
args.append('dev')
regexp = r"(hci\d+)" regexp = r"(hci\d+)"
elif bsd: elif freebsd:
cmd = f'hccontrol' cmd = 'hccontrol'
opt = f'Read_Node_List' args = [cmd]
args.append('Read_Node_List')
regexp = r"(^ubt\d+hci)" regexp = r"(^ubt\d+hci)"
try: try:
# Run hciconfig to list all devices # Run hciconfig to list all devices
devices_list_output = subprocess.check_output([cmd, opt], stderr=subprocess.STDOUT, text=True) devices_list_output = subprocess.check_output(args, stderr=subprocess.STDOUT, text=True)
print(devices_list_output)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Error running hciconfig: {e.output}") print(f"Error running {cmd}: {e.output}")
return None return None
except FileNotFoundError: except FileNotFoundError:
print("Error: hciconfig command not found") print(f"Error: {cmd} command not found")
return None return None
# Regex to find device names (e.g., hci0, hci1) # Regex to find device names (e.g., hci0, hci1)
device_names = re.findall(regexp, devices_list_output, re.MULTILINE) device_names = re.findall(regexp, devices_list_output, re.MULTILINE)
@@ -207,7 +217,7 @@ def list_devices_by_version(min_version):
version_decimal = get_bluetooth_version(device_name) version_decimal = get_bluetooth_version(device_name)
if version_decimal is None or version_decimal < min_version: if version_decimal is None or version_decimal < min_version:
continue continue
bt_version = LMP_version_map[version_decimal] bt_version = LMP[version_decimal]
device = [device_name, bt_version] device = [device_name, bt_version]
found_devices.append(device) found_devices.append(device)
return found_devices return found_devices
@@ -216,7 +226,7 @@ from typing import Optional
def find_device(hci_in: Optional[str]) -> Optional[str]: def find_device(hci_in: Optional[str]) -> Optional[str]:
global hci global hci
list = list_devices_by_version(min_version=6) list = list_devices_by_version(min_version=6)
if len(list) == 0: if list is None or len(list) == 0:
return None return None
hci = None hci = None
if hci_in is not None: if hci_in is not None:
@@ -234,9 +244,9 @@ def find_device(hci_in: Optional[str]) -> Optional[str]:
print(f'warning: {count} HCI devices were found, the first found will be used') 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)') print(f'(to override this choice, specify "--device=..." in optional arguments)')
if linux: if linux:
cmd = ['sudo', '-n', 'hciconfig', hci, 'reset'] cmd = sudo + ['hciconfig', hci, 'reset']
elif bsd: elif freebsd:
cmd = ['sudo', '-n', 'hccontrol', '-n', hci, 'Reset'] cmd = sudo + ['hccontrol', '-n', hci, 'Reset']
try: try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True) result = subprocess.run(cmd, capture_output=True, text=True, check=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@@ -246,3 +256,4 @@ def find_device(hci_in: Optional[str]) -> Optional[str]:
raise SystemExit(1) raise SystemExit(1)
return hci return hci
print('loaded uxplay_beacon_module_HCI')

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: LGPL-2.1-or-later # SPDX-License-Identifier: LGPL-2.1-or-later
#---------------------------------------------------------------- #---------------------------------------------------------------
# winrt (Windows) module for a standalone python-3.6 AirPlay Service-Discovery Bluetooth LE beacon for UxPlay # winrt (Windows) module for a standalone python-3.6 AirPlay Service-Discovery Bluetooth LE beacon for UxPlay
# (c) F. Duncanh, March 2026 # (c) F. Duncanh, March 2026
@@ -29,21 +29,36 @@ except ImportError:
print(f'You may need to use pip option "--break-system-packages" (disregard the warning)') print(f'You may need to use pip option "--break-system-packages" (disregard the warning)')
raise SystemExit(1) raise SystemExit(1)
import os
import asyncio
import ipaddress
from typing import Literal
from typing import Optional
#global variables
publisher = None publisher = None
advertised_port = None advertised_port = None
advertised_address = None advertised_address = None
quiet = False
def on_status_changed(sender, args): def on_status_changed(sender, args):
global publisher global publisher
if not quiet:
print(f"Publisher status change to: {args.status.name}") print(f"Publisher status change to: {args.status.name}")
if args.status.name == "ABORTED":
print(f'Publisher was aborted after starting: perhaps no Bluetooth interface is available?')
print(f'Stopping')
os._exit(1)
if args.status.name == "STOPPED": if args.status.name == "STOPPED":
publisher = None publisher = None
def create_airplay_service_discovery_advertisement_publisher(ipv4_str, port): def create_airplay_service_discovery_advertisement_publisher(ipv4_str, port):
global publisher
global advertised_port
global advertised_address
assert port > 0 assert port > 0
assert port <= 65535 assert port <= 65535
mfg_data = bytearray([0x09, 0x08, 0x13, 0x30]) # Apple Data Unit type 9 (Airplay), length 8, flags 0001 0011, seed 30 mfg_data = bytearray([0x09, 0x08, 0x13, 0x30]) # Apple Data Unit type 9 (Airplay), length 8, flags 0001 0011, seed 30
import ipaddress
ipv4_address = ipaddress.ip_address(ipv4_str) ipv4_address = ipaddress.ip_address(ipv4_str)
ipv4 = bytearray(ipv4_address.packed) ipv4 = bytearray(ipv4_address.packed)
mfg_data.extend(ipv4) mfg_data.extend(ipv4)
@@ -56,9 +71,6 @@ def create_airplay_service_discovery_advertisement_publisher(ipv4_str, port):
manufacturer_data.data = writer.detach_buffer() manufacturer_data.data = writer.detach_buffer()
advertisement = ble_adv.BluetoothLEAdvertisement() advertisement = ble_adv.BluetoothLEAdvertisement()
advertisement.manufacturer_data.append(manufacturer_data) advertisement.manufacturer_data.append(manufacturer_data)
global publisher
global advertised_port
global advertised_address
publisher = ble_adv.BluetoothLEAdvertisementPublisher(advertisement) publisher = ble_adv.BluetoothLEAdvertisementPublisher(advertisement)
advertised_port = port advertised_port = port
advertised_address = ipv4_str advertised_address = ipv4_str
@@ -69,6 +81,7 @@ async def publish_advertisement():
global advertised_address global advertised_address
try: try:
publisher.start() publisher.start()
if not quiet:
print(f"AirPlay Service_Discovery Advertisement ({advertised_address}:{advertised_port}) registered") print(f"AirPlay Service_Discovery Advertisement ({advertised_address}:{advertised_port}) registered")
except Exception as e: except Exception as e:
print(f"Failed to start Publisher: {e}") print(f"Failed to start Publisher: {e}")
@@ -76,31 +89,34 @@ async def publish_advertisement():
advertised_address = None advertised_address = None
advertised_port = None advertised_port = None
from typing import Literal
def setup_beacon(ipv4_str: str, port:int , advmin: Literal[None], advmax :Literal[None], index :Literal[None]) ->bool: def setup_beacon(ipv4_str: str, port:int , advmin: Literal[None], advmax :Literal[None], index :Literal[None]) ->bool:
global quiet
quiet = False
if port == 1:
#fake port used for testing
print(f'beacon test')
quiet = True
if (advmin is not None) or (advmax is not None) or (index is not None): if (advmin is not None) or (advmax is not None) or (index is not None):
raise ValueError('uxplay_beacon_module_winrt: advmin, advmax, index were not all None') raise ValueError('uxplay_beacon_module_winrt: advmin, advmax, index were not all None')
create_airplay_service_discovery_advertisement_publisher(ipv4_str, port) create_airplay_service_discovery_advertisement_publisher(ipv4_str, port)
return True return True
from typing import Optional
def beacon_on() -> Optional[int]: def beacon_on() -> Optional[int]:
import asyncio global publisher
global advertised_port
try: try:
asyncio.run(publish_advertisement()) asyncio.run(publish_advertisement())
except Exception as e: except Exception as e:
print(f"Failed to start publisher: {e}") print(f"Failed to start publisher: {e}")
global publisher
publisher = None publisher = None
finally:
#advertised_port is set to None if publish_advertisement failed #advertised_port is set to None if publish_advertisement failed
global advertised_port
return advertised_port return advertised_port
def beacon_off(): def beacon_off():
publisher.stop()
global advertised_port global advertised_port
global advertised_address global advertised_address
publisher.stop()
advertised_port = None advertised_port = None
advertised_address = None advertised_address = None