Fix BlueZ GATT server registration and OBD-II mode 03/04 parsing

Add @dbus_property(PropertyAccess.READ) to GattServiceIface,
GattCharacteristicIface, and GattDescriptorIface so BlueZ can
validate objects via the standard Properties interface — fixes
"No valid service object found" error on RegisterApplication.

Also fix ELM327 emulator rejecting 2-char OBD commands (mode 03
for DTCs and mode 04 for clear) by padding to 4 chars.
This commit is contained in:
Ryan Malloy 2026-02-12 13:01:28 -07:00
parent d0f0565e62
commit 5dc5f640c7
2 changed files with 51 additions and 9 deletions

View File

@ -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)

View File

@ -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: