kevincar / bless

Cross-platform Bluetooth Low Energy Server Python Library
MIT License
97 stars 29 forks source link

Connect multiple Clients to BLE Server #128

Open metinkale38 opened 6 months ago

metinkale38 commented 6 months ago

Hello,

i implemented a server with bless successfully and it is working. I used the Basic Server Example.

The only problem is, that i can only connect with one device. While i am connected to the BLE-Server, other Devices cant see it anymore. I have to disconnect the first device to connect another.

I am not sure, whether this is a BT or Bless limitation, but it would be very helpfull, if there is a solution for that problem.

decodeais commented 6 months ago

You are not alone, in this moment I made the same test. I changed the order. Only the first is working.

juliencouturier commented 5 months ago

I have the same issue on linux but not on windows : I have to restart the bluetooth service and my script to be able to connect another client.

decodeais commented 5 months ago

I checked the BLE source code and found somewhere in the script that only the first service is advertised. When I tried to change it, I had no success. There seemed to be a limitation. May be it is really hardware dependend.

mklemarczyk commented 3 months ago

Hello, I will be working on the multi-client scenario. I will check your problem.

tmcg0 commented 2 weeks ago

@mklemarczyk any luck on that?

kevincar commented 1 week ago

@metinkale38 Thanks for bringing this up. Without diving into this yet, this issue seems to be a bless limitation more so than a hardware one. That said, we've definitely run into OS-specific interface challenges between windows, linux, and macOS.

As of BLE 4.1, limitations on peripheral vs central was removed and BLE 5.0 no longer has any limitations. However, although the specifications do not limit this, this could be limited at the hardware level and even at the OS level if using a built in chip, which most systems come with.

Either way worth looking into.

mklemarczyk commented 23 hours ago

@tmcg0 No luck so far. I will need to acquire some more hardware for testing. Currently to update you I am waiting for my new Raspberry Pi 5.

The often cause of multi-client not working is auto-disable of advertisement when first client is connected. I am digging into that.

It is also important for my current project as I have n temperature reporting stations (BTstack) to central hub (bless). They connect when they power on to hub to send new data and than power down. You can imagine 8 stations connecting to central hub at the same time every minute. Currently I kind of offset the code in remote so they do not clash.

tmcg0 commented 6 hours ago

Awesome, will definitely keep an eye out for anything you think might be in the right direction. I took a look through the codebase but am not seeing anything obvious that would cause this. I think you're right that the root cause is the stop advertising after first client connect.

Weirdly enough for the project I'm working on I switched to using the python dbus bindings, and all seemed well for a bit, but now it seems this issue has emerged again with that implementation. Which makes me feel the issue is OS-level, and I'm not sure if there's an elegant cross-platform (if the issue is cross-platform) way to handle this. FWIW I'm on an Raspberry Pi 5 running their 64-bit Debian bookworm-based OS.

I've found a few OS-level tweaks that seem to help, but not sure exactly which subsets of these operations are actually helping. And obviously still haven't fully tracked down whatever is stopping advertisement.

In case it helps anyone, here's some functions from my current codebase that do some of the OS-level operations:


def setup_bluetooth():
    """Setup the Bluetooth adapter"""
    if os.name == "posix" and os.uname().sysname == "Linux":
        try:
            subprocess.run(["sudo", "systemctl", "stop", "bluetooth"], check=True)
            subprocess.run(["sudo", "hciconfig", "hci0", "down"], check=True)
            subprocess.run(["sudo", "hciconfig", "hci0", "up"], check=True)

            if ensure_bluetooth_config():
                ble_logger.info("Bluetooth configuration is set up correctly.")

                # Restart the Bluetooth service
                subprocess.run(
                    ["sudo", "systemctl", "restart", "bluetooth"], check=True
                )

                # Wait for the Bluetooth service to fully start
                time.sleep(2)

                # Disable Bluetooth agent and set advertising
                subprocess.run(
                    ["sudo", "btmgmt", "-i", "hci0", "pairable", "off"], check=True
                )
                subprocess.run(
                    ["sudo", "btmgmt", "-i", "hci0", "connectable", "on"], check=True
                )
                subprocess.run(
                    ["sudo", "btmgmt", "-i", "hci0", "advertising", "on"], check=True
                )

                # Modify /etc/bluetooth/input.conf so that the device doesn't require input for pairing
                ensure_bluetooth_input_config()

                ble_logger.info("Bluetooth setup completed successfully.")
            else:
                ble_logger.warning("Failed to set up Bluetooth configuration.")
        except subprocess.CalledProcessError as e:
            ble_logger.error(f"Error during Bluetooth setup: {e}", exc_info=True)
    else:
        ble_logger.info(
            f"Bluetooth setup routine not implemented for this OS, {os.name}"
        )

