168 lines
7.4 KiB
Python
168 lines
7.4 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
# Emulate the "BaroBalDav" sensor on this PC's Bluetooth so the phone's baldav can be tested
|
||
|
|
# against the REAL Kotlin BLE read path before the ESP32 arrives. Advertises the standard
|
||
|
|
# Environmental Sensing service (0x181A) with Pressure (0x2A6D, uint32 0.1 Pa) and Temperature
|
||
|
|
# (0x2A6E, int16 0.01 C), and pushes a simulated pressure that rises and falls so altitude moves.
|
||
|
|
#
|
||
|
|
# Pure BlueZ D-Bus GATT server (python3-dbus + python3-gi) - no bluetoothctl, no pip installs.
|
||
|
|
# Run: python3 tools/baro-sensor/emulate_baro.py (Ctrl+C to stop)
|
||
|
|
import struct, math, time, sys
|
||
|
|
import dbus, dbus.mainloop.glib, dbus.service
|
||
|
|
from gi.repository import GLib
|
||
|
|
|
||
|
|
BLUEZ = "org.bluez"
|
||
|
|
ADAPTER_IFACE = "org.bluez.Adapter1"
|
||
|
|
GATT_MANAGER = "org.bluez.GattManager1"
|
||
|
|
LE_ADV_MANAGER = "org.bluez.LEAdvertisingManager1"
|
||
|
|
DBUS_OM = "org.freedesktop.DBus.ObjectManager"
|
||
|
|
DBUS_PROP = "org.freedesktop.DBus.Properties"
|
||
|
|
|
||
|
|
ESS_UUID = "0000181a-0000-1000-8000-00805f9b34fb"
|
||
|
|
PRESSURE_UUID = "00002a6d-0000-1000-8000-00805f9b34fb"
|
||
|
|
TEMP_UUID = "00002a6e-0000-1000-8000-00805f9b34fb"
|
||
|
|
|
||
|
|
def find_adapter(bus):
|
||
|
|
om = dbus.Interface(bus.get_object(BLUEZ, "/"), DBUS_OM)
|
||
|
|
for path, ifaces in om.GetManagedObjects().items():
|
||
|
|
if ADAPTER_IFACE in ifaces and LE_ADV_MANAGER in ifaces:
|
||
|
|
return path
|
||
|
|
return None
|
||
|
|
|
||
|
|
# ---- GATT application / service / characteristic boilerplate (BlueZ example pattern) ----
|
||
|
|
class Application(dbus.service.Object):
|
||
|
|
def __init__(self, bus):
|
||
|
|
self.path = "/dev/baldav/baro"
|
||
|
|
self.services = []
|
||
|
|
dbus.service.Object.__init__(self, bus, self.path)
|
||
|
|
def get_path(self): return dbus.ObjectPath(self.path)
|
||
|
|
def add_service(self, s): self.services.append(s)
|
||
|
|
@dbus.service.method(DBUS_OM, out_signature="a{oa{sa{sv}}}")
|
||
|
|
def GetManagedObjects(self):
|
||
|
|
out = {}
|
||
|
|
for s in self.services:
|
||
|
|
out[s.get_path()] = s.get_properties()
|
||
|
|
for c in s.chars:
|
||
|
|
out[c.get_path()] = c.get_properties()
|
||
|
|
return out
|
||
|
|
|
||
|
|
class Service(dbus.service.Object):
|
||
|
|
def __init__(self, bus, index, uuid, primary=True):
|
||
|
|
self.path = "/dev/baldav/baro/service%d" % index
|
||
|
|
self.uuid = uuid; self.primary = primary; self.chars = []
|
||
|
|
dbus.service.Object.__init__(self, bus, self.path)
|
||
|
|
def get_path(self): return dbus.ObjectPath(self.path)
|
||
|
|
def add_char(self, c): self.chars.append(c)
|
||
|
|
def get_properties(self):
|
||
|
|
return {"org.bluez.GattService1": {
|
||
|
|
"UUID": self.uuid, "Primary": self.primary,
|
||
|
|
"Characteristics": dbus.Array([c.get_path() for c in self.chars], signature="o")}}
|
||
|
|
|
||
|
|
class Characteristic(dbus.service.Object):
|
||
|
|
def __init__(self, bus, index, uuid, flags, service):
|
||
|
|
self.path = service.path + "/char%d" % index
|
||
|
|
self.uuid = uuid; self.flags = flags; self.service = service
|
||
|
|
self.notifying = False; self.value = []
|
||
|
|
dbus.service.Object.__init__(self, bus, self.path)
|
||
|
|
def get_path(self): return dbus.ObjectPath(self.path)
|
||
|
|
def get_properties(self):
|
||
|
|
return {"org.bluez.GattCharacteristic1": {
|
||
|
|
"Service": self.service.get_path(), "UUID": self.uuid, "Flags": self.flags}}
|
||
|
|
@dbus.service.method(DBUS_PROP, in_signature="s", out_signature="a{sv}")
|
||
|
|
def GetAll(self, iface):
|
||
|
|
if iface != "org.bluez.GattCharacteristic1":
|
||
|
|
raise dbus.exceptions.DBusException("org.bluez.Error.InvalidArguments")
|
||
|
|
return self.get_properties()["org.bluez.GattCharacteristic1"]
|
||
|
|
@dbus.service.method("org.bluez.GattCharacteristic1", in_signature="a{sv}", out_signature="ay")
|
||
|
|
def ReadValue(self, options): return dbus.Array(self.value, signature="y")
|
||
|
|
@dbus.service.method("org.bluez.GattCharacteristic1")
|
||
|
|
def StartNotify(self):
|
||
|
|
self.notifying = True
|
||
|
|
print(" [%s] StartNotify" % self.uuid[4:8])
|
||
|
|
@dbus.service.method("org.bluez.GattCharacteristic1")
|
||
|
|
def StopNotify(self):
|
||
|
|
self.notifying = False
|
||
|
|
print(" [%s] StopNotify" % self.uuid[4:8])
|
||
|
|
@dbus.service.signal(DBUS_PROP, signature="sa{sv}as")
|
||
|
|
def PropertiesChanged(self, iface, changed, invalidated): pass
|
||
|
|
def set_value(self, raw):
|
||
|
|
self.value = [dbus.Byte(b) for b in raw]
|
||
|
|
if self.notifying:
|
||
|
|
self.PropertiesChanged("org.bluez.GattCharacteristic1",
|
||
|
|
{"Value": dbus.Array(self.value, signature="y")}, [])
|
||
|
|
|
||
|
|
# ---- LE advertisement ----
|
||
|
|
class Advertisement(dbus.service.Object):
|
||
|
|
def __init__(self, bus, index):
|
||
|
|
self.path = "/dev/baldav/baro/adv%d" % index
|
||
|
|
dbus.service.Object.__init__(self, bus, self.path)
|
||
|
|
def get_path(self): return dbus.ObjectPath(self.path)
|
||
|
|
@dbus.service.method(DBUS_PROP, in_signature="s", out_signature="a{sv}")
|
||
|
|
def GetAll(self, iface):
|
||
|
|
if iface != "org.bluez.LEAdvertisement1":
|
||
|
|
raise dbus.exceptions.DBusException("org.bluez.Error.InvalidArguments")
|
||
|
|
return {
|
||
|
|
"Type": "peripheral",
|
||
|
|
"ServiceUUIDs": dbus.Array([ESS_UUID], signature="s"),
|
||
|
|
"LocalName": dbus.String("BaroBalDav"),
|
||
|
|
"Includes": dbus.Array(["tx-power"], signature="s"),
|
||
|
|
}
|
||
|
|
@dbus.service.method("org.bluez.LEAdvertisement1")
|
||
|
|
def Release(self): print("Advertisement released")
|
||
|
|
|
||
|
|
# ---- pressure/temperature simulation ----
|
||
|
|
def enc_pressure(pa): return struct.pack("<I", round(pa * 10)) # uint32, 0.1 Pa
|
||
|
|
def enc_temp(tc): return struct.pack("<h", round(tc * 100)) # int16, 0.01 C
|
||
|
|
|
||
|
|
def main():
|
||
|
|
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
|
||
|
|
bus = dbus.SystemBus()
|
||
|
|
adapter = find_adapter(bus)
|
||
|
|
if not adapter:
|
||
|
|
print("No BLE adapter with GATT+Advertising found"); sys.exit(1)
|
||
|
|
print("adapter:", adapter)
|
||
|
|
|
||
|
|
app = Application(bus)
|
||
|
|
svc = Service(bus, 0, ESS_UUID, True)
|
||
|
|
press = Characteristic(bus, 0, PRESSURE_UUID, ["read", "notify"], svc)
|
||
|
|
temp = Characteristic(bus, 1, TEMP_UUID, ["read", "notify"], svc)
|
||
|
|
svc.add_char(press); svc.add_char(temp); app.add_service(svc)
|
||
|
|
|
||
|
|
gatt = dbus.Interface(bus.get_object(BLUEZ, adapter), GATT_MANAGER)
|
||
|
|
adv_mgr = dbus.Interface(bus.get_object(BLUEZ, adapter), LE_ADV_MANAGER)
|
||
|
|
adv = Advertisement(bus, 0)
|
||
|
|
|
||
|
|
t0 = time.time()
|
||
|
|
def tick():
|
||
|
|
t = time.time() - t0
|
||
|
|
# sea-level-ish pressure that swings ~600 Pa (~ +-50 m) with a ~60 s period, so altitude moves
|
||
|
|
pa = 101325.0 - 600.0 * math.sin(t / 10.0)
|
||
|
|
tc = 24.0 + 0.5 * math.sin(t / 25.0)
|
||
|
|
press.set_value(enc_pressure(pa))
|
||
|
|
temp.set_value(enc_temp(tc))
|
||
|
|
if int(t) % 5 == 0:
|
||
|
|
print(" sim: P=%.1f Pa (%.2f hPa) T=%.2f C" % (pa, pa/100.0, tc))
|
||
|
|
return True
|
||
|
|
tick()
|
||
|
|
|
||
|
|
def reg_ok(): print("GATT application registered")
|
||
|
|
def reg_err(e): print("GATT register error:", e); mainloop.quit()
|
||
|
|
def adv_ok(): print("Advertising as 'BaroBalDav' (service 0x181A)")
|
||
|
|
def adv_err(e): print("Advertising error:", e); mainloop.quit()
|
||
|
|
|
||
|
|
gatt.RegisterApplication(app.get_path(), {}, reply_handler=reg_ok, error_handler=reg_err)
|
||
|
|
adv_mgr.RegisterAdvertisement(adv.get_path(), {}, reply_handler=adv_ok, error_handler=adv_err)
|
||
|
|
|
||
|
|
GLib.timeout_add(1000, tick)
|
||
|
|
mainloop = GLib.MainLoop()
|
||
|
|
print("BaroBalDav emulator running - connect from the phone. Ctrl+C to stop.")
|
||
|
|
try:
|
||
|
|
mainloop.run()
|
||
|
|
except KeyboardInterrupt:
|
||
|
|
print("\nstopping")
|
||
|
|
try: adv_mgr.UnregisterAdvertisement(adv.get_path())
|
||
|
|
except Exception: pass
|
||
|
|
try: gatt.UnregisterApplication(app.get_path())
|
||
|
|
except Exception: pass
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|