From 73df805af08793ff681d90e9f272544e3518b504 Mon Sep 17 00:00:00 2001 From: mpeter Date: Fri, 8 Aug 2025 19:38:00 +0200 Subject: [PATCH 1/3] use pybluez bluetooth instead of linux socket directly on some distributions (incl. openSUSE) python doesnt have bluetooth support built-in: https://github.com/TheWeirdDev/Bluetooth_Headset_Battery_Level/issues/19#issuecomment-632573359 --- BluetoothHID.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/BluetoothHID.py b/BluetoothHID.py index 52cbc44..fd851b5 100644 --- a/BluetoothHID.py +++ b/BluetoothHID.py @@ -1,3 +1,4 @@ +import bluetooth # requires pybluez package import dbus import dbus.service import os @@ -65,12 +66,12 @@ class BluetoothHIDService(object): "Role": "server" } - sock_control = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP) - sock_control.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock_inter = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP) - sock_inter.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock_control = L2CAPSocket(True) + sock_inter = L2CAPSocket(True) + sock_control.bind((self.SELFMAC, self.P_CTRL)) sock_inter.bind((self.SELFMAC, self.P_INTR)) + manager.RegisterProfile(self.PROFILE_PATH, "00001124-0000-1000-8000-00805f9b34fb", opts) print("Registered") sock_control.listen(1) @@ -83,3 +84,13 @@ class BluetoothHIDService(object): def send(self, bytes_buf): self.cinter.send(bytes_buf) + + +# https://github.com/karulis/pybluez/blob/master/examples/simple/l2capserver.py +class L2CAPSocket(bluetooth.BluetoothSocket): + + def __init__(self, reuseaddr: False): + super().__init__(bluetooth.L2CAP) + + if reuseaddr: + self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) \ No newline at end of file From 03be0d7eec5d9d61e04e5ab2873cfeb35cd77098 Mon Sep 17 00:00:00 2001 From: mpeter Date: Fri, 8 Aug 2025 19:46:28 +0200 Subject: [PATCH 2/3] refactored BluetoothHID.py, added comments for future maintainability --- BluetoothHID.py | 52 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/BluetoothHID.py b/BluetoothHID.py index fd851b5..3986eaf 100644 --- a/BluetoothHID.py +++ b/BluetoothHID.py @@ -5,6 +5,15 @@ import os import socket +# see section "2.5 PSMs and SPSMs": https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Assigned_Numbers/out/en/Assigned_Numbers.pdf +BLUETOOTH_PORT_HID_CONTROL = 0x0011 +BLUETOOTH_PORT_HID_INTERRUPT = 0x0013 + +# see section "3.3 SDP Service Class and Profile Identifiers" of https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Assigned_Numbers/out/en/Assigned_Numbers.pdf +# and Vol 3, Part B, section 2.5.1 of https://www.bluetooth.com/specifications/specs/core-specification-5-3/ +BLUETOOT_PROFILE_HID = "00001124-0000-1000-8000-00805f9b34fb" + + class BluetoothHIDProfile(dbus.service.Object): def __init__(self, bus, path): super(BluetoothHIDProfile, self).__init__(bus, path) @@ -43,40 +52,51 @@ def error_handler(e): class BluetoothHIDService(object): - PROFILE_PATH = "/org/bluez/bthid_profile" + PROFILE_PATH = "/org/bluez/emubthid_profile" HOST = 0 PORT = 1 def __init__(self, service_record, MAC): - self.P_CTRL = 0x0011 - self.P_INTR = 0x0013 self.SELFMAC = MAC + bus = dbus.SystemBus() bluez_obj = bus.get_object("org.bluez", "/org/bluez") manager = dbus.Interface(bluez_obj, "org.bluez.ProfileManager1") BluetoothHIDProfile(bus, self.PROFILE_PATH) - opts = { - "ServiceRecord": service_record, - "Name": "BTKeyboardProfile", - "RequireAuthentication": False, - "RequireAuthorization": False, - "Service": "MY BTKBD", - "Role": "server" - } sock_control = L2CAPSocket(True) sock_inter = L2CAPSocket(True) - sock_control.bind((self.SELFMAC, self.P_CTRL)) - sock_inter.bind((self.SELFMAC, self.P_INTR)) + # The PSM parameter sets the bluetooth "port" for L2CAP connections + # "Valid values in the first range are assigned by the Bluetooth SIG and indicate protocols." + # https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-54/out/en/host/logical-link-control-and-adaptation-protocol-specification.html + sock_control.bind((self.SELFMAC, BLUETOOTH_PORT_HID_CONTROL)) + sock_inter.bind((self.SELFMAC, BLUETOOTH_PORT_HID_INTERRUPT)) - manager.RegisterProfile(self.PROFILE_PATH, "00001124-0000-1000-8000-00805f9b34fb", opts) - print("Registered") + # IDEs dont see this function because bluetooth.BluetoothSocket imports them dynamically sock_control.listen(1) sock_inter.listen(1) - print(f"waiting for connection at controller {MAC}, please double check with the MAC in bluetoothctl") + + manager.RegisterProfile(self.PROFILE_PATH, BLUETOOT_PROFILE_HID, { + "Name": "Emulated Bluetooth keyboard", + # "Service": "MY BTKBD", # this refers to the bluetooth spec service class uuid + "Role": "server", + "RequireAuthentication": True, + "RequireAuthorization": True, + "ServiceRecord": service_record, + # note: it seems as if socket creation could be replaced with working through DBus (see PSM option and NewConnection function). + # maybe currently the conenction is somehow handled at 2 places at the same time? + # + # https://www.bluez.org/bluez-5-api-introduction-and-porting-guide/ + # "The new Profile1 interface (and removal of org.bluez.Service)" + }) + + print("Registered Bluetooth HID profile in Bluez") + + print(f"Waiting for connection at controller {MAC}, please double check with the MAC in bluetoothctl") + self.ccontrol, cinfo = sock_control.accept() print("Control channel connected to " + cinfo[self.HOST]) self.cinter, cinfo = sock_inter.accept() From 6f5da9c6ac8ad1103c33256a3f6228ea74c39bf2 Mon Sep 17 00:00:00 2001 From: mpeter Date: Fri, 8 Aug 2025 20:40:10 +0200 Subject: [PATCH 3/3] read BT MAC from argument, instead of having to edit source code --- BluetoothHID.py | 3 +++ README.md | 6 +----- main.py | 18 ++++++++++-------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/BluetoothHID.py b/BluetoothHID.py index 3986eaf..328ff8d 100644 --- a/BluetoothHID.py +++ b/BluetoothHID.py @@ -58,6 +58,9 @@ class BluetoothHIDService(object): PORT = 1 def __init__(self, service_record, MAC): + if not bluetooth.is_valid_address(MAC): + raise ValueError(f"'{MAC}' is not a valid MAC address") + self.SELFMAC = MAC bus = dbus.SystemBus() diff --git a/README.md b/README.md index 7fa3f5c..e15a79c 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,6 @@ The client who uses the emulated HID device is an android 9 phone. ## How to Use -### (IMPORTANT) Update the bluetooth controller MAC in `main.py` - -Edit `main.py` and change the `CONTROLLER_MAC` variable in the beginning to your own MAC. You can find the MAC of the bluetooth controller in `bluetoothctl`. E.g. the "5C:87:9C:96:BE:5E" shown in the screenshot below is the MAC. - ### Enable bluetooth 1. make sure that bluetoothd has the plugin `input` disabled (i.e. "-P input"). @@ -67,7 +63,7 @@ Edit `main.py` and change the `CONTROLLER_MAC` variable in the beginning to your 5. Run `xhost +` to enable root user also draw something on a non-root user's X session. (see [this stackoverflow](https://stackoverflow.com/questions/31902846/how-to-fix-error-xlib-error-displayconnectionerror-cant-connect-to-display-0)) -6. Run `sudo python3 main.py` +6. Run `sudo python3 main.py ` In bluetoothctl, it should look like this, where a lot of UUIDs are registered diff --git a/main.py b/main.py index 4c3e660..ef86572 100755 --- a/main.py +++ b/main.py @@ -1,19 +1,12 @@ #!/usr/bin/python3 import sys -import os -import time from BluetoothHID import BluetoothHIDService from evdev_xkb_map import evdev_xkb_map, modkeys import keymap from Xlib import X, display, Xutil from dbus.mainloop.glib import DBusGMainLoop -""" -Change this CONTROLLER_MAC to the mac of your own device -""" -CONTROLLER_MAC = "5C:87:9C:96:BE:5E" - usbhid_map = {} with open("keycode.txt") as f: for line in f.read().splitlines(): @@ -222,13 +215,22 @@ class Window(object): else: prev_y = e.event_y + if __name__ == '__main__': + try: + bt_controller_mac = sys.argv[1] + except IndexError: + print("You need to pass the MAC address of your Bluetooth controller in the first argument.") + print("You can find the MAC of the bluetooth controller in `bluetoothctl`. In its format it is similar to \"5C:87:9C:96:BE:5E\".") + exit(1) + DBusGMainLoop(set_as_default=True) service_record = open("sdp_record_kbd.xml").read() d = display.Display() d.change_keyboard_control(auto_repeat_mode=X.AutoRepeatModeOff) + try: - bthid_srv = BluetoothHIDService(service_record, CONTROLLER_MAC) + bthid_srv = BluetoothHIDService(service_record, bt_controller_mac) Window(d).loop(bthid_srv.send) #Window(d).loop(print) finally: