diff --git a/src/mcbluetooth/gatt_server.py b/src/mcbluetooth/gatt_server.py index dab4851..263abc2 100644 --- a/src/mcbluetooth/gatt_server.py +++ b/src/mcbluetooth/gatt_server.py @@ -24,7 +24,7 @@ from typing import Any from dbus_fast import BusType, Message, MessageType, Variant from dbus_fast.aio import MessageBus -from dbus_fast.service import ServiceInterface, dbus_property, method +from dbus_fast.service import PropertyAccess, ServiceInterface, dbus_property, method log = logging.getLogger(__name__) @@ -130,11 +130,22 @@ class GattApplication(ServiceInterface): class GattServiceIface(ServiceInterface): """org.bluez.GattService1 — exported at each service path. - No D-Bus methods; properties served via GetManagedObjects only. + Properties must be exposed via @dbus_property so BlueZ can validate + the service object through the standard Properties interface. """ - def __init__(self): + def __init__(self, uuid: str, primary: bool): super().__init__("org.bluez.GattService1") + self._uuid = uuid + self._primary = primary + + @dbus_property(PropertyAccess.READ) + def UUID(self) -> "s": # noqa: F821 + return self._uuid + + @dbus_property(PropertyAccess.READ) + def Primary(self) -> "b": # noqa: F821 + return self._primary class GattCharacteristicIface(ServiceInterface): @@ -142,6 +153,7 @@ class GattCharacteristicIface(ServiceInterface): ReadValue returns the stored value. WriteValue stores the value AND records a WriteEvent for LLM polling (like agent.py's pending_requests). + Properties exposed via @dbus_property for BlueZ introspection. """ def __init__(self, char: ServerCharacteristic, manager: "GattServerManager"): @@ -149,6 +161,18 @@ class GattCharacteristicIface(ServiceInterface): self._char = char self._manager = manager + @dbus_property(PropertyAccess.READ) + def UUID(self) -> "s": # noqa: F821 + return self._char.uuid + + @dbus_property(PropertyAccess.READ) + def Service(self) -> "o": # noqa: F821 + return f"{APP_BASE_PATH}/{self._char.service_id}" + + @dbus_property(PropertyAccess.READ) + def Flags(self) -> "as": # noqa: F821 + return self._char.flags + @method() def ReadValue(self, options: "a{sv}") -> "ay": # noqa: F821 offset = 0 @@ -189,12 +213,27 @@ class GattCharacteristicIface(ServiceInterface): class GattDescriptorIface(ServiceInterface): - """org.bluez.GattDescriptor1 — handles reads/writes on descriptors.""" + """org.bluez.GattDescriptor1 — handles reads/writes on descriptors. + + Properties exposed via @dbus_property for BlueZ introspection. + """ def __init__(self, desc: ServerDescriptor): super().__init__("org.bluez.GattDescriptor1") self._desc = desc + @dbus_property(PropertyAccess.READ) + def UUID(self) -> "s": # noqa: F821 + return self._desc.uuid + + @dbus_property(PropertyAccess.READ) + def Characteristic(self) -> "o": # noqa: F821 + return f"{APP_BASE_PATH}/{self._desc.char_id}" + + @dbus_property(PropertyAccess.READ) + def Flags(self) -> "as": # noqa: F821 + return self._desc.flags + @method() def ReadValue(self, options: "a{sv}") -> "ay": # noqa: F821 offset = 0 @@ -226,15 +265,15 @@ class LEAdvertisementIface(ServiceInterface): def Release(self) -> None: log.info("GATT server: advertisement released by BlueZ") - @dbus_property() + @dbus_property(PropertyAccess.READ) def Type(self) -> "s": # noqa: F821 return self._type - @dbus_property() + @dbus_property(PropertyAccess.READ) def LocalName(self) -> "s": # noqa: F821 return self._local_name - @dbus_property() + @dbus_property(PropertyAccess.READ) def ServiceUUIDs(self) -> "as": # noqa: F821 return self._service_uuids @@ -277,7 +316,7 @@ class GattServerManager: uuid=uuid, primary=primary, ) - svc.dbus_obj = GattServiceIface() + svc.dbus_obj = GattServiceIface(uuid, primary) self._services[service_id] = svc log.info("GATT server: added service %s (UUID=%s)", service_id, uuid) diff --git a/src/mcbluetooth/tools/bt_elm327_emu.py b/src/mcbluetooth/tools/bt_elm327_emu.py index 07baf5c..d435832 100644 --- a/src/mcbluetooth/tools/bt_elm327_emu.py +++ b/src/mcbluetooth/tools/bt_elm327_emu.py @@ -231,7 +231,10 @@ class ELM327Emulator: cr = "\r\n" if self.linefeed else "\r" cmd = cmd.replace(" ", "") - if len(cmd) < 4: + # Modes 03/04 don't require a PID — pad to 4 chars + if len(cmd) == 2: + cmd += "00" + elif len(cmd) < 4: return f"?{cr}{cr}>" try: