# SPDX-License-Identifier: LGPL-2.1-or-later # adapted from https://github.com/bluez/bluez/blob/master/test/example-advertisement #---------------------------------------------------------------- # BlueZ/D-Bus (Linux) module for a standalone python-3.6 AirPlay Service-Discovery Bluetooth LE beacon for UxPlay # (c) F. Duncanh, March 2026 try: import dbus import dbus.exceptions import dbus.mainloop.glib import dbus.service except ImportError as e: print(f"ImportError: {e}, failed to import required dbus components") print(f"install the python3 dbus package") raise SystemExit(1) import os import ipaddress from typing import Optional #global variables ad_manager = None airplay_advertisement = None advertised_port = None advertised_address = None BLUEZ_SERVICE_NAME = 'org.bluez' LE_ADVERTISING_MANAGER_IFACE = 'org.bluez.LEAdvertisingManager1' DBUS_OM_IFACE = 'org.freedesktop.DBus.ObjectManager' DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties' LE_ADVERTISEMENT_IFACE = 'org.bluez.LEAdvertisement1' class InvalidArgsException(dbus.exceptions.DBusException): _dbus_error_name = 'org.freedesktop.DBus.Error.InvalidArgs' class NotSupportedException(dbus.exceptions.DBusException): _dbus_error_name = 'org.bluez.Error.NotSupported' class NotPermittedException(dbus.exceptions.DBusException): _dbus_error_name = 'org.bluez.Error.NotPermitted' class InvalidValueLengthException(dbus.exceptions.DBusException): _dbus_error_name = 'org.bluez.Error.InvalidValueLength' class FailedException(dbus.exceptions.DBusException): _dbus_error_name = 'org.bluez.Error.Failed' class AirPlay_Service_Discovery_Advertisement(dbus.service.Object): PATH_BASE = '/org/bluez/airplay_service_discovery_advertisement' def __init__(self, bus, index): self.path = self.PATH_BASE + str(index) self.bus = bus self.manufacturer_data = None self.min_intrvl = 0 self.max_intrvl = 0 dbus.service.Object.__init__(self, bus, self.path) def get_properties(self): properties = dict() properties['Type'] = 'broadcast' if self.manufacturer_data is not None: properties['ManufacturerData'] = dbus.Dictionary( self.manufacturer_data, signature='qv') if self.min_intrvl > 0: properties['MinInterval'] = dbus.UInt32(self.min_intrvl) if self.max_intrvl > 0: properties['MaxInterval'] = dbus.UInt32(self.max_intrvl) return {LE_ADVERTISEMENT_IFACE: properties} def get_path(self): return dbus.ObjectPath(self.path) def add_manufacturer_data(self, manuf_code, manuf_data): if not self.manufacturer_data: self.manufacturer_data = dbus.Dictionary({}, signature='qv') self.manufacturer_data[manuf_code] = dbus.Array(manuf_data, signature='y') def set_min_intrvl(self, min_intrvl): if self.min_intrvl == 0: self.min_intrvl = 100 self.min_intrvl = max(min_intrvl, 100) def set_max_intrvl(self, max_intrvl): if self.max_intrvl == 0: self.max_intrvl = 100 self.max_intrvl = max(max_intrvl, 100) @dbus.service.method(DBUS_PROP_IFACE, in_signature='s', out_signature='a{sv}') def GetAll(self, interface): if interface != LE_ADVERTISEMENT_IFACE: raise InvalidArgsException() return self.get_properties()[LE_ADVERTISEMENT_IFACE] @dbus.service.method(LE_ADVERTISEMENT_IFACE, in_signature='', out_signature='') def Release(self): print(f'{self.path}: D-Bus Released! (Bluetooth USB adapter removed?)') print(f'Stopping ...') os._exit(1) class AirPlayAdvertisement(AirPlay_Service_Discovery_Advertisement): def __init__(self, bus, index, ipv4_str, port, min_intrvl, max_intrvl): AirPlay_Service_Discovery_Advertisement.__init__(self, bus, index) assert port > 0 assert port <= 65535 mfg_data = bytearray([0x09, 0x08, 0x13, 0x30]) # Apple Data Unit type 9 (Airplay), length 8, flags 0001 0011, seed 30 ipv4_address = ipaddress.ip_address(ipv4_str) ipv4 = bytearray(ipv4_address.packed) mfg_data.extend(ipv4) port_bytes = port.to_bytes(2, 'big') mfg_data.extend(port_bytes) self.add_manufacturer_data(0x004c, mfg_data) self.set_min_intrvl(min_intrvl) self.set_max_intrvl(max_intrvl) def register_ad_cb(): print(f'AirPlay Service_Discovery Advertisement ({advertised_address}:{advertised_port}) registered') def register_ad_error_cb(error): global ad_manager global advertised_port global advertised_address ad_manager = None advertised_port = None advertised_address = None def find_adapter(bus): try: remote_om = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, '/'), 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() for o, props in objects.items(): if LE_ADVERTISING_MANAGER_IFACE in props: return o print(f'Error: Bluetooth adapter not found') print(f'Stopping ...') os._exit(1) def setup_beacon(ipv4_str :str, port :int, advmin :int, advmax :int, index :int ) ->int: global ad_manager global airplay_advertisement global advertised_address global advertised_port advertised_port = port advertised_address = ipv4_str dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) bus = dbus.SystemBus() adapter = find_adapter(bus) adapter_props = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, adapter), "org.freedesktop.DBus.Properties") adapter_props.Set("org.bluez.Adapter1", "Powered", dbus.Boolean(1)) ad_manager = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, adapter), LE_ADVERTISING_MANAGER_IFACE) airplay_advertisement = AirPlayAdvertisement(bus, index, ipv4_str, port, advmin, advmax) return True def beacon_on() ->Optional[int]: 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(), {}, reply_handler=register_ad_cb, error_handler=register_ad_error_cb) # if registration error occurs, advertised_port is set to None by callback if advertised_port is None: airplay_advertisement = None return advertised_port def beacon_off(): global ad_manager global airplay_advertisement global advertised_port global advertised_address if ad_manager is not None: ad_manager.UnregisterAdvertisement(airplay_advertisement) print(f'AirPlay Service-Discovery beacon advertisement unregistered') ad_manager = None if airplay_advertisement is not None: dbus.service.Object.remove_from_connection(airplay_advertisement) airplay_advertisement = None advertised_Port = None advertised_address = None print(f'loaded uxplay_beacon_module_BlueZ ')