Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connect multiple Clients to BLE Server #128

Open
metinkale38 opened this issue Mar 20, 2024 · 9 comments
Open

Connect multiple Clients to BLE Server #128

metinkale38 opened this issue Mar 20, 2024 · 9 comments
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@metinkale38
Copy link

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
Copy link

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

@juliencouturier
Copy link

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
Copy link

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
Copy link

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

@tmcg0
Copy link

tmcg0 commented Sep 19, 2024

@mklemarczyk any luck on that?

@kevincar kevincar added enhancement New feature or request help wanted Extra attention is needed labels Sep 21, 2024
@kevincar
Copy link
Owner

@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
Copy link

mklemarczyk commented Oct 3, 2024

@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
Copy link

tmcg0 commented Oct 4, 2024

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

@Jakeler
Copy link
Contributor

Jakeler commented Dec 7, 2024

Surprisingly this completely works in my testing. I can connect with 2 clients and the data is forwarded as expected: when the server sends all clients receive the notify, also each of the clients can individually send to the server.

Tested on Arch Linux (default configs regarding bluez) and Windows. Cheap Bluetooth 5.0 dongle based on Realtek RTL8761B: https://linux-hardware.org/?id=usb:0bda-8771

Code Jakeler/ble-serial#60:
https://github.com/Jakeler/ble-serial/blob/server-mode/ble_serial/bluetooth/ble_server.py

I would even say it works too well right now. While there is some logging on client connect, as someone running a server you don't really know or control who is receiving or sending. The feature #114 with metadata would already help a lot. Also it would be good to have a method to disconnect specific clients and a configurable limit/filter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

7 participants