def ensure_bluetooth_input_config():
    """Ensure the Bluetooth input configuration is set up correctly"""
    required_lines = ["IdleTimeout=0"]

    try:
        # If the file doesn't exist, create it
        if not BLE_INPUT_CONFIG_FILE.exists():
            subprocess.run(["sudo", "touch", str(BLE_INPUT_CONFIG_FILE)], check=True)

        # Copy the file to a temporary location
        subprocess.run(
            ["sudo", "cp", str(BLE_INPUT_CONFIG_FILE), str(BLE_TMP_INPUT_CONFIG_FILE)],
            check=True,
        )

        # Change permissions of the temporary file
        subprocess.run(
            ["sudo", "chmod", "666", str(BLE_TMP_INPUT_CONFIG_FILE)], check=True
        )

        # Read the current content of the temporary file
        with BLE_TMP_INPUT_CONFIG_FILE.open("r") as f:
            content = f.readlines()

        # Check if [General] section exists, if not add it
        if not any(line.strip() == "[General]" for line in content):
            content.insert(0, "[General]\n")

        # Check and add required lines
        lines_to_add = required_lines.copy()
        for i, line in enumerate(content):
            for req_line in required_lines:
                if line.strip().startswith(req_line.split("=")[0]):
                    content[i] = req_line + "\n"
                    if req_line in lines_to_add:
                        lines_to_add.remove(req_line)

        # Append any remaining required lines
        content.extend([line + "\n" for line in lines_to_add])

        # Write the updated content back to the temporary file
        with BLE_TMP_INPUT_CONFIG_FILE.open("w") as f:
            f.writelines(content)

        # Copy the temporary file back to the original location
        subprocess.run(
            ["sudo", "cp", str(BLE_TMP_INPUT_CONFIG_FILE), str(BLE_INPUT_CONFIG_FILE)],
            check=True,
        )

        # Remove the temporary file
        BLE_TMP_INPUT_CONFIG_FILE.unlink()

        ble_logger.info("Bluetooth input configuration updated successfully.")
        return True

    except Exception as e:
        ble_logger.error(
            f"Error updating Bluetooth input configuration: {str(e)}", exc_info=True
        )
        return False

def ensure_bluetooth_config() -> bool:
    """Ensure the Bluetooth configuration is set up correctly to prevent pairing

    Returns:
        bool: True if the configuration was updated successfully, False otherwise
    """
    required_lines = ["NoInputNoOutput=true", "Pairable=false", "PairableTimeout=0"]

    try:
        # Update bluetooth.conf
        if not update_config_file(required_lines):
            return False

        # Update dbus bluetooth.conf
        if not update_dbus_config():
            return False

        ble_logger.info("Bluetooth configuration updated successfully.")
        return True

    except Exception as e:
        ble_logger.error(
            f"Error updating Bluetooth configuration: {str(e)}", exc_info=True
        )
        return False

def update_config_file(required_lines: list[str]) -> bool:
    """Update the BLE configuration file with the required lines

    Args:
        required_lines (list[str]): The list of required lines to add to the configuration file

    Returns:
        bool: True if the configuration was updated successfully, False otherwise
    """
    # ensure the config file exists
    if not BLE_CONFIG_FILE.exists():
        ble_logger.warning(f"Config file {BLE_CONFIG_FILE} does not exist.")
        return False

    try:
        # copy the config file to a temporary location
        subprocess.run(
            ["sudo", "cp", str(BLE_CONFIG_FILE), str(BLE_TMP_CONFIG_FILE)], check=True
        )
        subprocess.run(["sudo", "chmod", "666", str(BLE_TMP_CONFIG_FILE)], check=True)

        # read the current content
        with BLE_TMP_CONFIG_FILE.open("r") as f:
            content = f.readlines()

        # check and add required lines
        lines_to_add = required_lines.copy()
        for i, line in enumerate(content):
            for req_line in required_lines:
                if line.strip().startswith(req_line.split("=")[0]):
                    content[i] = req_line + "\n"
                    if req_line in lines_to_add:
                        lines_to_add.remove(req_line)

        content.extend([line + "\n" for line in lines_to_add])

        # write the updated content back to the temporary file
        with BLE_TMP_CONFIG_FILE.open("w") as f:
            f.writelines(content)

        # copy the temporary file back to the original location
        subprocess.run(
            ["sudo", "cp", str(BLE_TMP_CONFIG_FILE), str(BLE_CONFIG_FILE)], check=True
        )

        # remove the temporary file
        BLE_TMP_CONFIG_FILE.unlink()
        return True

    except Exception as e:
        ble_logger.error(f"Error updating {BLE_CONFIG_FILE}: {str(e)}", exc_info=True)
        return False

def update_dbus_config() -> bool:
    """Update the D-Bus configuration file to allow communication with BlueZ

    Returns:
        bool: True if the configuration was updated successfully, False otherwise
    """
    # check if the dbus config file exists
    if not BLE_DBUS_CONFIG_FILE.exists():
        ble_logger.warning(f"D-Bus config file {BLE_DBUS_CONFIG_FILE} does not exist.")
        return False

    try:
        # copy the dbus config file to a temporary location
        subprocess.run(
            ["sudo", "cp", str(BLE_DBUS_CONFIG_FILE), str(BLE_TMP_DBUS_CONFIG_FILE)],
            check=True,
        )
        subprocess.run(
            ["sudo", "chmod", "666", str(BLE_TMP_DBUS_CONFIG_FILE)], check=True
        )

        # read the current content
        tree = ET.parse(str(BLE_TMP_DBUS_CONFIG_FILE))
        root = tree.getroot()

        # check if the policy group exists, if not add it
        policy = root.find(".//policy[@group='bluetooth']")
        if policy is None:
            policy = ET.SubElement(root, "policy", group="bluetooth")

        # check if the allow element exists, if not add it
        allow = policy.find("./allow[@send_destination='org.bluez']")
        if allow is None:
            ET.SubElement(policy, "allow", send_destination="org.bluez")

        # write the updated content back to the temporary file
        tree.write(str(BLE_TMP_DBUS_CONFIG_FILE))

        # copy the temporary file back to the original location
        subprocess.run(
            ["sudo", "cp", str(BLE_TMP_DBUS_CONFIG_FILE), str(BLE_DBUS_CONFIG_FILE)],
            check=True,
        )

        # remove the temporary file
        BLE_TMP_DBUS_CONFIG_FILE.unlink()
        return True

    except Exception as e:
        ble_logger.error(f"Error updating D-Bus configuration: {str(e)}", exc_info=True)
        return False