Skip to content

Commit 3a76703

Browse files
authored
Bluez DBus Emulator v0.1.0
Initial Release
2 parents 8c75661 + c8882c4 commit 3a76703

File tree

12 files changed

+465
-1
lines changed

12 files changed

+465
-1
lines changed

.github/workflows/deploy.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Run Deploy
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: New Version
8+
default: ""
9+
required: true
10+
publish:
11+
description: Make Public(true or false)
12+
default: "false"
13+
required: true
14+
15+
jobs:
16+
RunDeploy:
17+
runs-on: windows-2019
18+
19+
steps:
20+
# check users permission
21+
- name: "Check Permissions"
22+
uses: "lannonbr/[email protected]"
23+
with:
24+
permission: "admin"
25+
env:
26+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27+
# Install Dependencies
28+
- name: Clone Repository
29+
uses: actions/checkout@v2
30+
- name: Install Python Deploy Tools
31+
run: |
32+
python -m pip install wheel
33+
python -m pip install twine
34+
shell: cmd
35+
# Prepare package
36+
- name: Prepare Python Package
37+
run: |
38+
cd $env:GITHUB_WORKSPACE
39+
(gc .\setup.py).replace('0.0.1', $env:VERSION) | Out-File -encoding ASCII setup.py
40+
type setup.py
41+
python setup.py sdist bdist_wheel
42+
env:
43+
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
44+
VERSION: ${{ github.event.inputs.version }}
45+
# Publish package
46+
- name: Publish Packages
47+
if: github.event.inputs.publish == 'true'
48+
run: |
49+
cd $env:GITHUB_WORKSPACE
50+
twine upload --skip-existing dist/*.whl --user $env:PYPI_USER --password $env:PYPI_PASSWORD
51+
env:
52+
PYPI_USER: $ {{ secrets.PYPI_USER }}
53+
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
54+
# To be able to test/share whl wo publishing
55+
- name: Upload Artifacts
56+
uses: actions/upload-artifact@v1
57+
with:
58+
name: dist
59+
path: dist

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Changelog
2+
All notable changes to this project will be documented in this file.
3+
4+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6+
7+
## [0.1.0] - 2021-07-24
8+
- First implementation with a basic example emulating a device exposing the Nordic UART Service.

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,28 @@
1-
# python_bluez_dbus_emulator
1+
# bluez_dbus_emulator
2+
3+
A simple set of libraries to allow emulating the behavior of a BlueZ
4+
Bluetooth device over DBus.
5+
6+
## Prerequisites
7+
8+
Before you begin, ensure you have met the following requirements:
9+
- [dbus_next](https://github.com/altdesktop/python-dbus-next)
10+
11+
## Installation
12+
13+
```
14+
pip3 install bluez_dbus_emulator
15+
```
16+
17+
## Usage
18+
19+
For usage instructions, just follow the examples provided in the `examples` folder.
20+
21+
## Contributors
22+
23+
Thanks to the following people who have contributed to this project:
24+
* [@Andrey1994](https://github.com/Andrey1994)
25+
26+
## License
27+
28+
This project is licensed under the terms of the [MIT Licence](LICENCE.md).

bluez_dbus_emulator/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from bluez_dbus_emulator.emulator import Emulator
2+
from bluez_dbus_emulator.adapter1 import Adapter1
3+
from bluez_dbus_emulator.device1 import Device1
4+
from bluez_dbus_emulator.gattservice1 import GattService1
5+
from bluez_dbus_emulator.gattcharacteristic1 import GattCharacteristic1

bluez_dbus_emulator/adapter1.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from dbus_next.service import ServiceInterface, method, dbus_property, PropertyAccess
2+
3+
import asyncio
4+
import random
5+
6+
7+
class Adapter1(ServiceInterface):
8+
def __init__(self, bus, path):
9+
self.bus = bus
10+
self.path = path
11+
super().__init__("org.bluez.Adapter1")
12+
self._discovering = False
13+
self._address = "00:00:00:00:00:00"
14+
15+
self._devices = []
16+
17+
def export(self):
18+
self.bus.export(f"/org/bluez/{self.path}", self)
19+
20+
def add_device(self, device):
21+
self._devices.append(device)
22+
23+
@method()
24+
def SetDiscoveryFilter(self, properties: "a{sv}"):
25+
return
26+
27+
@method()
28+
async def StartDiscovery(self):
29+
print("StartDiscovery")
30+
await self._update_discoverying(True)
31+
for device in self._devices:
32+
await device.task_scanning_start()
33+
return
34+
35+
@method()
36+
async def StopDiscovery(self):
37+
print("StopDiscovery")
38+
await self._update_discoverying(False)
39+
for device in self._devices:
40+
device.task_scanning_stop()
41+
return
42+
43+
@dbus_property(access=PropertyAccess.READ)
44+
def Discovering(self) -> "b":
45+
return self._discovering
46+
47+
async def _update_discoverying(self, new_value: bool):
48+
await asyncio.sleep(random.uniform(0.5, 1.5))
49+
self._discovering = new_value
50+
self.emit_properties_changed({"Discovering": self._discovering})
51+
print(f"Property changed: {self._discovering}")

bluez_dbus_emulator/device1.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
from dbus_next.service import ServiceInterface, method, dbus_property, PropertyAccess
2+
3+
import asyncio
4+
import random
5+
6+
7+
class Device1(ServiceInterface):
8+
def __init__(self, bus, parent_path, mac_address="00:00:00:00:00:00"):
9+
self.bus = bus
10+
self.path = f"{parent_path}/dev_{'_'.join(mac_address.split(':'))}"
11+
super().__init__("org.bluez.Device1")
12+
self._exported = False
13+
self._connected = False
14+
self._services_resolved = False
15+
self._rssi = -128
16+
self._address = mac_address
17+
self._name = f"dev_{'_'.join(mac_address.split(':'))}"
18+
self._services = []
19+
20+
self.__task_scanning_active = False
21+
self.__task_connected_active = False
22+
23+
async def export(self):
24+
if not self._exported:
25+
await asyncio.sleep(random.uniform(0.5, 1.5))
26+
self.bus.export(f"/org/bluez/{self.path}", self)
27+
self._exported = True
28+
29+
def add_service(self, service):
30+
self._services.append(service)
31+
32+
def run_connected_task(self):
33+
pass
34+
35+
async def task_scanning_start(self):
36+
await self.export()
37+
self.__task_scanning_active = True
38+
asyncio.ensure_future(self._task_scanning_run())
39+
40+
def task_scanning_stop(self):
41+
self.__task_scanning_active = False
42+
43+
def task_connected_start(self):
44+
self.__task_connected_active = True
45+
asyncio.ensure_future(self._task_connected_run())
46+
47+
def task_connected_stop(self):
48+
self.__task_connected_active = False
49+
50+
async def _task_scanning_run(self):
51+
await asyncio.sleep(random.uniform(0.02, 0.2))
52+
# Execute scanning tasks
53+
await self._update_rssi(random.uniform(-90, -60))
54+
if self.__task_scanning_active:
55+
asyncio.ensure_future(self._task_scanning_run())
56+
57+
async def _task_connected_run(self):
58+
await asyncio.sleep(0.015)
59+
# Execute connected tasks
60+
self.run_connected_task()
61+
if self.__task_connected_active:
62+
asyncio.ensure_future(self._task_connected_run())
63+
64+
@method()
65+
async def Connect(self):
66+
print("Connect")
67+
await self._update_connected(True)
68+
for service in self._services:
69+
service.export()
70+
await self._update_services_resolved(True)
71+
self.task_connected_start()
72+
return
73+
74+
@method()
75+
async def Disconnect(self):
76+
print("Disconnect")
77+
await self._update_services_resolved(False)
78+
self.task_connected_stop()
79+
await self._update_connected(False)
80+
return
81+
82+
@dbus_property(access=PropertyAccess.READ)
83+
def Connected(self) -> "b":
84+
return self._connected
85+
86+
@dbus_property(access=PropertyAccess.READ)
87+
def ServicesResolved(self) -> "b":
88+
return self._services_resolved
89+
90+
@dbus_property(access=PropertyAccess.READ)
91+
def RSSI(self) -> "n":
92+
return self._rssi
93+
94+
@dbus_property(access=PropertyAccess.READ)
95+
def Name(self) -> "s":
96+
return self._name
97+
98+
async def _update_connected(self, new_value: bool):
99+
await asyncio.sleep(random.uniform(0.5, 1.5))
100+
self._connected = new_value
101+
property_changed = {"Connected": self._connected}
102+
self.emit_properties_changed(property_changed)
103+
print(f"Property changed: {property_changed}")
104+
105+
async def _update_services_resolved(self, new_value: bool):
106+
await asyncio.sleep(random.uniform(0.0, 0.5))
107+
self._services_resolved = new_value
108+
property_changed = {"ServicesResolved": self._services_resolved}
109+
self.emit_properties_changed(property_changed)
110+
print(f"Property changed: {property_changed}")
111+
112+
async def _update_rssi(self, new_value: int):
113+
self._rssi = int(new_value)
114+
property_changed = {"RSSI": self._rssi}
115+
self.emit_properties_changed(property_changed)

bluez_dbus_emulator/emulator.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from dbus_next.service import ServiceInterface, method
2+
3+
4+
class Emulator(ServiceInterface):
5+
"""
6+
Entry point for the bluez dbus emulator. Provides programmatic
7+
control of the async application.
8+
"""
9+
10+
def __init__(self, bus):
11+
self.bus = bus
12+
super().__init__("emulator.bluez_dbus")
13+
14+
def export(self):
15+
self.bus.export("/", self)
16+
17+
@method()
18+
def Exit(self):
19+
"""
20+
Finishes the emulation session by disconnecting from dbus.
21+
"""
22+
self.bus.disconnect()
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from dbus_next.service import ServiceInterface, method, dbus_property, PropertyAccess
2+
3+
import asyncio
4+
import random
5+
6+
7+
class GattCharacteristic1(ServiceInterface):
8+
def __init__(self, bus, parent_path, id_num, uuid):
9+
self.bus = bus
10+
self.path = f"{parent_path}/char{id_num:04x}"
11+
super().__init__("org.bluez.GattCharacteristic1")
12+
self._uuid = uuid
13+
self._value = bytes()
14+
self._notifying = False
15+
self._exported = False
16+
17+
def export(self):
18+
if not self._exported:
19+
self.bus.export(f"/org/bluez/{self.path}", self)
20+
self._exported = True
21+
22+
def update_value(self, new_value: bytes):
23+
self._update_value(new_value)
24+
25+
@method()
26+
async def StartNotify(self):
27+
await self._update_notifying(True)
28+
return
29+
30+
@method()
31+
async def StopNotify(self):
32+
await self._update_notifying(False)
33+
return
34+
35+
@method()
36+
def ReadValue(self, options: "a{sv}") -> "ay":
37+
return self._value
38+
39+
@method()
40+
def WriteValue(self, value: "ay", options: "a{sv}"):
41+
self._update_value(value)
42+
43+
@dbus_property(access=PropertyAccess.READ)
44+
def Notifying(self) -> "b":
45+
return self._notifying
46+
47+
@dbus_property(access=PropertyAccess.READ)
48+
def UUID(self) -> "s":
49+
return self._uuid
50+
51+
@dbus_property(access=PropertyAccess.READ)
52+
def Value(self) -> "ay":
53+
return self._value
54+
55+
def _update_value(self, new_value: bytes):
56+
self._value = new_value
57+
if self._notifying:
58+
property_changed = {"Value": self._value}
59+
self.emit_properties_changed(property_changed)
60+
61+
async def _update_notifying(self, new_value: bool):
62+
await asyncio.sleep(random.uniform(0.0, 0.2))
63+
self._notifying = new_value
64+
property_changed = {"Notifying": self._notifying}
65+
self.emit_properties_changed(property_changed)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from dbus_next.service import ServiceInterface, dbus_property, PropertyAccess
2+
3+
4+
class GattService1(ServiceInterface):
5+
def __init__(self, bus, parent_path, id_num, uuid):
6+
self.bus = bus
7+
self.path = f"{parent_path}/service{id_num:04x}"
8+
super().__init__("org.bluez.GattService1")
9+
self._uuid = uuid
10+
self._exported = False
11+
self._characteristics = []
12+
13+
def export(self):
14+
if not self._exported:
15+
self.bus.export(f"/org/bluez/{self.path}", self)
16+
for char in self._characteristics:
17+
char.export()
18+
self._exported = True
19+
20+
def add_characteristic(self, characteristic):
21+
self._characteristics.append(characteristic)
22+
23+
@dbus_property(access=PropertyAccess.READ)
24+
def UUID(self) -> "s":
25+
return self._uuid

0 commit comments

Comments
 (0)