Skip to content

Instantly share code, notes, and snippets.

@ukBaz

ukBaz/README.md Secret

Last active April 26, 2024 09:59
Show Gist options
  • Star 36 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save ukBaz/a47e71e7b87fbc851b27cde7d1c0fcf0 to your computer and use it in GitHub Desktop.
Save ukBaz/a47e71e7b87fbc851b27cde7d1c0fcf0 to your computer and use it in GitHub Desktop.
Create a Bluetooth HID server

This was an experiment to turn a Raspberry Pi into a Human Interface Device (HID). A keyboard to be more precise.

I followed the instructions at the following location to get me started:
http://yetanotherpointlesstechblog.blogspot.com/2016/04/emulating-bluetooth-keyboard-with.html

Note [Apr-2024]: The above link is now dead but it still available on the Wayback Machine archive:
https://web.archive.org/web/20181117162450/http://yetanotherpointlesstechblog.blogspot.com/2016/04/emulating-bluetooth-keyboard-with.html

I wanted to move to Python3 and tidy things up on the Bluetooth side to bring it in to line with current ways things are done in BlueZ.

Configure Raspberry Pi.

These instructions assuming you have BlueZ 5.43 installed. You can check this with:

$ bluetoothctl -v
5.43

Ensure Raspberry Pi is at the latest version:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get dist-upgrade

Check that the packages required for this are installed

sudo apt-get install python3-dbus
sudo pip install evdev

Here is an outline of things I changed:

Moved to Python3

I wanted to do this because not only is it a good thing to do but it also allowed some of the dependancies to be removed. After Python 3.3 Bluetooth sockets are supported in the native Python installs. The downside to this is that there are clear distinctions between str and bytes in the code. For me, this broke the keyboard client. This is what required the biggest re-write to get Python3 working.

Reconfigure the Bluetooth Daemon

The instructions worked that were provided but things have moved on a little bit. To stop the Bluetooth daemon running then the following command is preferred:

sudo service bluetooth stop

The input Bluetooth plugin needs to be removed so that it does not grab the sockets we require access to. As the original author says the way this was documented could be improved. If you want to restart the daemon (without the input plugin) from the command line then the following would seem the preferred:

sudo /usr/lib/bluetooth/bluetoothd -P input

If you want to make this the default for this Raspberry Pi then modify the /lib/systemd/system/bluetooth.service file. You will need to change the Service line from:

ExecStart=/usr/lib/bluetooth/bluetoothd

to

ExecStart=/usr/lib/bluetooth/bluetoothd -P input

Configure D-Bus

When a new service is created on the D-Bus, this service needs to be configured.

sudo cp org.yaptb.btkbservice.conf /etc/dbus-1/system.d

Event loop

The original article used Gtk for the event loop. I changed it to the library that I normally use and this removed the warning the original author was getting.

hciconfig

This command has been deprecated in the BlueZ project. https://wiki.archlinux.org/index.php/bluetooth#Deprecated_BlueZ_tools

In the setup of the original article the hciconfig command used to get the BD address. I have modified this so that the code queries the adapter and gets the address.

There were also os.system calls to hciconfig from within the Python. With the new BlueZ D-Bus interface these are unnecessary and have been replaced with D-Bus calls.

Sockets

Moving to a new version (> 3.3?) of Python will not require the import bluetooth line that was there previously. More information on the Python socket support of Bluetooth is available at: https://docs.python.org/3.4/library/socket.html#socket-families

Registering of Profile

As the original author noted, the registering of the HID profile does not seem to work as documented at: https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/profile-api.txt The NewConnection method did not seem to get called on a new connection being made. Requests to the BlueZ mailing list did not seem to yield any insight as to why this is.

Pairing

With the settings used in this setup the pairing steps described in the original tutorial should not be required. While this is probably not a sensible choice for a real situation, for this experiment I chose convenience over security.

Below is a transcript from the two terminal I had open for this experiment.

Terminal 1

pi@raspberrypi:~/python/bluetooth_hid/btkeyboard/server $ sudo service bluetooth stop
pi@raspberrypi:~/python/bluetooth_hid/btkeyboard/server $ sudo /usr/lib/bluetooth/bluetoothd -P input &
pi@raspberrypi:~/python/bluetooth_hid/btkeyboard/server $ sudo python3 btk_server.py
Setting up service
Setting up BT device
Configuring for name BT_HID_Keyboard
Configuring Bluez Profile
Reading service record
Profile registered
Waiting for connections

Scan for the keyboard Pi and connect from main computer

8C:2D:AA:44:0E:3A connected on the control socket
8C:2D:AA:44:0E:3A connected on the interrupt channel

Terminal 2

pi@raspberrypi:~/python/bluetooth_hid/btkeyboard/keyboard $ python3 kb_client.py
Setting up keyboard
found a keyboard
starting event loop
Listening...
#!/usr/bin/python3
"""
Bluetooth HID keyboard emulator DBUS Service
Original idea taken from:
http://yetanotherpointlesstechblog.blogspot.com/2016/04/emulating-bluetooth-keyboard-with.html
Moved to Python 3 and tested with BlueZ 5.43
"""
import os
import sys
import dbus
import dbus.service
import socket
from gi.repository import GLib
from dbus.mainloop.glib import DBusGMainLoop
class HumanInterfaceDeviceProfile(dbus.service.Object):
"""
BlueZ D-Bus Profile for HID
"""
fd = -1
@dbus.service.method('org.bluez.Profile1',
in_signature='', out_signature='')
def Release(self):
print('Release')
mainloop.quit()
@dbus.service.method('org.bluez.Profile1',
in_signature='oha{sv}', out_signature='')
def NewConnection(self, path, fd, properties):
self.fd = fd.take()
print('NewConnection({}, {})'.format(path, self.fd))
for key in properties.keys():
if key == 'Version' or key == 'Features':
print(' {} = 0x{:04x}'.format(key,
properties[key]))
else:
print(' {} = {}'.format(key, properties[key]))
@dbus.service.method('org.bluez.Profile1',
in_signature='o', out_signature='')
def RequestDisconnection(self, path):
print('RequestDisconnection {}'.format(path))
if self.fd > 0:
os.close(self.fd)
self.fd = -1
class BTKbDevice:
"""
create a bluetooth device to emulate a HID keyboard
"""
MY_DEV_NAME = 'BT_HID_Keyboard'
# Service port - must match port configured in SDP record
P_CTRL = 17
# Service port - must match port configured in SDP record#Interrrupt port
P_INTR = 19
# BlueZ dbus
PROFILE_DBUS_PATH = '/bluez/yaptb/btkb_profile'
ADAPTER_IFACE = 'org.bluez.Adapter1'
DEVICE_INTERFACE = 'org.bluez.Device1'
DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties'
DBUS_OM_IFACE = 'org.freedesktop.DBus.ObjectManager'
# file path of the sdp record to laod
install_dir = os.path.dirname(os.path.realpath(__file__))
SDP_RECORD_PATH = os.path.join(install_dir,
'sdp_record.xml')
# UUID for HID service (1124)
# https://www.bluetooth.com/specifications/assigned-numbers/service-discovery
UUID = '00001124-0000-1000-8000-00805f9b34fb'
def __init__(self, hci=0):
self.scontrol = None
self.ccontrol = None # Socket object for control
self.sinterrupt = None
self.cinterrupt = None # Socket object for interrupt
self.dev_path = '/org/bluez/hci{}'.format(hci)
print('Setting up BT device')
self.bus = dbus.SystemBus()
self.adapter_methods = dbus.Interface(
self.bus.get_object('org.bluez',
self.dev_path),
self.ADAPTER_IFACE)
self.adapter_property = dbus.Interface(
self.bus.get_object('org.bluez',
self.dev_path),
self.DBUS_PROP_IFACE)
self.bus.add_signal_receiver(self.interfaces_added,
dbus_interface=self.DBUS_OM_IFACE,
signal_name='InterfacesAdded')
self.bus.add_signal_receiver(self._properties_changed,
dbus_interface=self.DBUS_PROP_IFACE,
signal_name='PropertiesChanged',
arg0=self.DEVICE_INTERFACE,
path_keyword='path')
print('Configuring for name {}'.format(BTKbDevice.MY_DEV_NAME))
self.config_hid_profile()
# set the Bluetooth device configuration
self.alias = BTKbDevice.MY_DEV_NAME
self.discoverabletimeout = 0
self.discoverable = True
def interfaces_added(self):
pass
def _properties_changed(self, interface, changed, invalidated, path):
if self.on_disconnect is not None:
if 'Connected' in changed:
if not changed['Connected']:
self.on_disconnect()
def on_disconnect(self):
print('The client has been disconnect')
self.listen()
@property
def address(self):
"""Return the adapter MAC address."""
return self.adapter_property.Get(self.ADAPTER_IFACE,
'Address')
@property
def powered(self):
"""
power state of the Adapter.
"""
return self.adapter_property.Get(self.ADAPTER_IFACE, 'Powered')
@powered.setter
def powered(self, new_state):
self.adapter_property.Set(self.ADAPTER_IFACE, 'Powered', new_state)
@property
def alias(self):
return self.adapter_property.Get(self.ADAPTER_IFACE,
'Alias')
@alias.setter
def alias(self, new_alias):
self.adapter_property.Set(self.ADAPTER_IFACE,
'Alias',
new_alias)
@property
def discoverabletimeout(self):
"""Discoverable timeout of the Adapter."""
return self.adapter_props.Get(self.ADAPTER_IFACE,
'DiscoverableTimeout')
@discoverabletimeout.setter
def discoverabletimeout(self, new_timeout):
self.adapter_property.Set(self.ADAPTER_IFACE,
'DiscoverableTimeout',
dbus.UInt32(new_timeout))
@property
def discoverable(self):
"""Discoverable state of the Adapter."""
return self.adapter_props.Get(
self.ADAPTER_INTERFACE, 'Discoverable')
@discoverable.setter
def discoverable(self, new_state):
self.adapter_property.Set(self.ADAPTER_IFACE,
'Discoverable',
new_state)
def config_hid_profile(self):
"""
Setup and register HID Profile
"""
print('Configuring Bluez Profile')
service_record = self.read_sdp_service_record()
opts = {
'Role': 'server',
'RequireAuthentication': False,
'RequireAuthorization': False,
'AutoConnect': True,
'ServiceRecord': service_record,
}
manager = dbus.Interface(self.bus.get_object('org.bluez',
'/org/bluez'),
'org.bluez.ProfileManager1')
HumanInterfaceDeviceProfile(self.bus,
BTKbDevice.PROFILE_DBUS_PATH)
manager.RegisterProfile(BTKbDevice.PROFILE_DBUS_PATH,
BTKbDevice.UUID,
opts)
print('Profile registered ')
@staticmethod
def read_sdp_service_record():
"""
Read and return SDP record from a file
:return: (string) SDP record
"""
print('Reading service record')
try:
fh = open(BTKbDevice.SDP_RECORD_PATH, 'r')
except OSError:
sys.exit('Could not open the sdp record. Exiting...')
return fh.read()
def listen(self):
"""
Listen for connections coming from HID client
"""
print('Waiting for connections')
self.scontrol = socket.socket(socket.AF_BLUETOOTH,
socket.SOCK_SEQPACKET,
socket.BTPROTO_L2CAP)
self.scontrol.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sinterrupt = socket.socket(socket.AF_BLUETOOTH,
socket.SOCK_SEQPACKET,
socket.BTPROTO_L2CAP)
self.sinterrupt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.scontrol.bind((self.address, self.P_CTRL))
self.sinterrupt.bind((self.address, self.P_INTR))
# Start listening on the server sockets
self.scontrol.listen(1) # Limit of 1 connection
self.sinterrupt.listen(1)
self.ccontrol, cinfo = self.scontrol.accept()
print('{} connected on the control socket'.format(cinfo[0]))
self.cinterrupt, cinfo = self.sinterrupt.accept()
print('{} connected on the interrupt channel'.format(cinfo[0]))
def send(self, msg):
"""
Send HID message
:param msg: (bytes) HID packet to send
"""
self.cinterrupt.send(bytes(bytearray(msg)))
class BTKbService(dbus.service.Object):
"""
Setup of a D-Bus service to recieve HID messages from other
processes.
Send the recieved HID messages to the Bluetooth HID server to send
"""
def __init__(self):
print('Setting up service')
bus_name = dbus.service.BusName('org.yaptb.btkbservice',
bus=dbus.SystemBus())
dbus.service.Object.__init__(self, bus_name, '/org/yaptb/btkbservice')
# create and setup our device
self.device = BTKbDevice()
# start listening for socket connections
self.device.listen()
@dbus.service.method('org.yaptb.btkbservice',
in_signature='ay')
def send_keys(self, cmd):
self.device.send(cmd)
if __name__ == '__main__':
# The sockets require root permission
if not os.geteuid() == 0:
sys.exit('Only root can run this script')
DBusGMainLoop(set_as_default=True)
myservice = BTKbService()
mainloop = GLib.MainLoop()
mainloop.run()
import dbus
import evdev
import keymap
from time import sleep
HID_DBUS = 'org.yaptb.btkbservice'
HID_SRVC = '/org/yaptb/btkbservice'
class Kbrd:
"""
Take the events from a physically attached keyboard and send the
HID messages to the keyboard D-Bus server.
"""
def __init__(self):
self.target_length = 6
self.mod_keys = 0b00000000
self.pressed_keys = []
self.have_kb = False
self.dev = None
self.bus = dbus.SystemBus()
self.btkobject = self.bus.get_object(HID_DBUS,
HID_SRVC)
self.btk_service = dbus.Interface(self.btkobject,
HID_DBUS)
self.wait_for_keyboard()
def wait_for_keyboard(self, event_id=0):
"""
Connect to the input event file for the keyboard.
Can take a parameter of an integer that gets appended to the end of
/dev/input/event
:param event_id: Optional parameter if the keyboard is not event0
"""
while not self.have_kb:
try:
# try and get a keyboard - should always be event0 as
# we're only plugging one thing in
self.dev = evdev.InputDevice('/dev/input/event{}'.format(
event_id))
self.have_kb = True
except OSError:
print('Keyboard not found, waiting 3 seconds and retrying')
sleep(3)
print('found a keyboard')
def update_mod_keys(self, mod_key, value):
"""
Which modifier keys are active is stored in an 8 bit number.
Each bit represents a different key. This method takes which bit
and its new value as input
:param mod_key: The value of the bit to be updated with new value
:param value: Binary 1 or 0 depending if pressed or released
"""
bit_mask = 1 << (7-mod_key)
if value: # set bit
self.mod_keys |= bit_mask
else: # clear bit
self.mod_keys &= ~bit_mask
def update_keys(self, norm_key, value):
if value < 1:
self.pressed_keys.remove(norm_key)
elif norm_key not in self.pressed_keys:
self.pressed_keys.insert(0, norm_key)
len_delta = self.target_length - len(self.pressed_keys)
if len_delta < 0:
self.pressed_keys = self.pressed_keys[:len_delta]
elif len_delta > 0:
self.pressed_keys.extend([0] * len_delta)
@property
def state(self):
"""
property with the HID message to send for the current keys pressed
on the keyboards
:return: bytes of HID message
"""
return [0xA1, 0x01, self.mod_keys, 0, *self.pressed_keys]
def send_keys(self):
self.btk_service.send_keys(self.state)
def event_loop(self):
"""
Loop to check for keyboard events and send HID message
over D-Bus keyboard service when they happen
"""
print('Listening...')
for event in self.dev.read_loop():
# only bother if we hit a key and its an up or down event
if event.type == evdev.ecodes.EV_KEY and event.value < 2:
key_str = evdev.ecodes.KEY[event.code]
mod_key = keymap.modkey(key_str)
if mod_key > -1:
self.update_mod_keys(mod_key, event.value)
else:
self.update_keys(keymap.convert(key_str), event.value)
self.send_keys()
if __name__ == '__main__':
print('Setting up keyboard')
kb = Kbrd()
print('starting event loop')
kb.event_loop()
#
# www.linuxuser.co.uk/tutorials/emulate-a-bluetooth-keyboard-with-the-raspberry-pi
#
#
#
# Convert value returned from Linux event device ("evdev") to a HID code.
# This is reverse of what's actually hardcoded in the kernel.
#
# Lubomir Rintel <lkundrak@v3.sk>
# License: GPL
#
# Ported to a Python module by Liam Fraser.
#
keytable = {
"KEY_RESERVED": 0,
"KEY_ESC": 41,
"KEY_1": 30,
"KEY_2": 31,
"KEY_3": 32,
"KEY_4": 33,
"KEY_5": 34,
"KEY_6": 35,
"KEY_7": 36,
"KEY_8": 37,
"KEY_9": 38,
"KEY_0": 39,
"KEY_MINUS": 45,
"KEY_EQUAL": 46,
"KEY_BACKSPACE": 42,
"KEY_TAB": 43,
"KEY_Q": 20,
"KEY_W": 26,
"KEY_E": 8,
"KEY_R": 21,
"KEY_T": 23,
"KEY_Y": 28,
"KEY_U": 24,
"KEY_I": 12,
"KEY_O": 18,
"KEY_P": 19,
"KEY_LEFTBRACE": 47,
"KEY_RIGHTBRACE": 48,
"KEY_ENTER": 40,
"KEY_LEFTCTRL": 224,
"KEY_A": 4,
"KEY_S": 22,
"KEY_D": 7,
"KEY_F": 9,
"KEY_G": 10,
"KEY_H": 11,
"KEY_J": 13,
"KEY_K": 14,
"KEY_L": 15,
"KEY_SEMICOLON": 51,
"KEY_APOSTROPHE": 52,
"KEY_GRAVE": 53,
"KEY_LEFTSHIFT": 225,
"KEY_BACKSLASH": 50,
"KEY_Z": 29,
"KEY_X": 27,
"KEY_C": 6,
"KEY_V": 25,
"KEY_B": 5,
"KEY_N": 17,
"KEY_M": 16,
"KEY_COMMA": 54,
"KEY_DOT": 55,
"KEY_SLASH": 56,
"KEY_RIGHTSHIFT": 229,
"KEY_KPASTERISK": 85,
"KEY_LEFTALT": 226,
"KEY_SPACE": 44,
"KEY_CAPSLOCK": 57,
"KEY_F1": 58,
"KEY_F2": 59,
"KEY_F3": 60,
"KEY_F4": 61,
"KEY_F5": 62,
"KEY_F6": 63,
"KEY_F7": 64,
"KEY_F8": 65,
"KEY_F9": 66,
"KEY_F10": 67,
"KEY_NUMLOCK": 83,
"KEY_SCROLLLOCK": 71,
"KEY_KP7": 95,
"KEY_KP8": 96,
"KEY_KP9": 97,
"KEY_KPMINUS": 86,
"KEY_KP4": 92,
"KEY_KP5": 93,
"KEY_KP6": 94,
"KEY_KPPLUS": 87,
"KEY_KP1": 89,
"KEY_KP2": 90,
"KEY_KP3": 91,
"KEY_KP0": 98,
"KEY_KPDOT": 99,
"KEY_ZENKAKUHANKAKU": 148,
"KEY_102ND": 100,
"KEY_F11": 68,
"KEY_F12": 69,
"KEY_RO": 135,
"KEY_KATAKANA": 146,
"KEY_HIRAGANA": 147,
"KEY_HENKAN": 138,
"KEY_KATAKANAHIRAGANA": 136,
"KEY_MUHENKAN": 139,
"KEY_KPJPCOMMA": 140,
"KEY_KPENTER": 88,
"KEY_RIGHTCTRL": 228,
"KEY_KPSLASH": 84,
"KEY_SYSRQ": 70,
"KEY_RIGHTALT": 230,
"KEY_HOME": 74,
"KEY_UP": 82,
"KEY_PAGEUP": 75,
"KEY_LEFT": 80,
"KEY_RIGHT": 79,
"KEY_END": 77,
"KEY_DOWN": 81,
"KEY_PAGEDOWN": 78,
"KEY_INSERT": 73,
"KEY_DELETE": 76,
"KEY_MUTE": 239,
"KEY_VOLUMEDOWN": 238,
"KEY_VOLUMEUP": 237,
"KEY_POWER": 102,
"KEY_KPEQUAL": 103,
"KEY_PAUSE": 72,
"KEY_KPCOMMA": 133,
"KEY_HANGEUL": 144,
"KEY_HANJA": 145,
"KEY_YEN": 137,
"KEY_LEFTMETA": 227,
"KEY_RIGHTMETA": 231,
"KEY_COMPOSE": 101,
"KEY_STOP": 243,
"KEY_AGAIN": 121,
"KEY_PROPS": 118,
"KEY_UNDO": 122,
"KEY_FRONT": 119,
"KEY_COPY": 124,
"KEY_OPEN": 116,
"KEY_PASTE": 125,
"KEY_FIND": 244,
"KEY_CUT": 123,
"KEY_HELP": 117,
"KEY_CALC": 251,
"KEY_SLEEP": 248,
"KEY_WWW": 240,
"KEY_COFFEE": 249,
"KEY_BACK": 241,
"KEY_FORWARD": 242,
"KEY_EJECTCD": 236,
"KEY_NEXTSONG": 235,
"KEY_PLAYPAUSE": 232,
"KEY_PREVIOUSSONG": 234,
"KEY_STOPCD": 233,
"KEY_REFRESH": 250,
"KEY_EDIT": 247,
"KEY_SCROLLUP": 245,
"KEY_SCROLLDOWN": 246,
"KEY_F13": 104,
"KEY_F14": 105,
"KEY_F15": 106,
"KEY_F16": 107,
"KEY_F17": 108,
"KEY_F18": 109,
"KEY_F19": 110,
"KEY_F20": 111,
"KEY_F21": 112,
"KEY_F22": 113,
"KEY_F23": 114,
"KEY_F24": 115
}
# Map modifier keys to array element in the bit array
modkeys = {
"KEY_RIGHTMETA": 0,
"KEY_RIGHTALT": 1,
"KEY_RIGHTSHIFT": 2,
"KEY_RIGHTCTRL": 3,
"KEY_LEFTMETA": 4,
"KEY_LEFTALT": 5,
"KEY_LEFTSHIFT": 6,
"KEY_LEFTCTRL": 7
}
def convert(evdev_keycode):
return keytable[evdev_keycode]
def modkey(evdev_keycode):
if evdev_keycode in modkeys:
return modkeys[evdev_keycode]
else:
return -1 # Return an invalid array element
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy context="default">
<allow own="org.yaptb.btkbservice"/>
<allow send_destination="org.yaptb.btkbservice"/>
</policy>
</busconfig>
<?xml version="1.0" encoding="UTF-8" ?>
<record>
<attribute id="0x0001">
<sequence>
<uuid value="0x1124" />
</sequence>
</attribute>
<attribute id="0x0004">
<sequence>
<sequence>
<uuid value="0x0100" />
<uint16 value="0x0011" />
</sequence>
<sequence>
<uuid value="0x0011" />
</sequence>
</sequence>
</attribute>
<attribute id="0x0005">
<sequence>
<uuid value="0x1002" />
</sequence>
</attribute>
<attribute id="0x0006">
<sequence>
<uint16 value="0x656e" />
<uint16 value="0x006a" />
<uint16 value="0x0100" />
</sequence>
</attribute>
<attribute id="0x0009">
<sequence>
<sequence>
<uuid value="0x1124" />
<uint16 value="0x0100" />
</sequence>
</sequence>
</attribute>
<attribute id="0x000d">
<sequence>
<sequence>
<sequence>
<uuid value="0x0100" />
<uint16 value="0x0013" />
</sequence>
<sequence>
<uuid value="0x0011" />
</sequence>
</sequence>
</sequence>
</attribute>
<attribute id="0x0100">
<text value="Raspberry Pi Virtual Keyboard" />
</attribute>
<attribute id="0x0101">
<text value="USB > BT Keyboard" />
</attribute>
<attribute id="0x0102">
<text value="Raspberry Pi" />
</attribute>
<attribute id="0x0200">
<uint16 value="0x0100" />
</attribute>
<attribute id="0x0201">
<uint16 value="0x0111" />
</attribute>
<attribute id="0x0202">
<uint8 value="0x40" />
</attribute>
<attribute id="0x0203">
<uint8 value="0x00" />
</attribute>
<attribute id="0x0204">
<boolean value="false" />
</attribute>
<attribute id="0x0205">
<boolean value="false" />
</attribute>
<attribute id="0x0206">
<sequence>
<sequence>
<uint8 value="0x22" />
<text encoding="hex" value="05010906a101850175019508050719e029e715002501810295017508810395057501050819012905910295017503910395067508150026ff000507190029ff8100c0050c0901a1018503150025017501950b0a23020a21020ab10109b809b609cd09b509e209ea09e9093081029501750d8103c0" />
</sequence>
</sequence>
</attribute>
<attribute id="0x0207">
<sequence>
<sequence>
<uint16 value="0x0409" />
<uint16 value="0x0100" />
</sequence>
</sequence>
</attribute>
<attribute id="0x020b">
<uint16 value="0x0100" />
</attribute>
<attribute id="0x020c">
<uint16 value="0x0c80" />
</attribute>
<attribute id="0x020d">
<boolean value="true" />
</attribute>
<attribute id="0x020e">
<boolean value="false" />
</attribute>
<attribute id="0x020f">
<uint16 value="0x0640" />
</attribute>
<attribute id="0x0210">
<uint16 value="0x0320" />
</attribute>
</record>
@yesimxev
Copy link

This command will act like a bluetooth device without screen or keyboard (like a headset where you can't input pairing code).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment