hypfvieh / bluez-dbus

bluetooth library for linux OSes using DBus and bluez (http://www.bluez.org/).
MIT License
68 stars 20 forks source link

Example code for a GattServer #63

Open mtexplorative opened 3 weeks ago

mtexplorative commented 3 weeks ago

Hi, we are trying to implement Bluetooth as an additional path to control an IoT-Device. Our current set-up regarding the compute hardware is a Raspberry Pi (may changed later) but will still be Linux ;) Our functionality is already accessible via REST-calls provided by a micronaut-based Java program. I struggle in setting-up a GattServer with this library. I would appreciate it if a simple example code could be provided showing how to provide a read function on a property (incl. advertsing it). Regards, Markus.

hypfvieh commented 3 weeks ago

Providing a Gatt-Server is not really straight forward using bluez and dbus. Without any warranty, here is some sample code, maybe you can start with that. Other samples in Python and C can be found in the bluez.git Repository (see tools and test directory).

package com.github.hypfvieh.sandbox.bluez;

import org.bluez.GattCharacteristic1;
import org.bluez.GattDescriptor1;
import org.bluez.GattManager1;
import org.bluez.GattService1;
import org.bluez.datatypes.TwoTuple;
import org.bluez.exceptions.*;
import org.freedesktop.dbus.DBusPath;
import org.freedesktop.dbus.FileDescriptor;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder;
import org.freedesktop.dbus.interfaces.DBusInterface;
import org.freedesktop.dbus.interfaces.Properties;
import org.freedesktop.dbus.types.UInt16;
import org.freedesktop.dbus.types.Variant;

import java.util.*;

public class GattExample {
    static final String FLAG_READ = "read";
    static final String FLAG_WRITE = "write";

    public static void main(String[] args) {

        MyGattApplication myGattApplication = new MyGattApplication();
        SampleService sampleService = new SampleService();
        SampleCharacteristics sampleCharacteristics = new SampleCharacteristics(sampleService);
        SampleGattDescriptor sampleDescriptor = new SampleGattDescriptor(sampleCharacteristics);
        sampleCharacteristics.getDescriptors().add(sampleDescriptor);
        myGattApplication.addService(sampleService);

        sampleService.getCharacteristics().add(sampleCharacteristics);

        try (DBusConnection dbus = DBusConnectionBuilder.forSystemBus().build()) {

            dbus.exportObject(myGattApplication);

            GattManager1 gattMgr = dbus.getRemoteObject("org.bluez", "/org/bluez/hci0", GattManager1.class);

            Map<String, Variant<?>> opts = new HashMap<>();

            gattMgr.RegisterApplication(new DBusPath(myGattApplication.getObjectPath()), opts);
            Thread.sleep(50000000L); // do something useful or a proper wait loop
        } catch (Exception _ex) {
            // TODO Auto-generated catch block
            _ex.printStackTrace();
        }
    }

    public static class MyGattApplication implements GattServiceApp {

        private final List<SampleService> services = new ArrayList<>();

        @Override
        public String getObjectPath() {
            return "/";
        }

        public void addService(SampleService _service) {
            services.add(_service);
        }

        @Override
        public Map<DBusPath, Map<String, Map<String, Variant<?>>>> GetManagedObjects() {

            Map<DBusPath, Map<String, Map<String, Variant<?>>>> result = new LinkedHashMap<>();

            for (SampleService svc : services) {
                result.put(new DBusPath(svc.getObjectPath()), svc.getProps());
                for (SampleCharacteristics chr : svc.getCharacteristics()) {
                    result.put(new DBusPath(chr.getObjectPath()), chr.getProps());
                    for (SampleGattDescriptor descriptor : chr.getDescriptors()) {
                        result.put(new DBusPath(descriptor.getObjectPath()), descriptor.getProps());
                    }
                }
            }

            return result;
        }

    }

    public static class SampleService implements GattService1, PropertiesHelper {
        static final String BASE_PATH = "/mygatt/application/";
        // This should be a static value, not a random one
        private final String uuid = UUID.randomUUID().toString();
        private final Boolean primary = true;
        private final List<SampleCharacteristics> characteristics = new ArrayList<>();

        public List<SampleCharacteristics> getCharacteristics() {
            return characteristics;
        }

        @Override
        public String getObjectPath() {
            return BASE_PATH + "service0";
        }

        public String getUuid() {
            return uuid;
        }

        public boolean isPrimary() {
            return primary;
        }

        @SuppressWarnings("unchecked")
        @Override
        public Variant<?> Get(String _interfaceName, String _propertyName) {
            if (!getBluezInterface().getName().equals(_interfaceName)) {
                throw new IllegalArgumentException();
            }

            if ("UUID".equals(_propertyName)) {
                return new Variant<>(uuid);
            } else if ("Primary".equals(_propertyName)) {
                return new Variant<>(primary);
            } else if ("Characteristics".equals(_propertyName)) {
                return getCharacteristicsAsVariantArray();
            }
            return null;
        }

        @Override
        public <A> void Set(String _interfaceName, String _propertyName, A _value) {
            throw new UnsupportedOperationException();
        }

        @Override
        public Map<String, Variant<?>> GetAll(String _interfaceName) {
            return Map.of("UUID", new Variant<>(uuid),
                "Primary", new Variant<>(primary),
                "Characteristics", getCharacteristicsAsVariantArray()
                );
        }

        @Override
        public Class<? extends DBusInterface> getBluezInterface() {
            return GattService1.class;
        }

        private Variant<List<DBusPath>> getCharacteristicsAsVariantArray() {
            List<DBusPath> pathes = new ArrayList<>();
            getCharacteristics().stream().map(d -> new DBusPath(d.getObjectPath())).forEach(pathes::add);

            return new Variant<>(pathes, "ao");
        }

    }

    public static class SampleCharacteristics implements GattCharacteristic1, PropertiesHelper {

        private final String uuid = UUID.randomUUID().toString();

        private final SampleService service;
        private final List<String> flags = new ArrayList<>();
        private final List<SampleGattDescriptor> descriptors = new ArrayList<>();

        public SampleCharacteristics(SampleService _service) {
            service = _service;
            flags.add(FLAG_READ);
        }

        public String getUuid() {
            return uuid;
        }

        public SampleService getService() {
            return service;
        }

        public List<SampleGattDescriptor> getDescriptors() {
            return descriptors;
        }

        @Override
        public Class<? extends DBusInterface> getBluezInterface() {
            return GattCharacteristic1.class;
        }

        @Override
        public String getObjectPath() {
            return service.getObjectPath() + "/char0";
        }

        @Override
        public byte[] ReadValue(Map<String, Variant<?>> _options)
            throws BluezFailedException, BluezInProgressException, BluezNotPermittedException, BluezNotAuthorizedException, BluezInvalidOffsetException, BluezNotSupportedException {

            throw new UnsupportedOperationException("Implement if needed");
        }

        @Override
        public void WriteValue(byte[] _value, Map<String, Variant<?>> _options)
            throws BluezFailedException, BluezInProgressException, BluezNotPermittedException, BluezInvalidValueLengthException, BluezNotAuthorizedException, BluezNotSupportedException {
            throw new UnsupportedOperationException("Implement if needed");

        }

        @Override
        public TwoTuple<FileDescriptor, UInt16> AcquireWrite(Map<String, Variant<?>> _options) throws BluezFailedException, BluezNotSupportedException {
            throw new UnsupportedOperationException("Implement if needed");
        }

        @Override
        public TwoTuple<FileDescriptor, UInt16> AcquireNotify(Map<String, Variant<?>> _options) throws BluezFailedException, BluezNotSupportedException {
            throw new UnsupportedOperationException("Implement if needed");
        }

        @Override
        public void StartNotify() throws BluezFailedException, BluezNotPermittedException, BluezInProgressException, BluezNotConnectedException, BluezNotSupportedException {
            throw new UnsupportedOperationException("Implement if needed");

        }

        @Override
        public void StopNotify() throws BluezFailedException {
            throw new UnsupportedOperationException("Implement if needed");

        }

        @Override
        public void Confirm() throws BluezFailedException {
            throw new UnsupportedOperationException("Implement if needed");
        }

        @SuppressWarnings("unchecked")
        @Override
        public Variant<?> Get(String _interfaceName, String _propertyName) {
            if (!getBluezInterface().getName().equals(_interfaceName)) {
                throw new IllegalArgumentException();
            }

            if ("UUID".equals(_propertyName)) {
                return  new Variant<>(uuid);
            } else if ("Service".equals(_propertyName)) {
                return new Variant<>(new DBusPath(service.getObjectPath()));
            } else if ("Descriptors".equals(_propertyName)) {
                return getDescriptorsAsVariantArray();
            } else if ("Flags".equals(_propertyName)) {
                return  new Variant<>(flags, "as");
            }
            return null;
        }

        @Override
        public <A> void Set(String _interfaceName, String _propertyName, A _value) {
            throw new UnsupportedOperationException();
        }

        @Override
        public Map<String, Variant<?>> GetAll(String _interfaceName) {
            return Map.of("UUID", new Variant<>(uuid),
                "Service", new Variant<>(new DBusPath(service.getObjectPath())),
                "Descriptors", getDescriptorsAsVariantArray(),
                "Flags", new Variant<>(flags, "as"));
        }

        private Variant<List<DBusPath>> getDescriptorsAsVariantArray() {
            List<DBusPath> pathes = new ArrayList<>();
            getDescriptors().stream().map(d -> new DBusPath(d.getObjectPath())).forEach(pathes::add);

            return new Variant<>(pathes, "ao");
        }

    }

    public static class SampleGattDescriptor implements GattDescriptor1, PropertiesHelper {

        private final String uuid = UUID.randomUUID().toString();
        private final SampleCharacteristics characteristic;
        private final List<String> flags = new ArrayList<>();

        public SampleGattDescriptor(SampleCharacteristics _characteristics) {
            characteristic = _characteristics;
            flags.add(FLAG_READ);
        }

        public String getUuid() {
            return uuid;
        }

        public SampleCharacteristics getCharacteristic() {
            return characteristic;
        }

        @Override
        public Class<? extends DBusInterface> getBluezInterface() {
            return GattDescriptor1.class;
        }

        @Override
        public String getObjectPath() {
            return characteristic.getObjectPath() + "/desc0";
        }

        @Override
        public byte[] ReadValue(Map<String, Variant<?>> _flags)
            throws BluezFailedException, BluezInProgressException, BluezNotPermittedException, BluezNotAuthorizedException, BluezNotSupportedException {
            throw new UnsupportedOperationException("Implement if needed");
        }

        @Override
        public void WriteValue(byte[] _value, Map<String, Variant<?>> _flags)
            throws BluezFailedException, BluezInProgressException, BluezNotPermittedException, BluezInvalidValueLengthException, BluezNotAuthorizedException, BluezNotSupportedException {
            throw new UnsupportedOperationException("Implement if needed");
        }

        @SuppressWarnings("unchecked")
        @Override
        public Variant<?> Get(String _interfaceName, String _propertyName) {
            if ("UUID".equals(_propertyName)) {
                return new Variant<>(uuid);
            } else if ("Characteristic  ".equals(_propertyName)) {
                return new Variant<>(characteristic);
            } else if ("Flags".equals(_propertyName)) {
                return new Variant<>(flags, "as");
            }
            return null;
        }

        @Override
        public <A> void Set(String _interfaceName, String _propertyName, A _value) {
            throw new UnsupportedOperationException();
        }

        @Override
        public Map<String, Variant<?>> GetAll(String _interfaceName) {
            return Map.of("UUID", new Variant<>(uuid),
                "Characteristic", new Variant<>(new DBusPath(characteristic.getObjectPath())),
                "Flags", new Variant<>(flags, "as"));
        }

    }

    public interface GattServiceApp extends DBusInterface {

        Map<DBusPath, Map<String, Map<String, Variant<?>>>> GetManagedObjects();
    }

    public interface PropertiesHelper extends Properties {

        Class<? extends DBusInterface> getBluezInterface();

        default Map<String, Map<String, Variant<?>>> getProps() {
            Map<String, Map<String, Variant<?>>> result = new LinkedHashMap<>();
            result.put(getBluezInterface().getName(), GetAll(null));
            return result;
        }

    }
}
mtexplorative commented 3 weeks ago

Wow, thank you so much for providing this sketch. I guess I will need some time to work through it. This is exactly what I was looking for. Regards, Markus.

Am 15.08.2024 um 21:08 schrieb David M. @.***>:

Providing a Gatt-Server is not really straight forward using bluez and dbus. Without any warranty, here is some sample code, maybe you can start with that. Other samples in Python and C can be found in the bluez.git https://github.com/bluez/bluez/ Repository (see tools and test directory).

package com.github.hypfvieh.sandbox.bluez;

import org.bluez.GattCharacteristic1; import org.bluez.GattDescriptor1; import org.bluez.GattManager1; import org.bluez.GattService1; import org.bluez.datatypes.TwoTuple; import org.bluez.exceptions.*; import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.FileDescriptor; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.interfaces.Properties; import org.freedesktop.dbus.types.UInt16; import org.freedesktop.dbus.types.Variant;

import java.util.*;

public class GattExample { static final String FLAG_READ = "read"; static final String FLAG_WRITE = "write";

public static void main(String[] args) {

    MyGattApplication myGattApplication = new MyGattApplication();
    SampleService sampleService = new SampleService();
    SampleCharacteristics sampleCharacteristics = new SampleCharacteristics(sampleService);
    SampleGattDescriptor sampleDescriptor = new SampleGattDescriptor(sampleCharacteristics);
    sampleCharacteristics.getDescriptors().add(sampleDescriptor);
    myGattApplication.addService(sampleService);

    sampleService.getCharacteristics().add(sampleCharacteristics);

    try (DBusConnection dbus = DBusConnectionBuilder.forSystemBus().build()) {

        dbus.exportObject(myGattApplication);

        GattManager1 gattMgr = dbus.getRemoteObject("org.bluez", "/org/bluez/hci0", GattManager1.class);

        Map<String, Variant<?>> opts = new HashMap<>();

        gattMgr.RegisterApplication(new DBusPath(myGattApplication.getObjectPath()), opts);
        Thread.sleep(50000000L); // do something useful or a proper wait loop
    } catch (Exception _ex) {
        // TODO Auto-generated catch block
        _ex.printStackTrace();
    }
}

public static class MyGattApplication implements GattServiceApp {

    private final List<SampleService> services = new ArrayList<>();

    @Override
    public String getObjectPath() {
        return "/";
    }

    public void addService(SampleService _service) {
        services.add(_service);
    }

    @Override
    public Map<DBusPath, Map<String, Map<String, Variant<?>>>> GetManagedObjects() {

        Map<DBusPath, Map<String, Map<String, Variant<?>>>> result = new LinkedHashMap<>();

        for (SampleService svc : services) {
            result.put(new DBusPath(svc.getObjectPath()), svc.getProps());
            for (SampleCharacteristics chr : svc.getCharacteristics()) {
                result.put(new DBusPath(chr.getObjectPath()), chr.getProps());
                for (SampleGattDescriptor descriptor : chr.getDescriptors()) {
                    result.put(new DBusPath(descriptor.getObjectPath()), descriptor.getProps());
                }
            }
        }

        return result;
    }

}

public static class SampleService implements GattService1, PropertiesHelper {
    static final String BASE_PATH = "/mygatt/application/";
    // This should be a static value, not a random one
    private final String uuid = UUID.randomUUID().toString();
    private final Boolean primary = true;
    private final List<SampleCharacteristics> characteristics = new ArrayList<>();

    public List<SampleCharacteristics> getCharacteristics() {
        return characteristics;
    }

    @Override
    public String getObjectPath() {
        return BASE_PATH + "service0";
    }

    public String getUuid() {
        return uuid;
    }

    public boolean isPrimary() {
        return primary;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Variant<?> Get(String _interfaceName, String _propertyName) {
        if (!getBluezInterface().getName().equals(_interfaceName)) {
            throw new IllegalArgumentException();
        }

        if ("UUID".equals(_propertyName)) {
            return new Variant<>(uuid);
        } else if ("Primary".equals(_propertyName)) {
            return new Variant<>(primary);
        } else if ("Characteristics".equals(_propertyName)) {
            return getCharacteristicsAsVariantArray();
        }
        return null;
    }

    @Override
    public <A> void Set(String _interfaceName, String _propertyName, A _value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Map<String, Variant<?>> GetAll(String _interfaceName) {
        return Map.of("UUID", new Variant<>(uuid),
            "Primary", new Variant<>(primary),
            "Characteristics", getCharacteristicsAsVariantArray()
            );
    }

    @Override
    public Class<? extends DBusInterface> getBluezInterface() {
        return GattService1.class;
    }

    private Variant<List<DBusPath>> getCharacteristicsAsVariantArray() {
        List<DBusPath> pathes = new ArrayList<>();
        getCharacteristics().stream().map(d -> new DBusPath(d.getObjectPath())).forEach(pathes::add);

        return new Variant<>(pathes, "ao");
    }

}

public static class SampleCharacteristics implements GattCharacteristic1, PropertiesHelper {

    private final String uuid = UUID.randomUUID().toString();

    private final SampleService service;
    private final List<String> flags = new ArrayList<>();
    private final List<SampleGattDescriptor> descriptors = new ArrayList<>();

    public SampleCharacteristics(SampleService _service) {
        service = _service;
        flags.add(FLAG_READ);
    }

    public String getUuid() {
        return uuid;
    }

    public SampleService getService() {
        return service;
    }

    public List<SampleGattDescriptor> getDescriptors() {
        return descriptors;
    }

    @Override
    public Class<? extends DBusInterface> getBluezInterface() {
        return GattCharacteristic1.class;
    }

    @Override
    public String getObjectPath() {
        return service.getObjectPath() + "/char0";
    }

    @Override
    public byte[] ReadValue(Map<String, Variant<?>> _options)
        throws BluezFailedException, BluezInProgressException, BluezNotPermittedException, BluezNotAuthorizedException, BluezInvalidOffsetException, BluezNotSupportedException {

        throw new UnsupportedOperationException("Implement if needed");
    }

    @Override
    public void WriteValue(byte[] _value, Map<String, Variant<?>> _options)
        throws BluezFailedException, BluezInProgressException, BluezNotPermittedException, BluezInvalidValueLengthException, BluezNotAuthorizedException, BluezNotSupportedException {
        throw new UnsupportedOperationException("Implement if needed");

    }

    @Override
    public TwoTuple<FileDescriptor, UInt16> AcquireWrite(Map<String, Variant<?>> _options) throws BluezFailedException, BluezNotSupportedException {
        throw new UnsupportedOperationException("Implement if needed");
    }

    @Override
    public TwoTuple<FileDescriptor, UInt16> AcquireNotify(Map<String, Variant<?>> _options) throws BluezFailedException, BluezNotSupportedException {
        throw new UnsupportedOperationException("Implement if needed");
    }

    @Override
    public void StartNotify() throws BluezFailedException, BluezNotPermittedException, BluezInProgressException, BluezNotConnectedException, BluezNotSupportedException {
        throw new UnsupportedOperationException("Implement if needed");

    }

    @Override
    public void StopNotify() throws BluezFailedException {
        throw new UnsupportedOperationException("Implement if needed");

    }

    @Override
    public void Confirm() throws BluezFailedException {
        throw new UnsupportedOperationException("Implement if needed");
    }

    @SuppressWarnings("unchecked")
    @Override
    public Variant<?> Get(String _interfaceName, String _propertyName) {
        if (!getBluezInterface().getName().equals(_interfaceName)) {
            throw new IllegalArgumentException();
        }

        if ("UUID".equals(_propertyName)) {
            return  new Variant<>(uuid);
        } else if ("Service".equals(_propertyName)) {
            return new Variant<>(new DBusPath(service.getObjectPath()));
        } else if ("Descriptors".equals(_propertyName)) {
            return getDescriptorsAsVariantArray();
        } else if ("Flags".equals(_propertyName)) {
            return  new Variant<>(flags, "as");
        }
        return null;
    }

    @Override
    public <A> void Set(String _interfaceName, String _propertyName, A _value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Map<String, Variant<?>> GetAll(String _interfaceName) {
        return Map.of("UUID", new Variant<>(uuid),
            "Service", new Variant<>(new DBusPath(service.getObjectPath())),
            "Descriptors", getDescriptorsAsVariantArray(),
            "Flags", new Variant<>(flags, "as"));
    }

    private Variant<List<DBusPath>> getDescriptorsAsVariantArray() {
        List<DBusPath> pathes = new ArrayList<>();
        getDescriptors().stream().map(d -> new DBusPath(d.getObjectPath())).forEach(pathes::add);

        return new Variant<>(pathes, "ao");
    }

}

public static class SampleGattDescriptor implements GattDescriptor1, PropertiesHelper {

    private final String uuid = UUID.randomUUID().toString();
    private final SampleCharacteristics characteristic;
    private final List<String> flags = new ArrayList<>();

    public SampleGattDescriptor(SampleCharacteristics _characteristics) {
        characteristic = _characteristics;
        flags.add(FLAG_READ);
    }

    public String getUuid() {
        return uuid;
    }

    public SampleCharacteristics getCharacteristic() {
        return characteristic;
    }

    @Override
    public Class<? extends DBusInterface> getBluezInterface() {
        return GattDescriptor1.class;
    }

    @Override
    public String getObjectPath() {
        return characteristic.getObjectPath() + "/desc0";
    }

    @Override
    public byte[] ReadValue(Map<String, Variant<?>> _flags)
        throws BluezFailedException, BluezInProgressException, BluezNotPermittedException, BluezNotAuthorizedException, BluezNotSupportedException {
        throw new UnsupportedOperationException("Implement if needed");
    }

    @Override
    public void WriteValue(byte[] _value, Map<String, Variant<?>> _flags)
        throws BluezFailedException, BluezInProgressException, BluezNotPermittedException, BluezInvalidValueLengthException, BluezNotAuthorizedException, BluezNotSupportedException {
        throw new UnsupportedOperationException("Implement if needed");
    }

    @SuppressWarnings("unchecked")
    @Override
    public Variant<?> Get(String _interfaceName, String _propertyName) {
        if ("UUID".equals(_propertyName)) {
            return new Variant<>(uuid);
        } else if ("Characteristic  ".equals(_propertyName)) {
            return new Variant<>(characteristic);
        } else if ("Flags".equals(_propertyName)) {
            return new Variant<>(flags, "as");
        }
        return null;
    }

    @Override
    public <A> void Set(String _interfaceName, String _propertyName, A _value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Map<String, Variant<?>> GetAll(String _interfaceName) {
        return Map.of("UUID", new Variant<>(uuid),
            "Characteristic", new Variant<>(new DBusPath(characteristic.getObjectPath())),
            "Flags", new Variant<>(flags, "as"));
    }

}

public interface GattServiceApp extends DBusInterface {

    Map<DBusPath, Map<String, Map<String, Variant<?>>>> GetManagedObjects();
}

public interface PropertiesHelper extends Properties {

    Class<? extends DBusInterface> getBluezInterface();

    default Map<String, Map<String, Variant<?>>> getProps() {
        Map<String, Map<String, Variant<?>>> result = new LinkedHashMap<>();
        result.put(getBluezInterface().getName(), GetAll(null));
        return result;
    }

}

} — Reply to this email directly, view it on GitHub https://github.com/hypfvieh/bluez-dbus/issues/63#issuecomment-2292015260, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN4OIOD63IMU2IEHXV3ZQDZRT4CJAVCNFSM6AAAAABMR5KBISVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDEOJSGAYTKMRWGA. You are receiving this because you authored the thread.