Repository: brainflow-dev/brainflow (language: python)

Issue:
Core dumped connecting to Muse devices on linux
**Describe the bug**
*A clear and concise description of what the bug is.*

Get `Aborted (core dumped)` crash when connecting to muse on ubuntu. 

```
(brainflow_compiled) kernel@stoic-hofstadter:~/Code/eegnb_dev/brainflow/python-package$ python ../tests/python/brainflow_get_data.py --board-id 39
[2022-01-04 16:24:51.765] [board_logger] [info] incoming json: {
    "file": "",
    "ip_address": "",
    "ip_port": 0,
    "ip_protocol": 0,
    "mac_address": "",
    "other_info": "",
    "serial_number": "",
    "serial_port": "",
    "timeout": 0
}
[2022-01-04 16:24:51.765] [board_logger] [trace] Board object created 39
[2022-01-04 16:24:51.765] [board_logger] [info] Use timeout for discovery: 5
[2022-01-04 16:24:51.765] [board_logger] [debug] use dyn lib: /home/kernel/miniconda3/envs/brainflow_compiled/lib/python3.7/site-packages/brainflow/lib/libsimpleble-c.so
[2022-01-04 16:24:52.137] [board_logger] [trace] address 0D:AD:93:A9:DB:50
[2022-01-04 16:24:52.137] [board_logger] [trace] identifier 
[2022-01-04 16:24:52.155] [board_logger] [trace] address 00:55:DA:B9:28:55
[2022-01-04 16:24:52.155] [board_logger] [trace] identifier MuseS-2855
[2022-01-04 16:24:52.155] [board_logger] [info] Found Muse device
[2022-01-04 16:24:52.814] [board_logger] [info] Connected to Muse Device
[2022-01-04 16:24:53.816] [board_logger] [trace] found servce 00001801-0000-1000-8000-00805f9b34fb
[2022-01-04 16:24:53.816] [board_logger] [trace] found characteristic 00002a05-0000-1000-8000-00805f9b34fb
[2022-01-04 16:24:53.816] [board_logger] [trace] found servce 0000fe8d-0000-1000-8000-00805f9b34fb
[2022-01-04 16:24:53.816] [board_logger] [trace] found characteristic 273e0001-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:53.816] [board_logger] [info] found control characteristic
[2022-01-04 16:24:53.884] [board_logger] [trace] found characteristic 273e0008-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:53.884] [board_logger] [trace] found characteristic 273e0009-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:53.973] [board_logger] [trace] found characteristic 273e000a-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:54.063] [board_logger] [trace] found characteristic 273e000b-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:54.063] [board_logger] [trace] found characteristic 273e0002-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:54.063] [board_logger] [trace] found characteristic 273e0003-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:54.153] [board_logger] [trace] found characteristic 273e0004-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:54.243] [board_logger] [trace] found characteristic 273e0005-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:54.333] [board_logger] [trace] found characteristic 273e0006-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:54.468] [board_logger] [trace] found characteristic 273e0007-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:54.468] [board_logger] [trace] found characteristic 273e000c-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:54.468] [board_logger] [trace] found characteristic 273e000d-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:54.468] [board_logger] [trace] found characteristic 273e000e-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:54.468] [board_logger] [trace] found characteristic 273e000f-4c4d-454d-96be-f03bac821358
[2022-01-04 16:24:54.558] [board_logger] [trace] found characteristic 273e0010-4c4d-454d-96be-f03bac821358
terminate called without an active exception
Aborted (core dumped)
```

Get same problem when I use `pip install` brainflow or when I compile from source. 

**To Reproduce**
*To report a bug you need to create a simple example that uses only BrainFlow API or use one of the tests we already have.  In this example, you need to call `enable_dev_board_logger ()` and share the output with us. Also, make sure the latest version is used.*

See command and terminal output above

**Info (please complete the following information):**
 - Board Id: `38,39` 
 - OS running on your PC: `Ubuntu Linux`
 - Programming Language and its version: `Python`
 - Architecture(x86, x64, ARM, etc): `x86_64`
 - In case of Python full output of `pip list` command, for other languages please provide BrainFlow version used

```
(brainflow_pip) kernel@stoic-hofstadter:~/Code/eegnb_dev/brainflow$ pip list
Package    Version
---------- ---------
brainflow  4.8.2
certifi    2021.10.8
nptyping   1.4.4
numpy      1.22.0
pip        21.2.4
setuptools 58.0.4
typish     1.9.3
wheel      0.37.0
```

**Expected behavior**
- A clear and concise description of what you expected to happen
- The above command to connect with out crashing. 


**Screenshots**
If applicable, add screenshots to help explain your problem.

**Additional context**
I noticed that when running the above command, the muse device first connects (stops flashing), and continues to be connected after the core dump. 

I did notice that this comp has in its config description 'virtualization: AMD-V`. Not sure if that is relevant here. 

```
(brainflow_pip) kernel@stoic-hofstadter:~/Code/eegnb_dev/brainflow$ lscpu
Architecture:                    x86_64
CPU op-mode(s):                  32-bit, 64-bit
Byte Order:                      Little Endian
Address sizes:                   48 bits physical, 48 bits virtual
CPU(s):                          12
On-line CPU(s) list:             0-11
Thread(s) per core:              2
Core(s) per socket:              6
Socket(s):                       1
NUMA node(s):                    1
Vendor ID:                       AuthenticAMD
CPU family:                      25
Model:                           33
Model name:                      AMD Ryzen 5 5600X 6-Core Processor
Stepping:                        0
Frequency boost:                 enabled
CPU MHz:                         2200.000
CPU max MHz:                     4650.2920
CPU min MHz:                     2200.0000
BogoMIPS:                        7400.14
Virtualization:                  AMD-V
L1d cache:                       192 KiB
L1i cache:                       192 KiB
L2 cache:                        3 MiB
L3 cache:                        32 MiB
NUMA node0 CPU(s):               0-11
Vulnerability Itlb multihit:     Not affected
Vulnerability L1tf:              Not affected
Vulnerability Mds:               Not affected
Vulnerability Meltdown:          Not affected
Vulnerability Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl and seccomp
Vulnerability Spectre v1:        Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:        Mitigation; Full AMD retpoline, IBPB conditional, IBRS_FW, STIBP always-on, RSB filling
Vulnerability Srbds:             Not affected
Vulnerability Tsx async abort:   Not affected
Flags:                           fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant
                                 _tsc rep_good nopl nonstop_tsc cpuid extd_apicid aperfmperf pni pclmulqdq monitor ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand lahf
                                 _lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw ibs skinit wdt tce topoext perfctr_core perfctr_nb bpext perfctr_llc mwaitx
                                  cpb cat_l3 cdp_l3 hw_pstate ssbd mba ibrs ibpb stibp vmmcall fsgsbase bmi1 avx2 smep bmi2 erms invpcid cqm rdt_a rdseed adx smap clflushopt clwb sha_ni x
                                 saveopt xsavec xgetbv1 xsaves cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local clzero irperf xsaveerptr rdpru wbnoinvd arat npt lbrv svm_lock nrip_save t
                                 sc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold avic v_vmsave_vmload vgif umip pku ospke vaes vpclmulqdq rdpid overflow_recov succor
                                  smca fsrm
```





Discussion / hints:
Welcome to BrainFlow project and thanks for your contribution! We will try to fix your issue ASAP. Make sure that you have read about issue format in the docs.

Fix patch:
diff --git a/src/board_controller/muse/muse.cpp b/src/board_controller/muse/muse.cpp
index 8bacf8a08..03ff76599 100644
--- a/src/board_controller/muse/muse.cpp
+++ b/src/board_controller/muse/muse.cpp
@@ -116,6 +116,8 @@ int Muse::prepare_session ()
         return (int)BrainFlowExitCodes::UNABLE_TO_OPEN_PORT_ERROR;
     }
 
+    safe_logger (spdlog::level::info, "found {} BLE adapter(s)", adapter_count);
+
     muse_adapter = simpleble_adapter_get_handle (0);
     if (muse_adapter == NULL)
     {
diff --git a/third_party/SimpleBLE/.github/workflows/ci_build.yml b/third_party/SimpleBLE/.github/workflows/ci_build.yml
index ca0fcb49e..edb7e2573 100644
--- a/third_party/SimpleBLE/.github/workflows/ci_build.yml
+++ b/third_party/SimpleBLE/.github/workflows/ci_build.yml
@@ -127,3 +127,15 @@ jobs:
         cd $GITHUB_WORKSPACE/build_examples
         cmake -DCMAKE_INSTALL_PREFIX=$GITHUB_WORKSPACE/installed -DCMAKE_BUILD_TYPE=Release ../examples
         make -j2
+    - name:  Compile ASAN for Ubuntu
+      run: |
+        mkdir $GITHUB_WORKSPACE/build_asan
+        cd $GITHUB_WORKSPACE/build_asan
+        cmake -DCMAKE_INSTALL_PREFIX=$GITHUB_WORKSPACE/installed -DCMAKE_BUILD_TYPE=Release -DSIMPLEBLE_SANITIZE=Address ..
+        make -j2
+    - name:  Compile TSAN for Ubuntu
+      run: |
+        mkdir $GITHUB_WORKSPACE/build_tsan
+        cd $GITHUB_WORKSPACE/build_tsan
+        cmake -DCMAKE_INSTALL_PREFIX=$GITHUB_WORKSPACE/installed -DCMAKE_BUILD_TYPE=Release -DSIMPLEBLE_SANITIZE=Thread ..
+        make -j2
\ No newline at end of file
diff --git a/third_party/SimpleBLE/.github/workflows/clang_format.yml b/third_party/SimpleBLE/.github/workflows/clang_format.yml
deleted file mode 100644
index d0b5cf9e7..000000000
--- a/third_party/SimpleBLE/.github/workflows/clang_format.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: ClangFormat
-
-on: [push, pull_request]
-
-jobs:
-  clang-format:
-    runs-on: ubuntu-latest
-
-    steps:
-    - uses: actions/checkout@v2
-    - name: Run clang-format
-      uses: DoozyX/clang-format-lint-action@v0.11
-      with:
-        source: 'src'
-        extensions: 'h,cpp,c'
-        clangFormatVersion: 11
-        inplace: False
diff --git a/third_party/SimpleBLE/.github/workflows/cppcheck.yml b/third_party/SimpleBLE/.github/workflows/cppcheck.yml
deleted file mode 100644
index 987faf32a..000000000
--- a/third_party/SimpleBLE/.github/workflows/cppcheck.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-name: CppCheck
-
-on: [push, pull_request]
-
-jobs:
-  cppcheck:
-    runs-on: ubuntu-latest
-
-    steps:
-    - uses: actions/checkout@v2
-    - name: Install CppCheck
-      run: |
-        sudo -H apt-get update -y
-        sudo -H apt-get install -y cppcheck
-      env:
-        DEBIAN_FRONTEND: noninteractive
-    - name: Run Cppcheck
-      run: cppcheck --suppress=objectIndex:src/windows/Utils.cpp --std=c++17 --error-exitcode=1 --xml --xml-version=2 --force src 2>cppcheck_res.xml
-    - name: Generate Report
-      if: ${{ failure() }}
-      run: cppcheck-htmlreport --title=SimpleBLE --file=cppcheck_res.xml --report-dir=report
-    - name: Upload Report
-      if: ${{ failure() }}
-      uses: actions/upload-artifact@v1
-      with:
-        name: report
-        path: report
diff --git a/third_party/SimpleBLE/.github/workflows/run_unix.yml b/third_party/SimpleBLE/.github/workflows/run_unix.yml
deleted file mode 100644
index 2aa17825e..000000000
--- a/third_party/SimpleBLE/.github/workflows/run_unix.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-name: Run Unix
-
-on: [push, pull_request]
-
-jobs:
-  RunUnix:
-    runs-on: ${{ matrix.os }}
-
-    strategy:
-      fail-fast: false
-      max-parallel: 4
-      matrix:
-        os: [ubuntu-latest, macos-11.0]
-
-    steps:
-    # compile and prepare env
-    - name: Clone Repository
-      uses: actions/checkout@v2
-    - name: Install Dependencies
-      if: (matrix.os == 'ubuntu-latest')
-      run: |
-        sudo -H apt-get update -y
-        sudo -H apt-get install -y libdbus-1-dev
-      env:
-        DEBIAN_FRONTEND: noninteractive
-    - name: Install Ninja
-      if: (matrix.os == 'macos-11.0')
-      uses: seanmiddleditch/gha-setup-ninja@master
-      with:
-        version: 1.10.2
-    - name: Setup Cmake
-      uses: jwlawson/actions-setup-cmake@v1.4
-      with:
-        cmake-version: '3.16.x'
-    - name: Compile on MacOS
-      if: (matrix.os == 'macos-11.0')
-      run: |
-        mkdir $GITHUB_WORKSPACE/build
-        cd $GITHUB_WORKSPACE/build
-        cmake -G Ninja -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" -DCMAKE_OSX_DEPLOYMENT_TARGET=10.13 -DCMAKE_INSTALL_PREFIX=$GITHUB_WORKSPACE/installed -DCMAKE_BUILD_TYPE=Release ..
-        ninja
-    - name: Compile on Ubuntu
-      if: (matrix.os == 'ubuntu-latest')
-      run: |
-        mkdir $GITHUB_WORKSPACE/build
-        cd $GITHUB_WORKSPACE/build
-        cmake -DCMAKE_INSTALL_PREFIX=$GITHUB_WORKSPACE/installed -DCMAKE_BUILD_TYPE=Release ..
-        make
diff --git a/third_party/SimpleBLE/.github/workflows/run_windows.yml b/third_party/SimpleBLE/.github/workflows/run_windows.yml
deleted file mode 100644
index 4e4977176..000000000
--- a/third_party/SimpleBLE/.github/workflows/run_windows.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-name: Run Windows
-
-on: [push, pull_request]
-
-jobs:
-  RunWindows:
-    runs-on: ${{ matrix.os }}
-
-    strategy:
-      fail-fast: false
-      max-parallel: 4
-      matrix:
-        os: [windows-2019]
-
-    steps:
-    - name: Clone Repository
-      uses: actions/checkout@v2
-    - name: Add msbuild to PATH
-      uses: microsoft/setup-msbuild@v1.0.2
-    - name: Compile x32
-      run: |
-        mkdir %GITHUB_WORKSPACE%\build32
-        cd %GITHUB_WORKSPACE%\build32
-        cmake -G "Visual Studio 16 2019" -A Win32 -DCMAKE_SYSTEM_VERSION=10.0.19041.0 ..
-        cmake --build . --config Release -j 2 --parallel 2
-      shell: cmd
-    - name: Compile x64
-      run: |
-        mkdir %GITHUB_WORKSPACE%\build64
-        cd %GITHUB_WORKSPACE%\build64
-        cmake -G "Visual Studio 16 2019" -A x64 -DCMAKE_SYSTEM_VERSION=10.0.19041.0 ..
-        cmake --build . --config Release -j 2 --parallel 2
-      shell: cmd
diff --git a/third_party/SimpleBLE/.gitignore b/third_party/SimpleBLE/.gitignore
index 4a98d034f..86a2ff791 100644
--- a/third_party/SimpleBLE/.gitignore
+++ b/third_party/SimpleBLE/.gitignore
@@ -1,4 +1,2 @@
-.build
-build
 .vscode
-*.binlog
\ No newline at end of file
+*.binlog
diff --git a/third_party/SimpleBLE/CHANGELOG.md b/third_party/SimpleBLE/CHANGELOG.md
index 916e480d3..e9088e67f 100644
--- a/third_party/SimpleBLE/CHANGELOG.md
+++ b/third_party/SimpleBLE/CHANGELOG.md
@@ -4,12 +4,29 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [0.0.2] - 2021-XX-XX
+## [0.1.1] - 2021-XX-XX
+
+### Fixed
+- (Windows) Proper cleanup of callbacks during destruction.
+- (Windows) Async timeout reduced to 10 seconds.
+
+## [0.1.0] - 2021-12-28
+
+### Changed
+- Referenced specific version of SimpleBluez to avoid breaking changes as those libraries evolve.
+- (Linux) When `scan_stop` is called, it is now guaranteed that no more scan results will be received.
+- Updated Linux implementation to use SimpleBluez v0.1.1.
+
+### Fixed
+- (Linux) Scan will never stop sleeping.
+
+## [0.0.2] - 2021-10-09
 
 ### Added
 - Safe implementation of `SimpleBLE::Adapter` and `SimpleBLE::Peripheral` classes.
 - CppCheck and ClangFormat CI checks. _(Thanks Andrey1994!)_
 - C-style API with examples.
+- Access to manufacturer data in the `SimpleBLE::Peripheral` class, for Windows and MacOS.
 
 ### Fixed
 - Compilation errors that came up during development. _(Thanks fidoriel!)_
@@ -17,6 +34,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - `SimpleBLE::Adapter` would fail to stop scanning. _(Thanks PatrykSajdok!)_
 - Switched WinRT initialization to single-threaded.
 
+### Changed
+- SimpleBluez dependency migrated to OpenBluetoothToolbox.
+
 ## [0.0.1] - 2021-09-06
 
 ### Added
diff --git a/third_party/SimpleBLE/README.md b/third_party/SimpleBLE/README.md
index 6fdf9f4f8..fe07796ad 100644
--- a/third_party/SimpleBLE/README.md
+++ b/third_party/SimpleBLE/README.md
@@ -85,6 +85,20 @@ include_directories(${SIMPLEBLE_INCLUDES})
       `SimpleBLE::PeripheralBuilder` are provided for the case of
       allowing access to private methods during the build process.
 
+### Concurrency
+
+When designing your application using SimpleBLE, concurrency is a key aspect
+that needs to be taken into account. This is because internally the library
+relies on a thread pool to handle asynchronous operations and poll the OS's
+Bluetooth stack, which can suffer from contention and potentially cause the
+program to crash or freeze if these threads are significantly delayed.
+
+This can have an important effect when using SimpleBLE with UI applications,
+such as WxWidgets or Unity.
+
+A future version of the library will rely on the use of a thread pool to run
+callbacks, thus mitigating the potential of these problems.
+
 ### Security
 
 One key security feature of the library is it allows the user to specify
@@ -93,6 +107,13 @@ from internal or secure sources without the risk of those getting compromised.
 This is done by specifying the additional command line arguments to
 the `cmake` command:
 
+- `SIMPLEBLUEZ_GIT_REPOSITORY`
+  Call CMake with `-DSIMPLEBLUEZ_GIT_REPOSITORY=<path>` to override the
+  default location of the SimpleBluez repository.
+- `SIMPLEBLUEZ_GIT_TAG`
+  Call CMake with `-DSIMPLEBLUEZ_GIT_TAG=<tag>` to override the default
+  tag of the SimpleBluez repository.
+
 - `SIMPLEDBUS_GIT_REPOSITORY`
   Call CMake with `-DSIMPLEDBUS_GIT_REPOSITORY=<path>` to override the
   default location of the SimpleDBus repository.
@@ -100,6 +121,29 @@ the `cmake` command:
   Call CMake with `-DSIMPLEDBUS_GIT_TAG=<tag>` to override the default
   tag of the SimpleDBus repository.
 
+Alternatively, instead of specifying the repository and tag, the user can
+specify the path to a local version of the dependencies. This is done by
+specifying the additional command line arguments to the `cmake` command:
+
+  - `SIMPLEBLUEZ_LOCAL_PATH`
+    Call CMake with `-DSIMPLEBLUEZ_LOCAL_PATH=<path>` to override the
+    default location of the SimpleBluez repository.
+  - `SIMPLEDBUS_LOCAL_PATH`
+    Call CMake with `-DSIMPLEDBUS_LOCAL_PATH=<path>` to override the
+    default location of the SimpleDBus repository.
+
+### Tests
+Testing is currently not available for the library as a whole, yet there are some
+build settings that can be used to find issues with the library.
+
+#### (Linux) Address Sanitizer
+In order to run tests with Address Sanitizer, CMake needs to be called with
+the following option: `-DSIMPLEBLE_SANITIZE=Address`.
+
+#### (Linux) Thread Sanitizer
+In order to run tests with Thread Sanitizer, CMake needs to be called with
+the following option: `-DSIMPLEBLE_SANITIZE=Thread`.
+
 ## Collaborating
 
 ### Coding & Naming Conventions
@@ -110,63 +154,20 @@ the `cmake` command:
     - Class protected and private property names must end with an underscore (`_`).
     - Class protected and private method names must start with an underscore (`_`).
 
-### API per OS
-
-The following tables describe the state of each available API per operating system.
-Any field that is not specified as supported will throw a compilation error if used
-or just be ignored.
-
-| API                                                  | Linux | Windows | MacOS |
-| ---------------------------------------------------- | ----- | ------- | ----- |
-| `SimpleBLE::Adapter::identifier`                     | Yes   | Yes     | Yes   |
-| `SimpleBLE::Adapter::address`                        | Yes   | Yes     | Yes   |
-| `SimpleBLE::Adapter::scan_start`                     | Yes   | Yes     | Yes   |
-| `SimpleBLE::Adapter::scan_stop`                      | Yes   | Yes     | Yes   |
-| `SimpleBLE::Adapter::scan_for`                       | Yes   | Yes     | Yes   |
-| `SimpleBLE::Adapter::scan_is_active`                 | Yes   | Yes     | Yes   |
-| `SimpleBLE::Adapter::scan_get_results`               | Yes   | Yes     | Yes   |
-| `SimpleBLE::Adapter::set_callback_on_scan_start`     | Yes   | Yes     | Yes   |
-| `SimpleBLE::Adapter::set_callback_on_scan_stop`      | Yes   | Yes     | Yes   |
-| `SimpleBLE::Adapter::set_callback_on_scan_updated`   | Yes   | Yes     | Yes   |
-| `SimpleBLE::Adapter::set_callback_on_scan_found`     | Yes   | Yes     | Yes   |
-| `SimpleBLE::Adapter::get_adapters`                   | Yes   | Yes     | Yes   |
-| ---------------------------------------------------- | ----- | ------- | ----- |
-| `SimpleBLE::Peripheral::identifier`                  | Yes   | Yes     | Yes   |
-| `SimpleBLE::Peripheral::address`                     | Yes   | Yes     | Yes   |
-| `SimpleBLE::Peripheral::connect`                     | Yes   | Yes     | Yes   |
-| `SimpleBLE::Peripheral::disconnect`                  | Yes   | Yes     | Yes   |
-| `SimpleBLE::Peripheral::is_connected`                | Yes   | Yes     | Yes   |
-| `SimpleBLE::Peripheral::is_connectable`              | No    | No      | Yes   |
-| `SimpleBLE::Peripheral::services`                    | Yes   | Yes     | Yes   |
-| `SimpleBLE::Peripheral::manufacturer_data`           | No    | No      | No    |
-| `SimpleBLE::Peripheral::read`                        | No    | Yes     | Yes   |
-| `SimpleBLE::Peripheral::write_request`               | Yes   | Yes     | Yes   |
-| `SimpleBLE::Peripheral::write_command`               | Yes   | Yes     | Yes   |
-| `SimpleBLE::Peripheral::notify`                      | Yes   | Yes     | Yes   |
-| `SimpleBLE::Peripheral::indicate`                    | Yes   | Yes     | Yes   |
-| `SimpleBLE::Peripheral::unsubscribe`                 | Yes   | Yes     | Yes   |
-| `SimpleBLE::Peripheral::set_callback_on_connected`   | Yes   | Yes     | Yes   |
-| `SimpleBLE::Peripheral::set_callback_on_disconnected`| Yes   | Yes     | Yes   |
-
 ## Known Issues / To-Do's
 - [Linux] Fork safety is not guaranteed.
-- [Linux] `SimpleBLE::Peripheral::read` does not work.
 - [MacOS] Only the main system adapter can be detected.
 - [MacOS] Implementation has incomplete error handling and might crash if invalid parameters are passed.
 - [MacOS] Timeout logic can be DRYed up.
 - [MacOS] Raise exceptions upon error.
 - [MacOS] Service and characteristic UUIDs need to be normalized.
 - [Windows] Unclear if multiple adapters can be detected.
+- [All] Run callbacks in separate threads to prevent blocking internal threads.
 - [All] Add a signal handler to ensure all objects are disconnected when the program exits.
-- [All] Add safe version of the library that won't trigger any exceptions.
 - [All] Replace C-style casts with C++ style casts.
 - [All] Add Python bindings for the library.
 - [All] Add Javascript bindings for the library.
 - [All] Add Kotlin bindings for the library.
 
-## Ideas
-- Explore if callbacks can be wrapped in shared pointers to prevent them from being prematurely deleted.
-- Explore if callbacks can be handled in a separate thread to prevent blocking the main thread.
-
 ## License
 All components within this project that have not been bundled from external creators, are licensed under the terms of the [MIT Licence](LICENCE.md).
diff --git a/third_party/SimpleBLE/examples/CMakeLists.txt b/third_party/SimpleBLE/examples/CMakeLists.txt
index 64ee442f3..e4083c54e 100644
--- a/third_party/SimpleBLE/examples/CMakeLists.txt
+++ b/third_party/SimpleBLE/examples/CMakeLists.txt
@@ -6,6 +6,9 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
 set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
 
+# Nice hack to automatically ignore the build directory
+file(WRITE ${CMAKE_BINARY_DIR}/.gitignore "*")
+
 set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_EXTENSIONS OFF)
 
@@ -23,3 +26,4 @@ add_subdirectory(connect_safe)
 add_subdirectory(read)
 add_subdirectory(write)
 add_subdirectory(notify)
+add_subdirectory(notify_c)
diff --git a/third_party/SimpleBLE/examples/connect/list_adapters/CMakeLists.txt b/third_party/SimpleBLE/examples/connect/list_adapters/CMakeLists.txt
deleted file mode 100644
index eeda2e9a2..000000000
--- a/third_party/SimpleBLE/examples/connect/list_adapters/CMakeLists.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-cmake_minimum_required(VERSION 3.16.0)
-
-project(EXAMPLE_LIST_ADAPTERS)
-
-message("-- [INFO] Building Example")
-add_executable(example_list_adapters list_adapters.cpp)
-target_link_libraries(example_list_adapters simpleble-static)
diff --git a/third_party/SimpleBLE/examples/connect/list_adapters/list_adapters.cpp b/third_party/SimpleBLE/examples/connect/list_adapters/list_adapters.cpp
deleted file mode 100644
index cc6318fc8..000000000
--- a/third_party/SimpleBLE/examples/connect/list_adapters/list_adapters.cpp
+++ /dev/null
@@ -1,11 +0,0 @@
-#include <iostream>
-
-#include "simpleble/Adapter.h"
-
-int main(int argc, char* argv[]) {
-    auto adapter_list = SimpleBLE::Adapter::get_adapters();
-    for (auto& adapter : adapter_list) {
-        std::cout << "Adapter: " << adapter.identifier() << " [" << adapter.address() << "]" << std::endl;
-    }
-    return 0;
-}
\ No newline at end of file
diff --git a/third_party/SimpleBLE/examples/connect/notify/CMakeLists.txt b/third_party/SimpleBLE/examples/connect/notify/CMakeLists.txt
deleted file mode 100644
index f2a314fd3..000000000
--- a/third_party/SimpleBLE/examples/connect/notify/CMakeLists.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-cmake_minimum_required(VERSION 3.16.0)
-
-project(EXAMPLE_NOTIFY)
-
-message("-- [INFO] Building Example")
-add_executable(example_notify notify.cpp)
-target_link_libraries(example_notify simpleble-static)
diff --git a/third_party/SimpleBLE/examples/connect/notify/notify.cpp b/third_party/SimpleBLE/examples/connect/notify/notify.cpp
deleted file mode 100644
index dc5d0c747..000000000
--- a/third_party/SimpleBLE/examples/connect/notify/notify.cpp
+++ /dev/null
@@ -1,89 +0,0 @@
-#include <chrono>
-#include <iomanip>
-#include <iostream>
-#include <thread>
-#include <vector>
-
-#include "simpleble/SimpleBLE.h"
-
-std::vector<SimpleBLE::Peripheral> peripherals;
-
-void print_byte_array(SimpleBLE::ByteArray& bytes) {
-    for (auto byte : bytes) {
-        std::cout << std::hex << std::setfill('0') << (uint32_t)((uint8_t)byte) << " ";
-    }
-    std::cout << std::endl;
-}
-
-int main(int argc, char* argv[]) {
-    auto adapter_list = SimpleBLE::Adapter::get_adapters();
-
-    if (adapter_list.size() == 0) {
-        std::cout << "No adapter was found." << std::endl;
-    }
-
-    // Pick the first detected adapter as the default.
-    // TODO: Allow the user to pick an adapter.
-    SimpleBLE::Adapter adapter = adapter_list[0];
-
-    adapter.set_callback_on_scan_start([]() { std::cout << "Scan started." << std::endl; });
-
-    adapter.set_callback_on_scan_stop([]() { std::cout << "Scan stopped." << std::endl; });
-
-    adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) {
-        std::cout << "Found device: " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl;
-        peripherals.push_back(peripheral);
-    });
-
-    // Scan for 5 seconds and return.
-    adapter.scan_for(5000);
-
-    std::cout << "The following devices were found:" << std::endl;
-    for (int i = 0; i < peripherals.size(); i++) {
-        std::cout << "[" << i << "] " << peripherals[i].identifier() << " [" << peripherals[i].address() << "]"
-                  << std::endl;
-    }
-
-    int selection = -1;
-    std::cout << "Please select a device to connect to: ";
-    std::cin >> selection;
-
-    if (selection >= 0 && selection < peripherals.size()) {
-        auto peripheral = peripherals[selection];
-        std::cout << "Connecting to " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl;
-        peripheral.connect();
-
-        std::cout << "Successfully connected, printing services and characteristics.." << std::endl;
-
-        // Store all service and characteristic uuids in a vector.
-        std::vector<std::pair<SimpleBLE::BluetoothUUID, SimpleBLE::BluetoothUUID>> uuids;
-        for (auto service : peripheral.services()) {
-            for (auto characteristic : service.characteristics) {
-                uuids.push_back(std::make_pair(service.uuid, characteristic));
-            }
-        }
-
-        std::cout << "The following services and characteristics were found:" << std::endl;
-        for (int i = 0; i < uuids.size(); i++) {
-            std::cout << "[" << i << "] " << uuids[i].first << " " << uuids[i].second << std::endl;
-        }
-
-        std::cout << "Please select a characteristic to read: ";
-        std::cin >> selection;
-
-        if (selection >= 0 && selection < uuids.size()) {
-            // Subscribe to the characteristic.
-            peripheral.notify(uuids[selection].first, uuids[selection].second, [&](SimpleBLE::ByteArray bytes) {
-                std::cout << "Received: ";
-                print_byte_array(bytes);
-            });
-
-            std::this_thread::sleep_for(std::chrono::seconds(5));
-
-            peripheral.unsubscribe(uuids[selection].first, uuids[selection].second);
-        }
-        peripheral.disconnect();
-    }
-
-    return 0;
-}
diff --git a/third_party/SimpleBLE/examples/connect/read/read.cpp b/third_party/SimpleBLE/examples/connect/read/read.cpp
deleted file mode 100644
index 5b534c7fe..000000000
--- a/third_party/SimpleBLE/examples/connect/read/read.cpp
+++ /dev/null
@@ -1,84 +0,0 @@
-#include <chrono>
-#include <iomanip>
-#include <iostream>
-#include <thread>
-#include <vector>
-
-#include "simpleble/SimpleBLE.h"
-
-std::vector<SimpleBLE::Peripheral> peripherals;
-
-void print_byte_array(SimpleBLE::ByteArray& bytes) {
-    for (auto byte : bytes) {
-        std::cout << std::hex << std::setfill('0') << (uint32_t)((uint8_t)byte) << " ";
-    }
-    std::cout << std::endl;
-}
-
-int main(int argc, char* argv[]) {
-    auto adapter_list = SimpleBLE::Adapter::get_adapters();
-
-    if (adapter_list.size() == 0) {
-        std::cout << "No adapter was found." << std::endl;
-    }
-
-    // Pick the first detected adapter as the default.
-    // TODO: Allow the user to pick an adapter.
-    SimpleBLE::Adapter adapter = adapter_list[0];
-
-    adapter.set_callback_on_scan_start([]() { std::cout << "Scan started." << std::endl; });
-
-    adapter.set_callback_on_scan_stop([]() { std::cout << "Scan stopped." << std::endl; });
-
-    adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) {
-        std::cout << "Found device: " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl;
-        peripherals.push_back(peripheral);
-    });
-
-    // Scan for 5 seconds and return.
-    adapter.scan_for(5000);
-
-    std::cout << "The following devices were found:" << std::endl;
-    for (int i = 0; i < peripherals.size(); i++) {
-        std::cout << "[" << i << "] " << peripherals[i].identifier() << " [" << peripherals[i].address() << "]"
-                  << std::endl;
-    }
-
-    int selection = -1;
-    std::cout << "Please select a device to connect to: ";
-    std::cin >> selection;
-
-    if (selection >= 0 && selection < peripherals.size()) {
-        auto peripheral = peripherals[selection];
-        std::cout << "Connecting to " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl;
-        peripheral.connect();
-
-        std::cout << "Successfully connected, printing services and characteristics.." << std::endl;
-
-        // Store all service and characteristic uuids in a vector.
-        std::vector<std::pair<SimpleBLE::BluetoothUUID, SimpleBLE::BluetoothUUID>> uuids;
-        for (auto service : peripheral.services()) {
-            for (auto characteristic : service.characteristics) {
-                uuids.push_back(std::make_pair(service.uuid, characteristic));
-            }
-        }
-
-        std::cout << "The following services and characteristics were found:" << std::endl;
-        for (int i = 0; i < uuids.size(); i++) {
-            std::cout << "[" << i << "] " << uuids[i].first << " " << uuids[i].second << std::endl;
-        }
-
-        std::cout << "Please select a characteristic to read: ";
-        std::cin >> selection;
-
-        if (selection >= 0 && selection < uuids.size()) {
-            // Attempt to read the characteristic.
-            SimpleBLE::ByteArray rx_data = peripheral.read(uuids[selection].first, uuids[selection].second);
-            std::cout << "Characteristic contents were: ";
-            print_byte_array(rx_data);
-        }
-        peripheral.disconnect();
-    }
-
-    return 0;
-}
diff --git a/third_party/SimpleBLE/examples/connect/scan/scan.cpp b/third_party/SimpleBLE/examples/connect/scan/scan.cpp
deleted file mode 100644
index 61465379c..000000000
--- a/third_party/SimpleBLE/examples/connect/scan/scan.cpp
+++ /dev/null
@@ -1,39 +0,0 @@
-#include <chrono>
-#include <iostream>
-#include <thread>
-
-#include "simpleble/Adapter.h"
-
-int main(int argc, char* argv[]) {
-    auto adapter_list = SimpleBLE::Adapter::get_adapters();
-
-    if (adapter_list.size() == 0) {
-        std::cout << "No adapter was found." << std::endl;
-    }
-
-    // Pick the first detected adapter as the default.
-    // TODO: Allow the user to pick the adapter.
-    SimpleBLE::Adapter adapter = adapter_list[0];
-
-    adapter.set_callback_on_scan_start([]() { std::cout << "Scan started." << std::endl; });
-
-    adapter.set_callback_on_scan_stop([]() { std::cout << "Scan stopped." << std::endl; });
-
-    adapter.set_callback_on_scan_found([](SimpleBLE::Peripheral peripheral) {
-        std::cout << "Found device: " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl;
-    });
-
-    adapter.set_callback_on_scan_updated([](SimpleBLE::Peripheral peripheral) {
-        std::cout << "Updated device: " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl;
-    });
-
-    // Scan for 5 seconds and return.
-    adapter.scan_for(5000);
-
-    // Sleep for an additional second before returning.
-    // If there are any detections during this period, it means that the
-    // internal peripheral took longer to stop than anticipated.
-    std::this_thread::sleep_for(std::chrono::seconds(1));
-
-    return 0;
-}
diff --git a/third_party/SimpleBLE/examples/connect/write/CMakeLists.txt b/third_party/SimpleBLE/examples/connect/write/CMakeLists.txt
deleted file mode 100644
index e5d46034e..000000000
--- a/third_party/SimpleBLE/examples/connect/write/CMakeLists.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-cmake_minimum_required(VERSION 3.16.0)
-
-project(EXAMPLE_WRITE)
-
-message("-- [INFO] Building Example")
-add_executable(example_write write.cpp)
-target_link_libraries(example_write simpleble-static)
diff --git a/third_party/SimpleBLE/examples/connect/write/write.cpp b/third_party/SimpleBLE/examples/connect/write/write.cpp
deleted file mode 100644
index 3a039bfca..000000000
--- a/third_party/SimpleBLE/examples/connect/write/write.cpp
+++ /dev/null
@@ -1,82 +0,0 @@
-#include <chrono>
-#include <iomanip>
-#include <iostream>
-#include <thread>
-#include <vector>
-
-#include "simpleble/SimpleBLE.h"
-
-std::vector<SimpleBLE::Peripheral> peripherals;
-
-int main(int argc, char* argv[]) {
-    auto adapter_list = SimpleBLE::Adapter::get_adapters();
-
-    if (adapter_list.size() == 0) {
-        std::cout << "No adapter was found." << std::endl;
-    }
-
-    // Pick the first detected adapter as the default.
-    // TODO: Allow the user to pick an adapter.
-    SimpleBLE::Adapter adapter = adapter_list[0];
-
-    adapter.set_callback_on_scan_start([]() { std::cout << "Scan started." << std::endl; });
-
-    adapter.set_callback_on_scan_stop([]() { std::cout << "Scan stopped." << std::endl; });
-
-    adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) {
-        std::cout << "Found device: " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl;
-        peripherals.push_back(peripheral);
-    });
-
-    // Scan for 5 seconds and return.
-    adapter.scan_for(5000);
-
-    std::cout << "The following devices were found:" << std::endl;
-    for (int i = 0; i < peripherals.size(); i++) {
-        std::cout << "[" << i << "] " << peripherals[i].identifier() << " [" << peripherals[i].address() << "]"
-                  << std::endl;
-    }
-
-    int selection = -1;
-    std::cout << "Please select a device to connect to: ";
-    std::cin >> selection;
-
-    if (selection >= 0 && selection < peripherals.size()) {
-        auto peripheral = peripherals[selection];
-        std::cout << "Connecting to " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl;
-        peripheral.connect();
-
-        std::cout << "Successfully connected, printing services and characteristics.." << std::endl;
-
-        // Store all service and characteristic uuids in a vector.
-        std::vector<std::pair<SimpleBLE::BluetoothUUID, SimpleBLE::BluetoothUUID>> uuids;
-        for (auto service : peripheral.services()) {
-            for (auto characteristic : service.characteristics) {
-                uuids.push_back(std::make_pair(service.uuid, characteristic));
-            }
-        }
-
-        std::cout << "The following services and characteristics were found:" << std::endl;
-        for (int i = 0; i < uuids.size(); i++) {
-            std::cout << "[" << i << "] " << uuids[i].first << " " << uuids[i].second << std::endl;
-        }
-
-        std::cout << "Please select a characteristic to write into: ";
-        std::cin >> selection;
-
-        std::string contents;
-        std::cout << "Please write the contents to be sent: ";
-        std::cin >> contents;
-
-        if (selection >= 0 && selection < uuids.size()) {
-            // NOTE: Alternatively, `write_command` can be used to write to a characteristic too.
-            // `write_request` is for unacknowledged writes.
-            // `write_command` is for acknowledged writes.
-            peripheral.write_request(uuids[selection].first, uuids[selection].second, contents);
-        }
-
-        peripheral.disconnect();
-    }
-
-    return 0;
-}
diff --git a/third_party/SimpleBLE/examples/list_adapters/list_adapters.cpp b/third_party/SimpleBLE/examples/list_adapters/list_adapters.cpp
index cc6318fc8..febb4d157 100644
--- a/third_party/SimpleBLE/examples/list_adapters/list_adapters.cpp
+++ b/third_party/SimpleBLE/examples/list_adapters/list_adapters.cpp
@@ -4,8 +4,15 @@
 
 int main(int argc, char* argv[]) {
     auto adapter_list = SimpleBLE::Adapter::get_adapters();
+
+    if (adapter_list.size() == 0) {
+        std::cout << "No adapters found" << std::endl;
+        return 0;
+    }
+
     for (auto& adapter : adapter_list) {
         std::cout << "Adapter: " << adapter.identifier() << " [" << adapter.address() << "]" << std::endl;
     }
+    
     return 0;
 }
\ No newline at end of file
diff --git a/third_party/SimpleBLE/examples/notify_c/CMakeLists.txt b/third_party/SimpleBLE/examples/notify_c/CMakeLists.txt
new file mode 100644
index 000000000..c9c9618c7
--- /dev/null
+++ b/third_party/SimpleBLE/examples/notify_c/CMakeLists.txt
@@ -0,0 +1,7 @@
+cmake_minimum_required(VERSION 3.16.0)
+
+project(EXAMPLE_NOTIFY_C)
+
+message("-- [INFO] Building Example")
+add_executable(example_notify_c notify_c.c)
+target_link_libraries(example_notify_c simpleble-c-static)
diff --git a/third_party/SimpleBLE/examples/notify_c/notify_c.c b/third_party/SimpleBLE/examples/notify_c/notify_c.c
new file mode 100644
index 000000000..faec4f169
--- /dev/null
+++ b/third_party/SimpleBLE/examples/notify_c/notify_c.c
@@ -0,0 +1,215 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+#ifdef _WIN32
+#include <Windows.h>
+#else
+#include <unistd.h>
+#endif
+
+void msleep(int ms) {
+#ifdef _WIN32
+    Sleep(ms);
+#else
+    usleep(ms * 1000);
+#endif
+}
+
+#include "simpleble_c/simpleble.h"
+
+#define PERIPHERAL_LIST_SIZE (size_t)10
+#define SERVICES_LIST_SIZE (size_t)32
+
+typedef struct {
+    simpleble_uuid_t service;
+    simpleble_uuid_t characteristic;
+} service_characteristic_t;
+
+static void clean_on_exit(void);
+
+static void adapter_on_scan_start(simpleble_adapter_t adapter, void* userdata);
+static void adapter_on_scan_stop(simpleble_adapter_t adapter, void* userdata);
+static void adapter_on_scan_found(simpleble_adapter_t adapter, simpleble_peripheral_t peripheral, void* userdata);
+static void peripheral_on_notify(simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t* data,
+                                 size_t data_length, void* userdata);
+
+static service_characteristic_t characteristic_list[SERVICES_LIST_SIZE] = {0};
+static simpleble_peripheral_t peripheral_list[PERIPHERAL_LIST_SIZE] = {0};
+static size_t peripheral_list_len = 0;
+static simpleble_adapter_t adapter = NULL;
+
+int main(int argc, char* argv[]) {
+    simpleble_err_t err_code = SIMPLEBLE_SUCCESS;
+    atexit(clean_on_exit);
+
+    // NOTE: It's necessary to call this function before any other to allow the
+    // underlying driver to run its initialization routine.
+    size_t adapter_count = simpleble_adapter_get_count();
+    if (adapter_count == 0) {
+        printf("No adapter was found.\n");
+        return 1;
+    }
+
+    // TODO: Allow the user to pick an adapter.
+    adapter = simpleble_adapter_get_handle(0);
+    if (adapter == NULL) {
+        printf("No adapter was found.\n");
+        return 1;
+    }
+
+    simpleble_adapter_set_callback_on_scan_start(adapter, adapter_on_scan_start, NULL);
+    simpleble_adapter_set_callback_on_scan_stop(adapter, adapter_on_scan_stop, NULL);
+    simpleble_adapter_set_callback_on_scan_found(adapter, adapter_on_scan_found, NULL);
+
+    simpleble_adapter_scan_for(adapter, 5000);
+
+    printf("The following devices were found:\n");
+    for (size_t i = 0; i < peripheral_list_len; i++) {
+        simpleble_peripheral_t peripheral = peripheral_list[i];
+        char* peripheral_identifier = simpleble_peripheral_identifier(peripheral);
+        char* peripheral_address = simpleble_peripheral_address(peripheral);
+        printf("[%zu] %s [%s]\n", i, peripheral_identifier, peripheral_address);
+        simpleble_free(peripheral_identifier);
+        simpleble_free(peripheral_address);
+    }
+
+    int selection = -1;
+    printf("Please select a device to connect to: ");
+    scanf("%d", &selection);
+
+    if (selection < 0 || selection >= (int)peripheral_list_len) {
+        printf("Invalid selection.\n");
+        return 1;
+    }
+
+    simpleble_peripheral_t peripheral = peripheral_list[selection];
+
+    char* peripheral_identifier = simpleble_peripheral_identifier(peripheral);
+    char* peripheral_address = simpleble_peripheral_address(peripheral);
+    printf("Connecting to %s [%s]\n", peripheral_identifier, peripheral_address);
+    simpleble_free(peripheral_identifier);
+    simpleble_free(peripheral_address);
+
+    err_code = simpleble_peripheral_connect(peripheral);
+    if (err_code != SIMPLEBLE_SUCCESS) {
+        printf("Failed to connect.\n");
+        return 1;
+    }
+
+    printf("Successfully connected, listing services and characteristics.\n");
+
+    size_t characteristic_count = 0;
+    for (size_t i = 0; i < simpleble_peripheral_services_count(peripheral); i++) {
+        simpleble_service_t service;
+        err_code = simpleble_peripheral_services_get(peripheral, i, &service);
+
+        if (err_code != SIMPLEBLE_SUCCESS) {
+            printf("Failed to get service.\n");
+            return 1;
+        }
+
+        for (size_t j = 0; j < service.characteristic_count; j++) {
+            if (characteristic_count >= SERVICES_LIST_SIZE) {
+                break;
+            }
+
+            printf("[%ld] %s %s\n", characteristic_count, service.uuid.value, service.characteristics[j].value);
+            characteristic_list[characteristic_count].service = service.uuid;
+            characteristic_list[characteristic_count].characteristic = service.characteristics[j];
+            characteristic_count++;
+        }
+    }
+
+    selection = -1;
+    printf("Please select a characteristic to read from: ");
+    scanf("%d", &selection);
+
+    if (selection < 0 || selection >= (int)characteristic_count) {
+        printf("Invalid selection.\n");
+        return 1;
+    }
+
+    simpleble_peripheral_notify(peripheral, characteristic_list[selection].service,
+                                characteristic_list[selection].characteristic, peripheral_on_notify, NULL);
+
+    // Sleep for 5 seconds
+    msleep(5000);
+
+    simpleble_peripheral_unsubscribe(peripheral, characteristic_list[selection].service,
+                                     characteristic_list[selection].characteristic);
+
+    simpleble_peripheral_disconnect(peripheral);
+
+    return 0;
+}
+
+static void clean_on_exit(void) {
+    printf("Releasing allocated resources.\n");
+
+    // Release all saved peripherals
+    for (size_t i = 0; i < peripheral_list_len; i++) {
+        simpleble_peripheral_release_handle(peripheral_list[i]);
+    }
+
+    // Let's not forget to release the associated handle.
+    simpleble_adapter_release_handle(adapter);
+}
+
+static void adapter_on_scan_start(simpleble_adapter_t adapter, void* userdata) {
+    char* identifier = simpleble_adapter_identifier(adapter);
+
+    if (identifier == NULL) {
+        return;
+    }
+
+    printf("Adapter %s started scanning.\n", identifier);
+
+    // Let's not forget to clear the allocated memory.
+    simpleble_free(identifier);
+}
+
+static void adapter_on_scan_stop(simpleble_adapter_t adapter, void* userdata) {
+    char* identifier = simpleble_adapter_identifier(adapter);
+
+    if (identifier == NULL) {
+        return;
+    }
+
+    printf("Adapter %s stopped scanning.\n", identifier);
+
+    // Let's not forget to clear the allocated memory.
+    simpleble_free(identifier);
+}
+
+static void adapter_on_scan_found(simpleble_adapter_t adapter, simpleble_peripheral_t peripheral, void* userdata) {
+    char* adapter_identifier = simpleble_adapter_identifier(adapter);
+    char* peripheral_identifier = simpleble_peripheral_identifier(peripheral);
+    char* peripheral_address = simpleble_peripheral_address(peripheral);
+
+    if (adapter_identifier == NULL || peripheral_identifier == NULL || peripheral_address == NULL) {
+        return;
+    }
+
+    printf("Adapter %s found device: %s [%s]\n", adapter_identifier, peripheral_identifier, peripheral_address);
+
+    if (peripheral_list_len < PERIPHERAL_LIST_SIZE) {
+        // Save the peripheral
+        peripheral_list[peripheral_list_len++] = peripheral;
+    } else {
+        // As there was no space left for this peripheral, release the associated handle.
+        simpleble_peripheral_release_handle(peripheral);
+    }
+
+    // Let's not forget to release all allocated memory.
+    simpleble_free(peripheral_identifier);
+    simpleble_free(peripheral_address);
+}
+
+static void peripheral_on_notify(simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t* data,
+                                 size_t data_length, void* userdata) {
+    printf("Received: ");
+    for (size_t i = 0; i < data_length; i++) {
+        printf("%02X ", data[i]);
+    }
+    printf("\n");
+}
diff --git a/third_party/SimpleBLE/examples/scan/scan.cpp b/third_party/SimpleBLE/examples/scan/scan.cpp
index 9e86a66d9..985b1636b 100644
--- a/third_party/SimpleBLE/examples/scan/scan.cpp
+++ b/third_party/SimpleBLE/examples/scan/scan.cpp
@@ -1,9 +1,19 @@
 #include <chrono>
 #include <iostream>
 #include <thread>
+#include <sstream>
+#include <iomanip>
 
 #include "simpleble/Adapter.h"
 
+std::string byte_array_to_string(SimpleBLE::ByteArray& bytes) {
+    std::ostringstream oss;
+    for (auto byte : bytes) {
+        oss << std::hex << std::setfill('0') << std::setw(2) << (uint32_t)((uint8_t)byte) << " ";
+    }
+    return oss.str();
+}
+
 int main(int argc, char* argv[]) {
     auto adapter_list = SimpleBLE::Adapter::get_adapters();
 
@@ -40,8 +50,15 @@ int main(int argc, char* argv[]) {
     std::vector<SimpleBLE::Peripheral> peripherals = adapter.scan_get_results();
     std::cout << "The following devices were found:" << std::endl;
     for (int i = 0; i < peripherals.size(); i++) {
-        std::cout << "[" << i << "] " << peripherals[i].identifier() << " [" << peripherals[i].address() << "]"
-                  << std::endl;
+        std::string connectable_string = peripherals[i].is_connectable() ? "Connectable" : "Non-Connectable";
+        std::string peripheral_string = peripherals[i].identifier() + " [" + peripherals[i].address() + "]";
+
+        std::cout << "[" << i << "] " << peripheral_string << " " << connectable_string << std::endl;
+        std::map<uint16_t, SimpleBLE::ByteArray> manufacturer_data = peripherals[i].manufacturer_data();
+        for (auto& [manufacturer_id, data] : manufacturer_data) {
+            std::cout << "    Manufacturer ID: " << manufacturer_id << std::endl;
+            std::cout << "    Manufacturer data: " << byte_array_to_string(data) << std::endl;
+        }
     }
 
     return 0;
diff --git a/third_party/SimpleBLE/include/simpleble/Adapter.h b/third_party/SimpleBLE/include/simpleble/Adapter.h
index 9b4c3c25c..6b023b57a 100644
--- a/third_party/SimpleBLE/include/simpleble/Adapter.h
+++ b/third_party/SimpleBLE/include/simpleble/Adapter.h
@@ -16,7 +16,7 @@ class AdapterBase;
 class Adapter {
   public:
     Adapter();
-    ~Adapter();
+    virtual ~Adapter();
 
     std::string identifier();
     BluetoothAddress address();
diff --git a/third_party/SimpleBLE/include/simpleble/AdapterSafe.h b/third_party/SimpleBLE/include/simpleble/AdapterSafe.h
index b8f4920e5..ccbd3cc08 100644
--- a/third_party/SimpleBLE/include/simpleble/AdapterSafe.h
+++ b/third_party/SimpleBLE/include/simpleble/AdapterSafe.h
@@ -10,7 +10,7 @@ namespace Safe {
 class Adapter : public SimpleBLE::Adapter {
   public:
     Adapter(SimpleBLE::Adapter& adapter);
-    ~Adapter();
+    virtual ~Adapter();
 
     std::optional<std::string> identifier() noexcept;
     std::optional<BluetoothAddress> address() noexcept;
diff --git a/third_party/SimpleBLE/include/simpleble/Peripheral.h b/third_party/SimpleBLE/include/simpleble/Peripheral.h
index 594f52b57..ce5ea27fc 100644
--- a/third_party/SimpleBLE/include/simpleble/Peripheral.h
+++ b/third_party/SimpleBLE/include/simpleble/Peripheral.h
@@ -16,7 +16,7 @@ class PeripheralBase;
 class Peripheral {
   public:
     Peripheral();
-    ~Peripheral();
+    virtual ~Peripheral();
 
     std::string identifier();
     BluetoothAddress address();
diff --git a/third_party/SimpleBLE/include/simpleble/PeripheralSafe.h b/third_party/SimpleBLE/include/simpleble/PeripheralSafe.h
index 14fa83b9c..24bacb7d3 100644
--- a/third_party/SimpleBLE/include/simpleble/PeripheralSafe.h
+++ b/third_party/SimpleBLE/include/simpleble/PeripheralSafe.h
@@ -11,7 +11,7 @@ namespace Safe {
 class Peripheral : public SimpleBLE::Peripheral {
   public:
     Peripheral(SimpleBLE::Peripheral& peripheral);
-    ~Peripheral();
+    virtual ~Peripheral();
 
     std::optional<std::string> identifier() noexcept;
     std::optional<BluetoothAddress> address() noexcept;
diff --git a/third_party/SimpleBLE/include/simpleble_c/adapter.h b/third_party/SimpleBLE/include/simpleble_c/adapter.h
index 198995fc7..e5c7adcd7 100644
--- a/third_party/SimpleBLE/include/simpleble_c/adapter.h
+++ b/third_party/SimpleBLE/include/simpleble_c/adapter.h
@@ -4,10 +4,9 @@
 #include <stddef.h>
 #include <stdint.h>
 
+#include <simpleble_c/shared_export.h>
 #include <simpleble_c/types.h>
 
-#include "shared_export.h"
-
 #ifdef __cplusplus
 extern "C" {
 #endif
diff --git a/third_party/SimpleBLE/include/simpleble_c/peripheral.h b/third_party/SimpleBLE/include/simpleble_c/peripheral.h
index 203d02958..2e45e933d 100644
--- a/third_party/SimpleBLE/include/simpleble_c/peripheral.h
+++ b/third_party/SimpleBLE/include/simpleble_c/peripheral.h
@@ -4,10 +4,9 @@
 #include <stddef.h>
 #include <stdint.h>
 
+#include <simpleble_c/shared_export.h>
 #include <simpleble_c/types.h>
 
-#include "shared_export.h"
-
 #ifdef __cplusplus
 extern "C" {
 #endif
@@ -141,7 +140,8 @@ SHARED_EXPORT simpleble_err_t CALLING_CONVENTION simpleble_peripheral_read(simpl
 SHARED_EXPORT simpleble_err_t CALLING_CONVENTION simpleble_peripheral_write_request(simpleble_peripheral_t handle,
                                                                                     simpleble_uuid_t service,
                                                                                     simpleble_uuid_t characteristic,
-                                                                                    uint8_t* data, size_t data_length);
+                                                                                    const uint8_t* data,
+                                                                                    size_t data_length);
 
 /**
  * @brief
@@ -156,7 +156,8 @@ SHARED_EXPORT simpleble_err_t CALLING_CONVENTION simpleble_peripheral_write_requ
 SHARED_EXPORT simpleble_err_t CALLING_CONVENTION simpleble_peripheral_write_command(simpleble_peripheral_t handle,
                                                                                     simpleble_uuid_t service,
                                                                                     simpleble_uuid_t characteristic,
-                                                                                    uint8_t* data, size_t data_length);
+                                                                                    const uint8_t* data,
+                                                                                    size_t data_length);
 
 /**
  * @brief
@@ -169,8 +170,8 @@ SHARED_EXPORT simpleble_err_t CALLING_CONVENTION simpleble_peripheral_write_comm
  */
 SHARED_EXPORT simpleble_err_t CALLING_CONVENTION
 simpleble_peripheral_notify(simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic,
-                            void (*callback)(simpleble_uuid_t service, simpleble_uuid_t characteristic, uint8_t* data,
-                                             size_t data_length, void* userdata),
+                            void (*callback)(simpleble_uuid_t service, simpleble_uuid_t characteristic,
+                                             const uint8_t* data, size_t data_length, void* userdata),
                             void* userdata);
 
 /**
@@ -184,8 +185,8 @@ simpleble_peripheral_notify(simpleble_peripheral_t handle, simpleble_uuid_t serv
  */
 SHARED_EXPORT simpleble_err_t CALLING_CONVENTION
 simpleble_peripheral_indicate(simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic,
-                              void (*callback)(simpleble_uuid_t service, simpleble_uuid_t characteristic, uint8_t* data,
-                                               size_t data_length, void* userdata),
+                              void (*callback)(simpleble_uuid_t service, simpleble_uuid_t characteristic,
+                                               const uint8_t* data, size_t data_length, void* userdata),
                               void* userdata);
 
 /**
diff --git a/third_party/SimpleBLE/include/simpleble_c/simpleble.h b/third_party/SimpleBLE/include/simpleble_c/simpleble.h
index 1dac87742..f9fd639d6 100644
--- a/third_party/SimpleBLE/include/simpleble_c/simpleble.h
+++ b/third_party/SimpleBLE/include/simpleble_c/simpleble.h
@@ -2,8 +2,7 @@
 
 #include <simpleble_c/adapter.h>
 #include <simpleble_c/peripheral.h>
-
-#include "shared_export.h"
+#include <simpleble_c/shared_export.h>
 
 #ifdef __cplusplus
 extern "C" {
diff --git a/third_party/SimpleBLE/scripts/linux/build_examples.sh b/third_party/SimpleBLE/scripts/linux/build_examples.sh
index 8ffbdda9c..3074faf34 100644
--- a/third_party/SimpleBLE/scripts/linux/build_examples.sh
+++ b/third_party/SimpleBLE/scripts/linux/build_examples.sh
@@ -22,5 +22,5 @@ fi
 # Compile!
 mkdir -p $BUILD_PATH
 cd $BUILD_PATH
-cmake -H$SOURCE_ROOT
+cmake -H$SOURCE_ROOT -DCMAKE_BUILD_TYPE=Debug
 make -j
diff --git a/third_party/SimpleBLE/scripts/windows/build_examples.ps1 b/third_party/SimpleBLE/scripts/windows/build_examples.ps1
index 1024e56be..ddf331f4f 100644
--- a/third_party/SimpleBLE/scripts/windows/build_examples.ps1
+++ b/third_party/SimpleBLE/scripts/windows/build_examples.ps1
@@ -9,17 +9,8 @@
 [string]$PROJECT_ROOT = Resolve-Path $($PSScriptRoot + "\..\..")
 [string]$BUILD_PATH = $PROJECT_ROOT + "\build"
 [string]$SOURCE_ROOT = $PROJECT_ROOT + "\examples"
-
-[string]$VSWHERE_PATH = Resolve-Path $($PROJECT_ROOT + "\scripts\windows\tools\vswhere.exe")
-# -products flag allows the system to also search for users who installed Visual Studio Build Tools 2019 (https://github.com/3F/hMSBuild/issues/12)
-[string]$MSBUILD_PATH = & $VSWHERE_PATH -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe -products * | select-object -first 1
 [string]$TARGET = "Release"
 
-if ($null -eq $MSBUILD_PATH) {
-    Write-Error "MSBuild path not found."
-    exit -1
-}
-
 # Validate the received architecture
 switch -regex ($arch) {
   'x86' {
@@ -38,10 +29,8 @@ if ($clean) {
 New-Item -ItemType Directory -Force -Path "$BUILD_PATH" | Out-Null
 
 # Run CMake to create our build files.
-cmake -S "$SOURCE_ROOT" -B "$BUILD_PATH" -A $WINDOWS_ARCH
-
-# Run the compiler
-& $MSBUILD_PATH "$BUILD_PATH\SimpleBLE_Examples.sln" -nologo -m:8 /bl:output.binlog /verbosity:minimal /p:Configuration=$TARGET
+cmake -S "$SOURCE_ROOT" -B "$BUILD_PATH" -A $WINDOWS_ARCH -DCMAKE_SYSTEM_VERSION="10.0.19041.0"
+cmake --build "$BUILD_PATH" --config $TARGET
 
 #Copy all generated files to the bin folder for consistency and remove the output folder.
 Copy-item -Force -Recurse "$BUILD_PATH\bin\$TARGET\*" -Destination "$BUILD_PATH\bin\"
diff --git a/third_party/SimpleBLE/scripts/windows/build_lib.ps1 b/third_party/SimpleBLE/scripts/windows/build_lib.ps1
index cc361801c..d102450b5 100644
--- a/third_party/SimpleBLE/scripts/windows/build_lib.ps1
+++ b/third_party/SimpleBLE/scripts/windows/build_lib.ps1
@@ -8,17 +8,8 @@
 
 [string]$PROJECT_ROOT = Resolve-Path $($PSScriptRoot + "\..\..")
 [string]$BUILD_PATH = $PROJECT_ROOT + "\build"
-
-[string]$VSWHERE_PATH = Resolve-Path $($PROJECT_ROOT + "\scripts\windows\tools\vswhere.exe")
-# -products flag allows the system to also search for users who installed Visual Studio Build Tools 2019 (https://github.com/3F/hMSBuild/issues/12)
-[string]$MSBUILD_PATH = & $VSWHERE_PATH -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe -products * | select-object -first 1
 [string]$TARGET = "Release"
 
-if ($null -eq $MSBUILD_PATH) {
-    Write-Error "MSBuild path not found."
-    exit -1
-}
-
 # Validate the received architecture
 switch -regex ($arch) {
   'x86' {
@@ -37,10 +28,8 @@ if ($clean) {
 New-Item -ItemType Directory -Force -Path "$BUILD_PATH" | Out-Null
 
 # Run CMake to create our build files.
-cmake -S "$PROJECT_ROOT" -B "$BUILD_PATH" -A $WINDOWS_ARCH
-
-# Run the compiler
-& $MSBUILD_PATH "$BUILD_PATH\SimpleBLE.sln" -nologo -m:8 /bl:output.binlog /verbosity:minimal /p:Configuration=$TARGET
+cmake -S "$PROJECT_ROOT" -B "$BUILD_PATH" -A $WINDOWS_ARCH -DCMAKE_SYSTEM_VERSION="10.0.19041.0"
+cmake --build "$BUILD_PATH" --config $TARGET
 
 #Copy all generated files to the bin folder for consistency and remove the output folder.
 Copy-item -Force -Recurse "$BUILD_PATH\bin\$TARGET\*" -Destination "$BUILD_PATH\bin\"
diff --git a/third_party/SimpleBLE/scripts/windows/tools/vswhere.exe b/third_party/SimpleBLE/scripts/windows/tools/vswhere.exe
deleted file mode 100644
index 582e82868..000000000
Binary files a/third_party/SimpleBLE/scripts/windows/tools/vswhere.exe and /dev/null differ
diff --git a/third_party/SimpleBLE/src/Adapter.cpp b/third_party/SimpleBLE/src/Adapter.cpp
index b702b416c..33de50701 100644
--- a/third_party/SimpleBLE/src/Adapter.cpp
+++ b/third_party/SimpleBLE/src/Adapter.cpp
@@ -3,8 +3,6 @@
 #include "AdapterBase.h"
 #include "AdapterBuilder.h"
 
-#include <iostream>
-
 using namespace SimpleBLE;
 
 std::vector<Adapter> Adapter::get_adapters() {
diff --git a/third_party/SimpleBLE/src/AdapterBuilder.h b/third_party/SimpleBLE/src/AdapterBuilder.h
index f11597674..f5f7eba3e 100644
--- a/third_party/SimpleBLE/src/AdapterBuilder.h
+++ b/third_party/SimpleBLE/src/AdapterBuilder.h
@@ -16,6 +16,7 @@ namespace SimpleBLE {
 class AdapterBuilder : public Adapter {
   public:
     AdapterBuilder(std::shared_ptr<AdapterBase> internal);
+    virtual ~AdapterBuilder() = default;
 };
 
-}  // namespace SimpleBLE
\ No newline at end of file
+}  // namespace SimpleBLE
diff --git a/third_party/SimpleBLE/src/Peripheral.cpp b/third_party/SimpleBLE/src/Peripheral.cpp
index 44a18c05f..6c94260ce 100644
--- a/third_party/SimpleBLE/src/Peripheral.cpp
+++ b/third_party/SimpleBLE/src/Peripheral.cpp
@@ -1,7 +1,6 @@
 #include <simpleble/Peripheral.h>
 
 #include <simpleble/Exceptions.h>
-#include <iostream>
 #include "PeripheralBase.h"
 
 using namespace SimpleBLE;
diff --git a/third_party/SimpleBLE/src/PeripheralBuilder.h b/third_party/SimpleBLE/src/PeripheralBuilder.h
index 189efd0a3..1ca768477 100644
--- a/third_party/SimpleBLE/src/PeripheralBuilder.h
+++ b/third_party/SimpleBLE/src/PeripheralBuilder.h
@@ -16,6 +16,7 @@ namespace SimpleBLE {
 class PeripheralBuilder : public Peripheral {
   public:
     PeripheralBuilder(std::shared_ptr<PeripheralBase> internal);
+    virtual ~PeripheralBuilder() = default;
 };
 
-}  // namespace SimpleBLE
\ No newline at end of file
+}  // namespace SimpleBLE
diff --git a/third_party/SimpleBLE/src/linux/AdapterBase.cpp b/third_party/SimpleBLE/src/linux/AdapterBase.cpp
index 59b95e062..8c9524f9e 100644
--- a/third_party/SimpleBLE/src/linux/AdapterBase.cpp
+++ b/third_party/SimpleBLE/src/linux/AdapterBase.cpp
@@ -3,87 +3,66 @@
 #include "PeripheralBase.h"
 #include "PeripheralBuilder.h"
 
-#include <iostream>
-
 using namespace SimpleBLE;
 
 std::vector<std::shared_ptr<AdapterBase>> AdapterBase::get_adapters() {
     std::vector<std::shared_ptr<AdapterBase>> adapter_list;
-    auto internal_adapters = Bluez::get()->bluez_service.get_all_adapters();
+    auto internal_adapters = Bluez::get()->bluez.get_adapters();
     for (auto& adapter : internal_adapters) {
         adapter_list.push_back(std::make_shared<AdapterBase>(adapter));
     }
     return adapter_list;
 }
 
-AdapterBase::AdapterBase(std::shared_ptr<BluezAdapter> adapter) { adapter_ = adapter; }
+AdapterBase::AdapterBase(std::shared_ptr<SimpleBluez::Adapter> adapter) : adapter_(adapter) {}
 
-AdapterBase::~AdapterBase() {}
+AdapterBase::~AdapterBase() { adapter_->clear_on_device_updated(); }
 
-std::string AdapterBase::identifier() {
-    auto adapter = adapter_.lock();
-    if (adapter) {
-        return adapter->get_identifier();
-    } else {
-        throw Exception::InvalidReference();
-    }
-}
+std::string AdapterBase::identifier() { return adapter_->identifier(); }
 
-BluetoothAddress AdapterBase::address() {
-    auto adapter = adapter_.lock();
-    if (adapter) {
-        return adapter->Address();
-    } else {
-        throw Exception::InvalidReference();
-    }
-}
+BluetoothAddress AdapterBase::address() { return adapter_->address(); }
 
 void AdapterBase::scan_start() {
-    auto adapter = adapter_.lock();
-    if (adapter) {
-        adapter->discovery_filter_transport_set("le");
-        this->seen_devices_.clear();
-        adapter->OnDeviceUpdated = [&](std::shared_ptr<BluezDevice> device) {
-            PeripheralBuilder peripheral_builder(std::make_shared<PeripheralBase>(device));
-
-            if (this->seen_devices_.count(peripheral_builder.address()) == 0) {
-                this->seen_devices_.insert(std::make_pair<>(peripheral_builder.address(), peripheral_builder));
-                if (this->callback_on_scan_found_) {
-                    this->callback_on_scan_found_(peripheral_builder);
-                }
-            } else {
-                if (this->callback_on_scan_updated_) {
-                    this->callback_on_scan_updated_(peripheral_builder);
-                }
-            }
-        };
+    adapter_->discovery_filter(SimpleBluez::Adapter::DiscoveryFilter::LE);
 
-        // Start scanning and notify the user.
-        adapter->StartDiscovery();
-        if (callback_on_scan_start_) {
-            callback_on_scan_start_();
+    seen_devices_.clear();
+    adapter_->set_on_device_updated([this](std::shared_ptr<SimpleBluez::Device> device) {
+        if (!this->is_scanning_) {
+            return;
         }
-    } else {
-        throw Exception::InvalidReference();
+
+        PeripheralBuilder peripheral_builder(std::make_shared<PeripheralBase>(device));
+
+        if (this->seen_devices_.count(peripheral_builder.address()) == 0) {
+            this->seen_devices_.insert(std::make_pair<>(peripheral_builder.address(), peripheral_builder));
+            if (this->callback_on_scan_found_) {
+                this->callback_on_scan_found_(peripheral_builder);
+            }
+        } else {
+            if (this->callback_on_scan_updated_) {
+                this->callback_on_scan_updated_(peripheral_builder);
+            }
+        }
+    });
+
+    // Start scanning and notify the user.
+    adapter_->discovery_start();
+    if (callback_on_scan_start_) {
+        callback_on_scan_start_();
     }
+    is_scanning_ = true;
 }
 
 void AdapterBase::scan_stop() {
-    auto adapter = adapter_.lock();
-    if (adapter) {
-        adapter->StopDiscovery();
-        // Due to the fact that Bluez takes some time to process the command
-        // and for SimpleDBus to flush the queue, wait until the driver
-        // has acknowledged that it is no longer discovering.
-        while (adapter->Property_Discovering()) {
-            std::this_thread::sleep_for(std::chrono::milliseconds(10));
-        }
-        if (callback_on_scan_stop_) {
-            callback_on_scan_stop_();
-        }
-    } else {
-        throw Exception::InvalidReference();
+    adapter_->discovery_stop();
+    is_scanning_ = false;
+    if (callback_on_scan_stop_) {
+        callback_on_scan_stop_();
     }
+
+    // Important: Bluez might continue scanning if another process is also requesting
+    // scanning from the adapter. The use of the is_scanning_ flag is to prevent
+    // any scan updates to reach the user when not expected.
 }
 
 void AdapterBase::scan_for(int timeout_ms) {
@@ -92,14 +71,7 @@ void AdapterBase::scan_for(int timeout_ms) {
     scan_stop();
 }
 
-bool AdapterBase::scan_is_active() {
-    auto adapter = adapter_.lock();
-    if (adapter) {
-        return adapter->Property_Discovering();
-    } else {
-        throw Exception::InvalidReference();
-    }
-}
+bool AdapterBase::scan_is_active() { return is_scanning_ && adapter_->discovering(); }
 
 std::vector<Peripheral> AdapterBase::scan_get_results() {
     std::vector<Peripheral> peripherals;
diff --git a/third_party/SimpleBLE/src/linux/AdapterBase.h b/third_party/SimpleBLE/src/linux/AdapterBase.h
index 974c7f22c..67fd20632 100644
--- a/third_party/SimpleBLE/src/linux/AdapterBase.h
+++ b/third_party/SimpleBLE/src/linux/AdapterBase.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <atomic>
 #include <functional>
 #include <memory>
 #include <set>
@@ -10,14 +11,14 @@
 #include <simpleble/Peripheral.h>
 #include <simpleble/Types.h>
 
-#include <BluezAdapter.h>
+#include <simplebluez/Adapter.h>
 
 namespace SimpleBLE {
 
 class AdapterBase {
   public:
-    AdapterBase(std::shared_ptr<BluezAdapter> adapter);
-    ~AdapterBase();
+    AdapterBase(std::shared_ptr<SimpleBluez::Adapter> adapter);
+    virtual ~AdapterBase();
 
     std::string identifier();
     BluetoothAddress address();
@@ -36,8 +37,9 @@ class AdapterBase {
     static std::vector<std::shared_ptr<AdapterBase>> get_adapters();
 
   private:
-    std::weak_ptr<BluezAdapter> adapter_;
+    std::shared_ptr<SimpleBluez::Adapter> adapter_;
 
+    std::atomic_bool is_scanning_;
     std::map<BluetoothAddress, Peripheral> seen_devices_;
 
     std::function<void()> callback_on_scan_start_;
diff --git a/third_party/SimpleBLE/src/linux/Bluez.cpp b/third_party/SimpleBLE/src/linux/Bluez.cpp
index b3b600185..b6a963c69 100644
--- a/third_party/SimpleBLE/src/linux/Bluez.cpp
+++ b/third_party/SimpleBLE/src/linux/Bluez.cpp
@@ -11,7 +11,7 @@ Bluez* Bluez::get() {
 }
 
 Bluez::Bluez() {
-    bluez_service.init();
+    bluez.init();
     async_thread_active = true;
     async_thread = new std::thread(&Bluez::async_thread_function, this);
 }
@@ -27,7 +27,7 @@ Bluez::~Bluez() {
 
 void Bluez::async_thread_function() {
     while (async_thread_active) {
-        bluez_service.run_async();
+        bluez.run_async();
         std::this_thread::sleep_for(std::chrono::microseconds(100));
     }
 }
diff --git a/third_party/SimpleBLE/src/linux/Bluez.h b/third_party/SimpleBLE/src/linux/Bluez.h
index 381c9496c..fd0a084d9 100644
--- a/third_party/SimpleBLE/src/linux/Bluez.h
+++ b/third_party/SimpleBLE/src/linux/Bluez.h
@@ -1,8 +1,8 @@
 #pragma once
 
+#include <simplebluez/Bluez.h>
 #include <atomic>
 #include <thread>
-#include "BluezService.h"
 
 namespace SimpleBLE {
 
@@ -10,7 +10,7 @@ class Bluez {
   public:
     static Bluez* get();
 
-    BluezService bluez_service;
+    SimpleBluez::Bluez bluez;
 
   private:
     Bluez();
diff --git a/third_party/SimpleBLE/src/linux/PeripheralBase.cpp b/third_party/SimpleBLE/src/linux/PeripheralBase.cpp
index ee63df5b7..d4ac34ac6 100644
--- a/third_party/SimpleBLE/src/linux/PeripheralBase.cpp
+++ b/third_party/SimpleBLE/src/linux/PeripheralBase.cpp
@@ -1,29 +1,38 @@
 #include "PeripheralBase.h"
 
 #include <simpleble/Exceptions.h>
-#include "Bluez.h"
+#include <simplebluez/Exceptions.h>
 
-#include <iostream>
+#include "Bluez.h"
 
 using namespace SimpleBLE;
 using namespace std::chrono_literals;
 
-PeripheralBase::PeripheralBase(std::shared_ptr<BluezDevice> device) {
-    device_ = device;
+PeripheralBase::PeripheralBase(std::shared_ptr<SimpleBluez::Device> device) : device_(device) {}
 
-    // NOTE: As Bluez will delete the object if it was advertising as non-connectable,
-    // we need to keep a copy of some of the properties around.
-    identifier_ = device->get_name();
-    address_ = device->get_address();
+PeripheralBase::~PeripheralBase() {
+    // TODO: A more extensive cleanup process is probably needed.
+    device_->clear_on_disconnected();
+    device_->clear_on_services_resolved();
 }
 
-PeripheralBase::~PeripheralBase() {}
-
-std::string PeripheralBase::identifier() { return identifier_; }
+std::string PeripheralBase::identifier() { return device_->name(); }
 
-BluetoothAddress PeripheralBase::address() { return address_; }
+BluetoothAddress PeripheralBase::address() { return device_->address(); }
 
 void PeripheralBase::connect() {
+    // Set the OnDisconnected callback
+    device_->set_on_disconnected([this]() {
+        this->disconnection_cv_.notify_all();
+
+        if (this->callback_on_disconnected_) {
+            this->callback_on_disconnected_();
+        }
+    });
+
+    // Set the OnServicesResolved callback
+    device_->set_on_services_resolved([this]() { this->connection_cv_.notify_all(); });
+
     // Attempt to connect to the device.
     for (size_t i = 0; i < 5; i++) {
         if (_attempt_connect()) {
@@ -50,23 +59,21 @@ void PeripheralBase::disconnect() {
 }
 
 bool PeripheralBase::is_connected() {
-    auto device = _get_device();
-
     // NOTE: For Bluez, a device being connected means that it's both
     // connected and services have been resolved.
-    return device->Property_Connected() && device->Property_ServicesResolved();
+    return device_->connected() && device_->services_resolved();
 }
 
-bool PeripheralBase::is_connectable() { return identifier_ != ""; }
+bool PeripheralBase::is_connectable() { return device_->name() != ""; }
 
 std::vector<BluetoothService> PeripheralBase::services() {
-    auto device = _get_device();
-
     std::vector<BluetoothService> service_list;
-    for (auto service_uuid : device->get_service_list()) {
+    for (auto bluez_service : device_->services()) {
         BluetoothService service;
-        service.uuid = service_uuid;
-        service.characteristics = device->get_characteristic_list(service_uuid);
+        service.uuid = bluez_service->uuid();
+        for (auto bluez_characteristic : bluez_service->characteristics()) {
+            service.characteristics.push_back(bluez_characteristic->uuid());
+        }
         service_list.push_back(service);
     }
 
@@ -74,10 +81,8 @@ std::vector<BluetoothService> PeripheralBase::services() {
 }
 
 std::map<uint16_t, ByteArray> PeripheralBase::manufacturer_data() {
-    auto device = _get_device();
-
     std::map<uint16_t, ByteArray> manufacturer_data;
-    for (auto& [manufacturer_id, value_array] : device->get_manufacturer_data()) {
+    for (auto& [manufacturer_id, value_array] : device_->manufacturer_data()) {
         manufacturer_data[manufacturer_id] = ByteArray((const char*)value_array.data(), value_array.size());
     }
 
@@ -86,22 +91,17 @@ std::map<uint16_t, ByteArray> PeripheralBase::manufacturer_data() {
 
 ByteArray PeripheralBase::read(BluetoothUUID service, BluetoothUUID characteristic) {
     // TODO: Check if the characteristic is readable.
-    auto characteristic_object = _get_characteristic(service, characteristic);
-
-    std::vector<uint8_t> value = characteristic_object->Property_Value();
-    return ByteArray((const char*)value.data(), value.size());
+    return _get_characteristic(service, characteristic)->value();
 }
 
 void PeripheralBase::write_request(BluetoothUUID service, BluetoothUUID characteristic, ByteArray data) {
     // TODO: Check if the characteristic is writable.
-    auto characteristic_object = _get_characteristic(service, characteristic);
-    characteristic_object->write_request((const uint8_t*)data.c_str(), data.size());
+    _get_characteristic(service, characteristic)->write_request(data);
 }
 
 void PeripheralBase::write_command(BluetoothUUID service, BluetoothUUID characteristic, ByteArray data) {
     // TODO: Check if the characteristic is writable.
-    auto characteristic_object = _get_characteristic(service, characteristic);
-    characteristic_object->write_command((const uint8_t*)data.c_str(), data.size());
+    _get_characteristic(service, characteristic)->write_command(data);
 }
 
 void PeripheralBase::notify(BluetoothUUID service, BluetoothUUID characteristic,
@@ -109,32 +109,24 @@ void PeripheralBase::notify(BluetoothUUID service, BluetoothUUID characteristic,
     // TODO: What to do if the characteristic is already being notified?
     // TODO: Check if the property can be notified.
     auto characteristic_object = _get_characteristic(service, characteristic);
-    characteristic_object->ValueChanged = [=](std::vector<uint8_t> value) {
-        callback(ByteArray((const char*)value.data(), value.size()));
-    };
-    characteristic_object->Action_StartNotify();
+    characteristic_object->set_on_value_changed([callback](SimpleBluez::ByteArray new_value) { callback(new_value); });
+    characteristic_object->start_notify();
 }
 
 void PeripheralBase::indicate(BluetoothUUID service, BluetoothUUID characteristic,
                               std::function<void(ByteArray payload)> callback) {
-    // TODO: What to do if the characteristic is already being indicated?
-    // TODO: Check if the property can be indicated.
-    auto characteristic_object = _get_characteristic(service, characteristic);
-    characteristic_object->ValueChanged = [=](std::vector<uint8_t> value) {
-        callback(ByteArray((const char*)value.data(), value.size()));
-    };
-    characteristic_object->Action_StartNotify();
+    notify(service, characteristic, callback);
 }
 
 void PeripheralBase::unsubscribe(BluetoothUUID service, BluetoothUUID characteristic) {
     // TODO: What to do if the characteristic is not being notified?
     auto characteristic_object = _get_characteristic(service, characteristic);
-    characteristic_object->Action_StopNotify();
+    characteristic_object->stop_notify();
 
     // Wait for the characteristic to stop notifying.
     // TODO: Upgrade SimpleDBus to provide a way to wait for this signal.
     auto timeout = std::chrono::system_clock::now() + 5s;
-    while (characteristic_object->Property_Notifying() && std::chrono::system_clock::now() < timeout) {
+    while (characteristic_object->notifying() && std::chrono::system_clock::now() < timeout) {
         std::this_thread::sleep_for(50ms);
     }
 }
@@ -150,33 +142,20 @@ void PeripheralBase::set_callback_on_disconnected(std::function<void()> on_disco
 // Private methods
 
 bool PeripheralBase::_attempt_connect() {
-    auto device = _get_device();
-
-    // Set the OnServiceDiscovered callback, which should properly indicate
-    // if the connection has been established.
-    device->OnServicesResolved = [=]() { this->connection_cv_.notify_all(); };
-
-    // Set the OnDisconnected callback
-    device->OnDisconnected = [this]() {
-        this->disconnection_cv_.notify_all();
-
-        if (this->callback_on_disconnected_) {
-            this->callback_on_disconnected_();
-        }
-    };
-
-    device->Action_Connect();
+    try {
+        device_->connect();
+    } catch (SimpleDBus::Exception::SendFailed& e) {
+        return false;
+    }
 
     // Wait for the connection to be confirmed.
     // The condition variable will return false if the connection was not established.
     std::unique_lock<std::mutex> lock(connection_mutex_);
-    return connection_cv_.wait_for(lock, 1s, [this]() { return is_connected(); });
+    return connection_cv_.wait_for(lock, 2s, [this]() { return is_connected(); });
 }
 
 bool PeripheralBase::_attempt_disconnect() {
-    auto device = _get_device();
-
-    device->Action_Disconnect();
+    device_->disconnect();
 
     // Wait for the disconnection to be confirmed.
     // The condition variable will return false if the connection is still active.
@@ -184,29 +163,13 @@ bool PeripheralBase::_attempt_disconnect() {
     return disconnection_cv_.wait_for(lock, 1s, [this]() { return !is_connected(); });
 }
 
-std::shared_ptr<BluezDevice> PeripheralBase::_get_device() {
-    std::shared_ptr<BluezDevice> device = device_.lock();
-
-    if (!device) {
-        throw Exception::InvalidReference();
-    }
-
-    return device;
-}
-
-std::shared_ptr<BluezGattCharacteristic> PeripheralBase::_get_characteristic(BluetoothUUID service_uuid,
-                                                                             BluetoothUUID characteristic_uuid) {
-    auto device = _get_device();
-
-    auto service = device->get_service(service_uuid);
-    if (!service) {
+std::shared_ptr<SimpleBluez::Characteristic> PeripheralBase::_get_characteristic(BluetoothUUID service_uuid,
+                                                                                 BluetoothUUID characteristic_uuid) {
+    try {
+        return device_->get_characteristic(service_uuid, characteristic_uuid);
+    } catch (SimpleBluez::Exception::ServiceNotFoundException& e) {
         throw Exception::ServiceNotFound(service_uuid);
-    }
-
-    auto characteristic = service->get_characteristic(characteristic_uuid);
-    if (!characteristic) {
+    } catch (SimpleBluez::Exception::CharacteristicNotFoundException& e) {
         throw Exception::CharacteristicNotFound(characteristic_uuid);
     }
-
-    return characteristic;
 }
diff --git a/third_party/SimpleBLE/src/linux/PeripheralBase.h b/third_party/SimpleBLE/src/linux/PeripheralBase.h
index e1c73016e..35e52f58d 100644
--- a/third_party/SimpleBLE/src/linux/PeripheralBase.h
+++ b/third_party/SimpleBLE/src/linux/PeripheralBase.h
@@ -5,7 +5,8 @@
 #include <simpleble/Exceptions.h>
 #include <simpleble/Types.h>
 
-#include <BluezDevice.h>
+#include <simplebluez/Characteristic.h>
+#include <simplebluez/Device.h>
 
 #include <condition_variable>
 
@@ -13,8 +14,8 @@ namespace SimpleBLE {
 
 class PeripheralBase {
   public:
-    PeripheralBase(std::shared_ptr<BluezDevice> device);
-    ~PeripheralBase();
+    PeripheralBase(std::shared_ptr<SimpleBluez::Device> device);
+    virtual ~PeripheralBase();
 
     std::string identifier();
     BluetoothAddress address();
@@ -38,10 +39,7 @@ class PeripheralBase {
     void set_callback_on_disconnected(std::function<void()> on_disconnected);
 
   private:
-    std::weak_ptr<BluezDevice> device_;
-
-    std::string identifier_;
-    BluetoothAddress address_;
+    std::shared_ptr<SimpleBluez::Device> device_;
 
     std::condition_variable connection_cv_;
     std::mutex connection_mutex_;
@@ -55,10 +53,8 @@ class PeripheralBase {
     bool _attempt_connect();
     bool _attempt_disconnect();
 
-    std::shared_ptr<BluezDevice> _get_device();
-
-    std::shared_ptr<BluezGattCharacteristic> _get_characteristic(BluetoothUUID service_uuid,
-                                                                 BluetoothUUID characteristic_uuid);
+    std::shared_ptr<SimpleBluez::Characteristic> _get_characteristic(BluetoothUUID service_uuid,
+                                                                     BluetoothUUID characteristic_uuid);
 };
 
 }  // namespace SimpleBLE
diff --git a/third_party/SimpleBLE/src/windows/AdapterBase.cpp b/third_party/SimpleBLE/src/windows/AdapterBase.cpp
index 0a3eae314..f369279e0 100644
--- a/third_party/SimpleBLE/src/windows/AdapterBase.cpp
+++ b/third_party/SimpleBLE/src/windows/AdapterBase.cpp
@@ -26,28 +26,42 @@ AdapterBase::AdapterBase(std::string device_id)
 
     // Configure the scanner object
     scanner_ = Advertisement::BluetoothLEAdvertisementWatcher();
-    scanner_.Stopped([=](const auto& w, const Advertisement::BluetoothLEAdvertisementWatcherStoppedEventArgs args) {
-        this->_scan_stopped_callback();
-    });
-    scanner_.Received([&](const auto& w, const Advertisement::BluetoothLEAdvertisementReceivedEventArgs args) {
-        advertising_data_t data;
-        data.mac_address = _mac_address_to_str(args.BluetoothAddress());
-        data.identifier = winrt::to_string(args.Advertisement().LocalName());
-        data.connectable = args.IsConnectable();
-
-        // Parse manufacturer data
-        auto manufacturer_data = args.Advertisement().ManufacturerData();
-        for (auto& item : manufacturer_data) {
-            uint16_t company_id = item.CompanyId();
-            ByteArray manufacturer_data_buffer = ibuffer_to_bytearray(item.Data());
-            data.manufacturer_data[company_id] = manufacturer_data_buffer;
-        }
 
-        this->_scan_received_callback(data);
-    });
+    scanner_stopped_token_ = scanner_.Stopped(
+        [this](const auto& w, const Advertisement::BluetoothLEAdvertisementWatcherStoppedEventArgs args) {
+            this->_scan_stopped_callback();
+        });
+
+    scanner_received_token_ = scanner_.Received(
+        [this](const auto& w, const Advertisement::BluetoothLEAdvertisementReceivedEventArgs args) {
+            advertising_data_t data;
+            data.mac_address = _mac_address_to_str(args.BluetoothAddress());
+            data.identifier = winrt::to_string(args.Advertisement().LocalName());
+            data.connectable = args.IsConnectable();
+
+            // Parse manufacturer data
+            auto manufacturer_data = args.Advertisement().ManufacturerData();
+            for (auto& item : manufacturer_data) {
+                uint16_t company_id = item.CompanyId();
+                ByteArray manufacturer_data_buffer = ibuffer_to_bytearray(item.Data());
+                data.manufacturer_data[company_id] = manufacturer_data_buffer;
+            }
+
+            this->_scan_received_callback(data);
+        });
 }
 
-AdapterBase::~AdapterBase() {}
+AdapterBase::~AdapterBase() {
+    // TODO: What happens if the adapter is still scanning?
+
+    if (scanner_received_token_) {
+        scanner_.Received(scanner_received_token_);
+    }
+
+    if (scanner_stopped_token_) {
+        scanner_.Stopped(scanner_stopped_token_);
+    }
+}
 
 std::vector<std::shared_ptr<AdapterBase>> AdapterBase::get_adapters() {
     initialize_winrt();
diff --git a/third_party/SimpleBLE/src/windows/AdapterBase.h b/third_party/SimpleBLE/src/windows/AdapterBase.h
index 7f489a0ed..4c977d29a 100644
--- a/third_party/SimpleBLE/src/windows/AdapterBase.h
+++ b/third_party/SimpleBLE/src/windows/AdapterBase.h
@@ -25,7 +25,7 @@ namespace SimpleBLE {
 class AdapterBase {
   public:
     AdapterBase(std::string device_id);
-    ~AdapterBase();
+    virtual ~AdapterBase();
 
     std::string identifier();
     BluetoothAddress address();
@@ -46,7 +46,10 @@ class AdapterBase {
   private:
     BluetoothAdapter adapter_;
     std::string identifier_;
+
     struct Advertisement::BluetoothLEAdvertisementWatcher scanner_;
+    winrt::event_token scanner_received_token_;
+    winrt::event_token scanner_stopped_token_;
 
     std::atomic_bool scan_is_active_{false};
     std::condition_variable scan_stop_cv_;
@@ -62,4 +65,4 @@ class AdapterBase {
     std::function<void(Peripheral)> callback_on_scan_found_;
 };
 
-}  // namespace SimpleBLE
\ No newline at end of file
+}  // namespace SimpleBLE
diff --git a/third_party/SimpleBLE/src/windows/PeripheralBase.cpp b/third_party/SimpleBLE/src/windows/PeripheralBase.cpp
index ad3337475..11e193c0c 100644
--- a/third_party/SimpleBLE/src/windows/PeripheralBase.cpp
+++ b/third_party/SimpleBLE/src/windows/PeripheralBase.cpp
@@ -18,13 +18,17 @@ using namespace std::chrono_literals;
 PeripheralBase::PeripheralBase(advertising_data_t advertising_data) {
     identifier_ = advertising_data.identifier;
     address_ = advertising_data.mac_address;
-    device_ = async_get(
-        BluetoothLEDevice::FromBluetoothAddressAsync(_str_to_mac_address(advertising_data.mac_address)));
     manufacturer_data_ = advertising_data.manufacturer_data;
     connectable_ = advertising_data.connectable;
+    device_ = async_get(
+        BluetoothLEDevice::FromBluetoothAddressAsync(_str_to_mac_address(advertising_data.mac_address)));
 }
 
-PeripheralBase::~PeripheralBase() {}
+PeripheralBase::~PeripheralBase() {
+    if (connection_status_changed_token_) {
+        device_.ConnectionStatusChanged(connection_status_changed_token_);
+    }
+}
 
 std::string PeripheralBase::identifier() { return identifier_; }
 
@@ -39,16 +43,17 @@ void PeripheralBase::connect() {
     }
 
     if (is_connected()) {
-        device_.ConnectionStatusChanged([=](const BluetoothLEDevice device, const auto args) {
-            if (device.ConnectionStatus() == BluetoothConnectionStatus::Disconnected) {
-                this->disconnection_cv_.notify_all();
-                if (callback_on_disconnected_) {
-                    callback_on_disconnected_();
+        connection_status_changed_token_ = device_.ConnectionStatusChanged(
+            [this](const BluetoothLEDevice device, const auto args) {
+                if (device.ConnectionStatus() == BluetoothConnectionStatus::Disconnected) {
+                    this->disconnection_cv_.notify_all();
+                    if (this->callback_on_disconnected_) {
+                        this->callback_on_disconnected_();
+                    }
                 }
-            }
-        });
-        if (callback_on_connected_) {
-            callback_on_connected_();
+            });
+        if (this->callback_on_connected_) {
+            this->callback_on_connected_();
         }
     }
 }
diff --git a/third_party/SimpleBLE/src/windows/PeripheralBase.h b/third_party/SimpleBLE/src/windows/PeripheralBase.h
index 714780ba0..4ca0b842a 100644
--- a/third_party/SimpleBLE/src/windows/PeripheralBase.h
+++ b/third_party/SimpleBLE/src/windows/PeripheralBase.h
@@ -21,7 +21,7 @@ namespace SimpleBLE {
 class PeripheralBase {
   public:
     PeripheralBase(advertising_data_t advertising_data);
-    ~PeripheralBase();
+    virtual ~PeripheralBase();
 
     std::string identifier();
     BluetoothAddress address();
@@ -56,6 +56,7 @@ class PeripheralBase {
     std::string identifier_;
     BluetoothAddress address_;
     bool connectable_;
+    winrt::event_token connection_status_changed_token_;
 
     std::condition_variable disconnection_cv_;
     std::mutex disconnection_mutex_;
diff --git a/third_party/SimpleBLE/src/windows/Utils.cpp b/third_party/SimpleBLE/src/windows/Utils.cpp
index ecdb368eb..5096004ea 100644
--- a/third_party/SimpleBLE/src/windows/Utils.cpp
+++ b/third_party/SimpleBLE/src/windows/Utils.cpp
@@ -1,5 +1,7 @@
 #include "Utils.h"
 
+#include <ntverp.h>
+#include <sdkddkver.h>
 #include <iomanip>
 #include <iostream>
 #include <sstream>
@@ -11,12 +13,20 @@ namespace SimpleBLE {
 void initialize_winrt() {
     // Attempt to initialize the WinRT backend if not already set.
     int32_t cotype, qualifier;
+#ifdef NTDDI_WIN4
+    WINRT_IMPL_CoGetApartmentType(&cotype, &qualifier);
+#else
     WINRT_CoGetApartmentType(&cotype, &qualifier);
+#endif
 
     if (cotype == -1 /* APTTYPE_CURRENT */) {
         // TODO: Investigate if multi or single threaded initialization is needed.
         winrt::apartment_type const type = winrt::apartment_type::multi_threaded;
+#ifdef NTDDI_WIN4
+        winrt::hresult const result = WINRT_IMPL_CoInitializeEx(nullptr, static_cast<uint32_t>(type));
+#else
         winrt::hresult const result = WINRT_RoInitialize(static_cast<uint32_t>(type));
+#endif
     }
 }
 
diff --git a/third_party/SimpleBLE/src/windows/Utils.h b/third_party/SimpleBLE/src/windows/Utils.h
index 79553ed13..4ae0bb607 100644
--- a/third_party/SimpleBLE/src/windows/Utils.h
+++ b/third_party/SimpleBLE/src/windows/Utils.h
@@ -12,8 +12,9 @@
 using namespace winrt::Windows;
 using namespace winrt::Windows::Storage::Streams;
 
-namespace SimpleBLE {
+constexpr uint32_t TEN_SECONDS_IN_MSECS = 10000;
 
+namespace SimpleBLE {
 void initialize_winrt();
 
 std::string _mac_address_to_str(uint64_t mac_address);
@@ -28,7 +29,7 @@ IBuffer bytearray_to_ibuffer(const ByteArray& array);
 template <typename async_t>
 static auto async_get(async_t const& async) {
     if (async.Status() == Foundation::AsyncStatus::Started) {
-        wait_for_completed(async, 0xFFFFFFFF);  // INFINITE
+        wait_for_completed(async, TEN_SECONDS_IN_MSECS);
     }
     return async.GetResults();
 }
diff --git a/third_party/SimpleBLE/src_c/peripheral.cpp b/third_party/SimpleBLE/src_c/peripheral.cpp
index 89c8866f6..b67ded3d5 100644
--- a/third_party/SimpleBLE/src_c/peripheral.cpp
+++ b/third_party/SimpleBLE/src_c/peripheral.cpp
@@ -202,7 +202,8 @@ simpleble_err_t simpleble_peripheral_read(simpleble_peripheral_t handle, simpleb
 }
 
 simpleble_err_t simpleble_peripheral_write_request(simpleble_peripheral_t handle, simpleble_uuid_t service,
-                                                   simpleble_uuid_t characteristic, uint8_t* data, size_t data_length) {
+                                                   simpleble_uuid_t characteristic, const uint8_t* data,
+                                                   size_t data_length) {
     if (handle == nullptr || data == nullptr) {
         return SIMPLEBLE_FAILURE;
     }
@@ -217,7 +218,8 @@ simpleble_err_t simpleble_peripheral_write_request(simpleble_peripheral_t handle
 }
 
 simpleble_err_t simpleble_peripheral_write_command(simpleble_peripheral_t handle, simpleble_uuid_t service,
-                                                   simpleble_uuid_t characteristic, uint8_t* data, size_t data_length) {
+                                                   simpleble_uuid_t characteristic, const uint8_t* data,
+                                                   size_t data_length) {
     if (handle == nullptr || data == nullptr) {
         return SIMPLEBLE_FAILURE;
     }
@@ -233,7 +235,7 @@ simpleble_err_t simpleble_peripheral_write_command(simpleble_peripheral_t handle
 
 simpleble_err_t simpleble_peripheral_notify(
     simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic,
-    void (*callback)(simpleble_uuid_t, simpleble_uuid_t, uint8_t*, size_t, void*), void* userdata) {
+    void (*callback)(simpleble_uuid_t, simpleble_uuid_t, const uint8_t*, size_t, void*), void* userdata) {
     if (handle == nullptr || callback == nullptr) {
         return SIMPLEBLE_FAILURE;
     }
@@ -242,7 +244,7 @@ simpleble_err_t simpleble_peripheral_notify(
 
     bool success = peripheral->notify(SimpleBLE::BluetoothUUID(service.value),
                                       SimpleBLE::BluetoothUUID(characteristic.value), [=](SimpleBLE::ByteArray data) {
-                                          callback(service, characteristic, (uint8_t*)data.data(), data.size(),
+                                          callback(service, characteristic, (const uint8_t*)data.data(), data.size(),
                                                    userdata);
                                       });
 
@@ -251,7 +253,7 @@ simpleble_err_t simpleble_peripheral_notify(
 
 simpleble_err_t simpleble_peripheral_indicate(
     simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic,
-    void (*callback)(simpleble_uuid_t, simpleble_uuid_t, uint8_t*, size_t, void*), void* userdata) {
+    void (*callback)(simpleble_uuid_t, simpleble_uuid_t, const uint8_t*, size_t, void*), void* userdata) {
     if (handle == nullptr || callback == nullptr) {
         return SIMPLEBLE_FAILURE;
     }
@@ -260,7 +262,7 @@ simpleble_err_t simpleble_peripheral_indicate(
 
     bool success = peripheral->indicate(SimpleBLE::BluetoothUUID(service.value),
                                         SimpleBLE::BluetoothUUID(characteristic.value), [=](SimpleBLE::ByteArray data) {
-                                            callback(service, characteristic, (uint8_t*)data.data(), data.size(),
+                                            callback(service, characteristic, (const uint8_t*)data.data(), data.size(),
                                                      userdata);
                                         });
 
diff --git a/third_party/SimpleBluez/.github/workflows/ci_build.yml b/third_party/SimpleBluez/.github/workflows/ci_build.yml
new file mode 100644
index 000000000..93683b22f
--- /dev/null
+++ b/third_party/SimpleBluez/.github/workflows/ci_build.yml
@@ -0,0 +1,39 @@
+name: CI Build
+
+on: [push, pull_request]
+
+jobs:
+  build-linux:
+    runs-on: ${{ matrix.os }}
+
+    strategy:
+      fail-fast: false
+      max-parallel: 4
+      matrix:
+        os: [ubuntu-20.04]
+
+    steps:
+      - name: Clone Repository
+        uses: actions/checkout@v2
+      - name: Install Dependencies
+        run: |
+          sudo -H apt-get update -y
+          sudo -H apt-get install -y libdbus-1-dev
+        env:
+          DEBIAN_FRONTEND: noninteractive
+      - name: Setup Cmake
+        uses: jwlawson/actions-setup-cmake@v1.4
+        with:
+          cmake-version: "3.16.x"
+      - name: Compile Lib for Ubuntu
+        run: |
+          mkdir $GITHUB_WORKSPACE/build
+          cd $GITHUB_WORKSPACE/build
+          cmake -DCMAKE_BUILD_TYPE=Release ..
+          make -j2
+      - name: Compile Examples for Ubuntu
+        run: |
+          mkdir $GITHUB_WORKSPACE/build_examples
+          cd $GITHUB_WORKSPACE/build_examples
+          cmake -DCMAKE_BUILD_TYPE=Release ../examples
+          make -j2
diff --git a/third_party/SimpleBluez/.github/workflows/ci_lint.yml b/third_party/SimpleBluez/.github/workflows/ci_lint.yml
new file mode 100644
index 000000000..7133d593d
--- /dev/null
+++ b/third_party/SimpleBluez/.github/workflows/ci_lint.yml
@@ -0,0 +1,40 @@
+name: CI Lint
+
+on: [push, pull_request]
+
+jobs:
+  clang-format:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Run clang-format
+        uses: DoozyX/clang-format-lint-action@v0.11
+        with:
+          source: "src"
+          extensions: "h,cpp,c"
+          clangFormatVersion: 11
+          inplace: False
+
+  cppcheck:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Install CppCheck
+        run: |
+          sudo -H apt-get update -y
+          sudo -H apt-get install -y cppcheck
+        env:
+          DEBIAN_FRONTEND: noninteractive
+      - name: Run Cppcheck
+        run: cppcheck --std=c++17 --error-exitcode=1 --xml --xml-version=2 --force src 2>cppcheck_res.xml
+      - name: Generate Report
+        if: ${{ failure() }}
+        run: cppcheck-htmlreport --title=SimpleDBus --file=cppcheck_res.xml --report-dir=report
+      - name: Upload Report
+        if: ${{ failure() }}
+        uses: actions/upload-artifact@v1
+        with:
+          name: report
+          path: report
diff --git a/third_party/SimpleBluez/.gitignore b/third_party/SimpleBluez/.gitignore
index c471fa329..9f94e5dd9 100644
--- a/third_party/SimpleBluez/.gitignore
+++ b/third_party/SimpleBluez/.gitignore
@@ -1,5 +1,2 @@
-bin
-build
-.build
 .vscode
-__pycache__
\ No newline at end of file
+__pycache__
diff --git a/third_party/SimpleBluez/CHANGELOG.md b/third_party/SimpleBluez/CHANGELOG.md
index c6229ad57..66b478616 100644
--- a/third_party/SimpleBluez/CHANGELOG.md
+++ b/third_party/SimpleBluez/CHANGELOG.md
@@ -4,5 +4,25 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [0.1.1] - 2021-12-28
+
+### Added
+- Function to access currently cached value from characteristics.
+- `Notifying` property on Characteristic1.
+- Added OnDisconnected callback to Device1.
+- Added ServicesResolved callback to Device1.
+- Address and Thread sanitization options.
+
+### Fixed
+- Removed unnecessary <iostream> includes.
+- Made sure all classes have proper virtual destructors.
+
+### Changed
+- All proxy and interface manipulation is now done through helper functions.
+- Access to all interface properties is now thread-safe.
+
+## [0.1.0] - 2021-12-14
+- Reimplementation of the library based on the SimpleDBus v2.0.0-alpha.2 API.
+
 ## [0.0.1] - 2021-11-09
 - First working implementation post migration, following the SimpleDBus v2.0.0-alpha API.
diff --git a/third_party/SimpleBluez/CMakeLists.txt b/third_party/SimpleBluez/CMakeLists.txt
index 6be86bd43..104734f08 100644
--- a/third_party/SimpleBluez/CMakeLists.txt
+++ b/third_party/SimpleBluez/CMakeLists.txt
@@ -22,7 +22,9 @@ message("-- [INFO] Using local SimpleDBus")
 add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../SimpleDBus ${CMAKE_CURRENT_BINARY_DIR}/simpledbus)
 
 include_directories(${SIMPLEDBUS_INCLUDES})
+include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
 include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)
+include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/legacy)
 
 message("-- [INFO] Building SimpleBluez")
 file(GLOB_RECURSE SRC_simplebluez_FILES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
@@ -35,8 +37,17 @@ target_compile_options(simplebluez PRIVATE -std=c++17 -fPIC -Wfatal-errors -Wped
 
 target_link_libraries(simplebluez-static PUBLIC simpledbus-static)
 target_link_libraries(simplebluez PUBLIC simpledbus-static)
+target_link_libraries(simplebluez-static PRIVATE fmt::fmt-header-only)
+target_link_libraries(simplebluez PRIVATE fmt::fmt-header-only)
 
 # Export the variables needed by the parent project
 if(NOT ${STANDALONE})
-    set(SIMPLEBLUEZ_INCLUDES ${CMAKE_CURRENT_SOURCE_DIR}/src ${SIMPLEDBUS_INCLUDES} PARENT_SCOPE)
+    set(
+        SIMPLEBLUEZ_INCLUDES
+        ${CMAKE_CURRENT_SOURCE_DIR}/include
+        ${CMAKE_CURRENT_SOURCE_DIR}/src
+        ${CMAKE_CURRENT_SOURCE_DIR}/src/legacy
+        ${SIMPLEDBUS_INCLUDES}
+        PARENT_SCOPE
+    )
 endif()
diff --git a/third_party/SimpleBluez/README.md b/third_party/SimpleBluez/README.md
index 7bef82e81..96b069d25 100644
--- a/third_party/SimpleBluez/README.md
+++ b/third_party/SimpleBluez/README.md
@@ -36,6 +36,20 @@ cmake ../examples
 make -j
 ```
 
+### Build tests
+To build and run unit and integration tests, the following packages are required:
+`sudo apt install libgtest-dev libgmock-dev python3-dev`
+
+#### Address Sanitizer
+In order to run tests with Address Sanitizer, CMake needs to be called with
+the following option: `-DSIMPLEBLUEZ_SANITIZE=Address`. It is also important to
+set the environment variable `PYTHONMALLOC=malloc` to prevent Python's memory
+allocator from triggering false positives.
+
+#### Thread Sanitizer
+In order to run tests with Thread Sanitizer, CMake needs to be called with
+the following option: `-DSIMPLEBLUEZ_SANITIZE=Thread`.
+
 ## Security
 
 One key security feature of the library is it allows the user to specify
@@ -50,6 +64,12 @@ the `cmake` command:
 - `SIMPLEDBUS_GIT_TAG`
   Call CMake with `-DSIMPLEDBUS_GIT_TAG=<tag>` to override the default
   tag of the SimpleDBus repository.
+- `LIBFMT_GIT_REPOSITORY`
+  Call CMake with `-DLIBFMT_GIT_REPOSITORY=<path>` to override the
+  default location of the LibFmt repository.
+- `LIBFMT_GIT_TAG`
+  Call CMake with `-DLIBFMT_GIT_TAG=<tag>` to override the default
+  tag of the LibFmt repository.
 
 Alternatively, instead of specifying the repository and tag, the user can
 specify the path to a local version of the dependencies. This is done by
@@ -58,17 +78,15 @@ specifying the additional command line arguments to the `cmake` command:
 - `SIMPLEDBUS_LOCAL_PATH`
   Call CMake with `-DSIMPLEDBUS_LOCAL_PATH=<path>` to override the
   default location of the SimpleDBus repository.
+- `LIBFMT_LOCAL_PATH`
+  Call CMake with `-DLIBFMT_LOCAL_PATH=<path>` to override the
+  default location of the LibFmt repository.
 
 ## More information
-- You can find the official documentation for BlueZ in [this link](https://git.kernel.org/pub/scm/bluetooth/bluez.git/plain/doc/gatt-api.txt).
-
-## Known issues
-- The handling of all externally exposed components with `std::shared_ptr` is not particularly safe in case the underlying object gets deleted by BlueZ, as the end user still has the capability to send requests to that deleted object. This will be addressed in a future release, but it's important for users of SimpleDBus to keep in mind.
+- You can find the official documentation for Bluez in [this link](https://git.kernel.org/pub/scm/bluetooth/bluez.git/plain/doc/gatt-api.txt).
 
 ## Contributing
 If you want to help, here is what's needed:
-- The remaining functionality of the existing components.
-- Errors are currently not being caught and surfaced up.
 - More documentation.
 - More examples.
 - Logging
diff --git a/third_party/SimpleBluez/examples/CMakeLists.txt b/third_party/SimpleBluez/examples/CMakeLists.txt
index df713da34..0a7fab8dd 100644
--- a/third_party/SimpleBluez/examples/CMakeLists.txt
+++ b/third_party/SimpleBluez/examples/CMakeLists.txt
@@ -6,6 +6,9 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
 set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
 
+# Nice hack to automatically ignore the build directory
+file(WRITE ${CMAKE_BINARY_DIR}/.gitignore "*")
+
 set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_EXTENSIONS OFF)
 
@@ -14,6 +17,9 @@ set(CMAKE_CXX_EXTENSIONS OFF)
 add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/.. ${CMAKE_BINARY_DIR}/simplebluez)
 include_directories(${SIMPLEBLUEZ_INCLUDES})
 
-add_subdirectory(ble_nus)
 add_subdirectory(list_adapters)
-add_subdirectory(manuf_data)
+add_subdirectory(scan)
+add_subdirectory(connect)
+add_subdirectory(read)
+
+add_subdirectory(ble_nus)
diff --git a/third_party/SimpleBluez/examples/ble_nus/CMakeLists.txt b/third_party/SimpleBluez/examples/ble_nus/CMakeLists.txt
index b2f11543a..96314e412 100644
--- a/third_party/SimpleBluez/examples/ble_nus/CMakeLists.txt
+++ b/third_party/SimpleBluez/examples/ble_nus/CMakeLists.txt
@@ -3,5 +3,5 @@ cmake_minimum_required(VERSION 3.16.0)
 project(EXAMPLE_BLE_NUS)
 
 message("-- [INFO] Building Example")
-add_executable(example_ble_nus main.cpp)
-target_link_libraries(example_ble_nus simplebluez-static pthread)
+add_executable(example_ble_nus ble_nus.cpp)
+target_link_libraries(example_ble_nus simplebluez-static)
diff --git a/third_party/SimpleBluez/examples/ble_nus/ble_nus.cpp b/third_party/SimpleBluez/examples/ble_nus/ble_nus.cpp
new file mode 100644
index 000000000..56168e933
--- /dev/null
+++ b/third_party/SimpleBluez/examples/ble_nus/ble_nus.cpp
@@ -0,0 +1,136 @@
+#include <simplebluez/Bluez.h>
+#include <simplebluez/Exceptions.h>
+
+#include <atomic>
+#include <chrono>
+#include <cstdlib>
+#include <iomanip>
+#include <iostream>
+#include <thread>
+
+SimpleBluez::Bluez bluez;
+
+std::atomic_bool async_thread_active = true;
+void async_thread_function() {
+    while (async_thread_active) {
+        bluez.run_async();
+        std::this_thread::sleep_for(std::chrono::microseconds(10));
+    }
+}
+
+void millisecond_delay(int ms) {
+    for (int i = 0; i < ms; i++) {
+        std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    }
+}
+
+void print_byte_array(SimpleBluez::ByteArray& bytes) {
+    for (auto byte : bytes) {
+        std::cout << std::hex << std::setfill('0') << (uint32_t)((uint8_t)byte) << " ";
+        break;
+    }
+    std::cout << std::endl;
+}
+
+std::vector<std::shared_ptr<SimpleBluez::Device>> peripherals;
+
+int main(int argc, char* argv[]) {
+    int selection = -1;
+
+    bluez.init();
+    std::thread* async_thread = new std::thread(async_thread_function);
+
+    auto adapters = bluez.get_adapters();
+    std::cout << "Available adapters:" << std::endl;
+    for (int i = 0; i < adapters.size(); i++) {
+        std::cout << "[" << i << "] " << adapters[i]->identifier() << " [" << adapters[i]->address() << "]"
+                  << std::endl;
+    }
+
+    std::cout << "Please select an adapter to scan: ";
+    std::cin >> selection;
+    if (selection < 0 || selection >= adapters.size()) {
+        std::cout << "Invalid selection" << std::endl;
+        return 1;
+    }
+
+    auto adapter = adapters[selection];
+    std::cout << "Scanning " << adapter->identifier() << " [" << adapter->address() << "]" << std::endl;
+
+    adapter->discovery_filter(SimpleBluez::Adapter::DiscoveryFilter::LE);
+
+    adapter->set_on_device_updated([](std::shared_ptr<SimpleBluez::Device> device) {
+        if (device->name() == "") {
+            return;
+        }
+
+        if (std::find(peripherals.begin(), peripherals.end(), device) == peripherals.end()) {
+            std::cout << "Found device: " << device->name() << " [" << device->address() << "]" << std::endl;
+            peripherals.push_back(device);
+        }
+    });
+
+    adapter->discovery_start();
+    millisecond_delay(3000);
+    adapter->discovery_stop();
+
+    std::cout << "The following devices were found:" << std::endl;
+    for (int i = 0; i < peripherals.size(); i++) {
+        std::cout << "[" << i << "] " << peripherals[i]->name() << " [" << peripherals[i]->address() << "]"
+                  << std::endl;
+    }
+
+    std::cout << "Please select a device to connect to: ";
+    std::cin >> selection;
+
+    if (selection >= 0 && selection < peripherals.size()) {
+        auto peripheral = peripherals[selection];
+        std::cout << "Connecting to " << peripheral->name() << " [" << peripheral->address() << "]" << std::endl;
+
+        for (int attempt = 0; attempt < 3; attempt++) {
+            try {
+                peripheral->connect();
+            } catch (SimpleDBus::Exception::SendFailed& e) {
+                millisecond_delay(100);
+            }
+        }
+
+        if (!peripheral->connected() && !peripheral->services_resolved()) {
+            std::cout << "Failed to connect to " << peripheral->name() << " [" << peripheral->address() << "]"
+                      << std::endl;
+            return 1;
+        }
+
+        std::cout << "Successfully connected, testing NUS service." << std::endl;
+
+        auto characteristic_rx = peripheral->get_characteristic("6e400001-b5a3-f393-e0a9-e50e24dcca9e",
+                                                                "6e400002-b5a3-f393-e0a9-e50e24dcca9e");
+
+        characteristic_rx->write_command("Hello World");
+
+        auto characteristic_tx = peripheral->get_characteristic("6e400001-b5a3-f393-e0a9-e50e24dcca9e",
+                                                                "6e400003-b5a3-f393-e0a9-e50e24dcca9e");
+
+        characteristic_tx->set_on_value_changed([&](SimpleBluez::ByteArray new_value) { print_byte_array(new_value); });
+
+        characteristic_tx->start_notify();
+        millisecond_delay(3000);
+        characteristic_tx->stop_notify();
+        millisecond_delay(1000);
+
+        peripheral->disconnect();
+
+        // Sleep for an additional second before returning.
+        // If there are any unexpected events, this example will help debug them.
+        millisecond_delay(1000);
+    }
+
+    async_thread_active = false;
+    while (!async_thread->joinable()) {
+        millisecond_delay(10);
+    }
+    async_thread->join();
+    delete async_thread;
+
+    return 0;
+}
diff --git a/third_party/SimpleBluez/examples/ble_nus/main.cpp b/third_party/SimpleBluez/examples/ble_nus/main.cpp
deleted file mode 100644
index a10ff8fef..000000000
--- a/third_party/SimpleBluez/examples/ble_nus/main.cpp
+++ /dev/null
@@ -1,81 +0,0 @@
-#include "BluezService.h"
-
-#include <stdlib.h>
-#include <chrono>
-#include <iomanip>
-#include <iostream>
-#include <thread>
-
-BluezService bluez_service;
-
-volatile bool async_thread_active = true;
-void async_thread_function() {
-    while (async_thread_active) {
-        bluez_service.run_async();
-        std::this_thread::sleep_for(std::chrono::milliseconds(10));
-    }
-}
-
-void millisecond_delay(int ms) {
-    for (int i = 0; i < ms; i++) {
-        std::this_thread::sleep_for(std::chrono::milliseconds(1));
-    }
-}
-
-/**
- * This is a minimal example in which we exchange some data
- * with a device exposing the Nordic UART service.
- */
-int main(int argc, char* argv[]) {
-    std::string mac_address = argv[1];
-    bluez_service.init();
-
-    std::thread* async_thread = new std::thread(async_thread_function);
-
-    auto adapter = bluez_service.get_first_adapter();
-    if (adapter == nullptr) {
-        std::cout << "No adapter found!" << std::endl;
-        return -1;
-    }
-
-    adapter->discovery_filter_transport_set("le");
-    adapter->StartDiscovery();
-    millisecond_delay(5000);
-    adapter->StopDiscovery();
-
-    auto device = adapter->get_device(mac_address);
-    if (device != nullptr) {
-        device->Connect();
-
-        // In theory, we should wait for the OnConnect event.
-        millisecond_delay(1000);
-
-        auto characteristic_tx = device->get_characteristic("6e400001-b5a3-f393-e0a9-e50e24dcca9e",
-                                                            "6e400003-b5a3-f393-e0a9-e50e24dcca9e");
-        auto characteristic_rx = device->get_characteristic("6e400001-b5a3-f393-e0a9-e50e24dcca9e",
-                                                            "6e400002-b5a3-f393-e0a9-e50e24dcca9e");
-        if (characteristic_tx != nullptr) {
-            characteristic_tx->ValueChanged = [&](std::vector<uint8_t> new_value) { /* Data! */ };
-            characteristic_tx->StartNotify();
-        }
-
-        if (characteristic_rx != nullptr) {
-            std::string message = "Hello World";
-            characteristic_rx->write_command((uint8_t*)message.c_str(), message.length());
-        }
-
-        millisecond_delay(2000);
-        characteristic_tx->StopNotify();
-        device->Disconnect();
-        millisecond_delay(1000);
-    }
-
-    async_thread_active = false;
-    while (!async_thread->joinable()) {
-        millisecond_delay(10);
-    }
-    async_thread->join();
-    delete async_thread;
-
-    return 0;
-}
diff --git a/third_party/SimpleBluez/examples/connect/CMakeLists.txt b/third_party/SimpleBluez/examples/connect/CMakeLists.txt
new file mode 100644
index 000000000..938cdd7ca
--- /dev/null
+++ b/third_party/SimpleBluez/examples/connect/CMakeLists.txt
@@ -0,0 +1,7 @@
+cmake_minimum_required(VERSION 3.16.0)
+
+project(EXAMPLE_CONNECT)
+
+message("-- [INFO] Building Example")
+add_executable(example_connect connect.cpp)
+target_link_libraries(example_connect simplebluez-static)
diff --git a/third_party/SimpleBluez/examples/connect/connect.cpp b/third_party/SimpleBluez/examples/connect/connect.cpp
new file mode 100644
index 000000000..6e04b84a7
--- /dev/null
+++ b/third_party/SimpleBluez/examples/connect/connect.cpp
@@ -0,0 +1,114 @@
+#include <simplebluez/Bluez.h>
+#include <simplebluez/Exceptions.h>
+
+#include <cstdlib>
+#include <atomic>
+#include <chrono>
+#include <iomanip>
+#include <iostream>
+#include <thread>
+
+SimpleBluez::Bluez bluez;
+
+std::atomic_bool async_thread_active = true;
+void async_thread_function() {
+    while (async_thread_active) {
+        bluez.run_async();
+        std::this_thread::sleep_for(std::chrono::microseconds(100));
+    }
+}
+
+void millisecond_delay(int ms) {
+    for (int i = 0; i < ms; i++) {
+        std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    }
+}
+
+std::vector<std::shared_ptr<SimpleBluez::Device>> peripherals;
+
+int main(int argc, char* argv[]) {
+    int selection = -1;
+
+    bluez.init();
+    std::thread* async_thread = new std::thread(async_thread_function);
+
+    auto adapters = bluez.get_adapters();
+    std::cout << "Available adapters:" << std::endl;
+    for (int i = 0; i < adapters.size(); i++) {
+        std::cout << "[" << i << "] " << adapters[i]->identifier() << " [" << adapters[i]->address() << "]"
+                  << std::endl;
+    }
+
+    std::cout << "Please select an adapter to scan: ";
+    std::cin >> selection;
+    if (selection < 0 || selection >= adapters.size()) {
+        std::cout << "Invalid selection" << std::endl;
+        return 1;
+    }
+
+    auto adapter = adapters[selection];
+    std::cout << "Scanning " << adapter->identifier() << " [" << adapter->address() << "]" << std::endl;
+
+    adapter->discovery_filter(SimpleBluez::Adapter::DiscoveryFilter::LE);
+
+    adapter->set_on_device_updated([](std::shared_ptr<SimpleBluez::Device> device) {
+        if (std::find(peripherals.begin(), peripherals.end(), device) == peripherals.end()) {
+            std::cout << "Found device: " << device->name() << " [" << device->address() << "]" << std::endl;
+            peripherals.push_back(device);
+        }
+    });
+
+    adapter->discovery_start();
+    millisecond_delay(3000);
+    adapter->discovery_stop();
+
+    std::cout << "The following devices were found:" << std::endl;
+    for (int i = 0; i < peripherals.size(); i++) {
+        std::cout << "[" << i << "] " << peripherals[i]->name() << " [" << peripherals[i]->address() << "]"
+                  << std::endl;
+    }
+
+    std::cout << "Please select a device to connect to: ";
+    std::cin >> selection;
+
+    if (selection >= 0 && selection < peripherals.size()) {
+        auto peripheral = peripherals[selection];
+        std::cout << "Connecting to " << peripheral->name() << " [" << peripheral->address() << "]" << std::endl;
+
+        for (int attempt = 0; attempt < 3; attempt++) {
+            try {
+                peripheral->connect();
+            } catch (SimpleDBus::Exception::SendFailed& e) {
+                millisecond_delay(100);
+            }
+        }
+
+        if (!peripheral->connected() && !peripheral->services_resolved()) {
+            std::cout << "Failed to connect to " << peripheral->name() << " [" << peripheral->address() << "]"
+                      << std::endl;
+            return 1;
+        }
+
+        std::cout << "Successfully connected, listing services." << std::endl;
+        for (auto service : peripheral->services()) {
+            std::cout << "Service: " << service->uuid() << std::endl;
+            for (auto characteristic : service->characteristics()) {
+                std::cout << "  Characteristic: " << characteristic->uuid() << std::endl;
+            }
+        }
+        peripheral->disconnect();
+
+        // Sleep for an additional second before returning.
+        // If there are any unexpected events, this example will help debug them.
+        millisecond_delay(1000);
+    }
+
+    async_thread_active = false;
+    while (!async_thread->joinable()) {
+        millisecond_delay(10);
+    }
+    async_thread->join();
+    delete async_thread;
+
+    return 0;
+}
diff --git a/third_party/SimpleBluez/examples/list_adapters/CMakeLists.txt b/third_party/SimpleBluez/examples/list_adapters/CMakeLists.txt
index 1cd3d0f8b..e483d8c9e 100644
--- a/third_party/SimpleBluez/examples/list_adapters/CMakeLists.txt
+++ b/third_party/SimpleBluez/examples/list_adapters/CMakeLists.txt
@@ -3,5 +3,5 @@ cmake_minimum_required(VERSION 3.16.0)
 project(EXAMPLE_LIST_ADAPTERS)
 
 message("-- [INFO] Building Example")
-add_executable(example_list_adapters main.cpp)
-target_link_libraries(example_list_adapters simplebluez-static pthread)
+add_executable(example_list_adapters list_adapters.cpp)
+target_link_libraries(example_list_adapters simplebluez-static)
diff --git a/third_party/SimpleBluez/examples/list_adapters/main.cpp b/third_party/SimpleBluez/examples/list_adapters/list_adapters.cpp
similarity index 58%
rename from third_party/SimpleBluez/examples/list_adapters/main.cpp
rename to third_party/SimpleBluez/examples/list_adapters/list_adapters.cpp
index 7b4039611..2b3de6b62 100644
--- a/third_party/SimpleBluez/examples/list_adapters/main.cpp
+++ b/third_party/SimpleBluez/examples/list_adapters/list_adapters.cpp
@@ -1,17 +1,17 @@
-#include "BluezService.h"
+#include <simplebluez/Bluez.h>
 
-#include <stdlib.h>
+#include <cstdlib>
 #include <chrono>
 #include <iomanip>
 #include <iostream>
 #include <thread>
 
-BluezService bluez_service;
+SimpleBluez::Bluez bluez;
 
-volatile bool async_thread_active = true;
+std::atomic_bool async_thread_active = true;
 void async_thread_function() {
     while (async_thread_active) {
-        bluez_service.run_async();
+        bluez.run_async();
         std::this_thread::sleep_for(std::chrono::milliseconds(10));
     }
 }
@@ -23,12 +23,14 @@ void millisecond_delay(int ms) {
 }
 
 int main(int argc, char* argv[]) {
-    bluez_service.init();
+    bluez.init();
     std::thread* async_thread = new std::thread(async_thread_function);
 
-    auto internal_adapters = bluez_service.get_all_adapters();
-    for (auto& adapter : internal_adapters) {
-        std::cout << "Adapter: " << adapter->get_identifier() << " [" << adapter->Address() << "]" << std::endl;
+    auto adapters = bluez.get_adapters();
+    std::cout << "The following adapters were found:" << std::endl;
+    for (int i = 0; i < adapters.size(); i++) {
+        std::cout << "[" << i << "] " << adapters[i]->identifier() << " [" << adapters[i]->address() << "]"
+                  << std::endl;
     }
 
     async_thread_active = false;
diff --git a/third_party/SimpleBluez/examples/manuf_data/CMakeLists.txt b/third_party/SimpleBluez/examples/manuf_data/CMakeLists.txt
deleted file mode 100644
index 90233ab16..000000000
--- a/third_party/SimpleBluez/examples/manuf_data/CMakeLists.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-cmake_minimum_required(VERSION 3.16.0)
-
-project(EXAMPLE_MANUF_DATA)
-
-message("-- [INFO] Building Example")
-add_executable(example_manuf_data main.cpp)
-target_link_libraries(example_manuf_data simplebluez-static pthread)
diff --git a/third_party/SimpleBluez/examples/manuf_data/main.cpp b/third_party/SimpleBluez/examples/manuf_data/main.cpp
deleted file mode 100644
index a0b2963ac..000000000
--- a/third_party/SimpleBluez/examples/manuf_data/main.cpp
+++ /dev/null
@@ -1,68 +0,0 @@
-#include "BluezService.h"
-
-#include <stdlib.h>
-#include <chrono>
-#include <iomanip>
-#include <iostream>
-#include <thread>
-
-BluezService bluez_service;
-
-volatile bool async_thread_active = true;
-void async_thread_function() {
-    while (async_thread_active) {
-        bluez_service.run_async();
-        std::this_thread::sleep_for(std::chrono::milliseconds(10));
-    }
-}
-
-void millisecond_delay(int ms) {
-    for (int i = 0; i < ms; i++) {
-        std::this_thread::sleep_for(std::chrono::milliseconds(1));
-    }
-}
-
-/**
- * This is a minimal example in which we exchange some data
- * with a device exposing the Nordic UART service.
- */
-int main(int argc, char* argv[]) {
-    bluez_service.init();
-
-    std::thread* async_thread = new std::thread(async_thread_function);
-
-    auto adapter = bluez_service.get_first_adapter();
-    if (adapter == nullptr) {
-        std::cout << "No adapter found!" << std::endl;
-        return -1;
-    }
-
-    adapter->discovery_filter_transport_set("le");
-
-    adapter->OnDeviceUpdated = [](std::shared_ptr<BluezDevice> device) {
-        std::cout << "Update received for " << device->get_address() << std::endl;
-        std::cout << "\tName " << device->get_name() << std::endl;
-        auto manufacturer_data = device->get_manufacturer_data();
-        for (auto& [manufacturer_id, value_array] : manufacturer_data) {
-            std::cout << "\tManuf ID " << std::dec << (int)manufacturer_id << std::endl;
-            std::cout << "\t\t";
-            for (int i = 0; i < value_array.size(); i++) {
-                std::cout << std::setfill('0') << std::setw(2) << std::hex << ((int)value_array[i]) << " ";
-            }
-            std::cout << std::endl;
-        }
-    };
-
-    adapter->StartDiscovery();
-    millisecond_delay(5000);
-    adapter->StopDiscovery();
-
-    async_thread_active = false;
-    while (!async_thread->joinable()) {
-        millisecond_delay(10);
-    }
-    async_thread->join();
-    delete async_thread;
-
-    return 0;
-}
diff --git a/third_party/SimpleBLE/examples/connect/read/CMakeLists.txt b/third_party/SimpleBluez/examples/read/CMakeLists.txt
similarity index 71%
rename from third_party/SimpleBLE/examples/connect/read/CMakeLists.txt
rename to third_party/SimpleBluez/examples/read/CMakeLists.txt
index 57aef80e9..21b079fcd 100644
--- a/third_party/SimpleBLE/examples/connect/read/CMakeLists.txt
+++ b/third_party/SimpleBluez/examples/read/CMakeLists.txt
@@ -4,4 +4,4 @@ project(EXAMPLE_READ)
 
 message("-- [INFO] Building Example")
 add_executable(example_read read.cpp)
-target_link_libraries(example_read simpleble-static)
+target_link_libraries(example_read simplebluez-static)
diff --git a/third_party/SimpleBluez/examples/read/read.cpp b/third_party/SimpleBluez/examples/read/read.cpp
new file mode 100644
index 000000000..3144459b9
--- /dev/null
+++ b/third_party/SimpleBluez/examples/read/read.cpp
@@ -0,0 +1,139 @@
+#include <simplebluez/Bluez.h>
+#include <simplebluez/Exceptions.h>
+
+#include <atomic>
+#include <chrono>
+#include <cstdlib>
+#include <iomanip>
+#include <iostream>
+#include <thread>
+
+SimpleBluez::Bluez bluez;
+
+std::atomic_bool async_thread_active = true;
+void async_thread_function() {
+    while (async_thread_active) {
+        bluez.run_async();
+        std::this_thread::sleep_for(std::chrono::microseconds(100));
+    }
+}
+
+void millisecond_delay(int ms) {
+    for (int i = 0; i < ms; i++) {
+        std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    }
+}
+
+void print_byte_array(SimpleBluez::ByteArray& bytes) {
+    for (auto byte : bytes) {
+        std::cout << std::hex << std::setfill('0') << (uint32_t)((uint8_t)byte) << " ";
+        break;
+    }
+    std::cout << std::endl;
+}
+
+std::vector<std::shared_ptr<SimpleBluez::Device>> peripherals;
+
+int main(int argc, char* argv[]) {
+    int selection = -1;
+
+    bluez.init();
+    std::thread* async_thread = new std::thread(async_thread_function);
+
+    auto adapters = bluez.get_adapters();
+    std::cout << "Available adapters:" << std::endl;
+    for (int i = 0; i < adapters.size(); i++) {
+        std::cout << "[" << i << "] " << adapters[i]->identifier() << " [" << adapters[i]->address() << "]"
+                  << std::endl;
+    }
+
+    std::cout << "Please select an adapter to scan: ";
+    std::cin >> selection;
+    if (selection < 0 || selection >= adapters.size()) {
+        std::cout << "Invalid selection" << std::endl;
+        return 1;
+    }
+
+    auto adapter = adapters[selection];
+    std::cout << "Scanning " << adapter->identifier() << " [" << adapter->address() << "]" << std::endl;
+
+    adapter->discovery_filter(SimpleBluez::Adapter::DiscoveryFilter::LE);
+
+    adapter->set_on_device_updated([](std::shared_ptr<SimpleBluez::Device> device) {
+        if (std::find(peripherals.begin(), peripherals.end(), device) == peripherals.end()) {
+            std::cout << "Found device: " << device->name() << " [" << device->address() << "]" << std::endl;
+            peripherals.push_back(device);
+        }
+    });
+
+    adapter->discovery_start();
+    millisecond_delay(3000);
+    adapter->discovery_stop();
+
+    std::cout << "The following devices were found:" << std::endl;
+    for (int i = 0; i < peripherals.size(); i++) {
+        std::cout << "[" << i << "] " << peripherals[i]->name() << " [" << peripherals[i]->address() << "]"
+                  << std::endl;
+    }
+
+    std::cout << "Please select a device to connect to: ";
+    std::cin >> selection;
+
+    if (selection >= 0 && selection < peripherals.size()) {
+        auto peripheral = peripherals[selection];
+        std::cout << "Connecting to " << peripheral->name() << " [" << peripheral->address() << "]" << std::endl;
+
+        for (int attempt = 0; attempt < 3; attempt++) {
+            try {
+                peripheral->connect();
+            } catch (SimpleDBus::Exception::SendFailed& e) {
+                millisecond_delay(100);
+            }
+        }
+
+        if (!peripheral->connected() && !peripheral->services_resolved()) {
+            std::cout << "Failed to connect to " << peripheral->name() << " [" << peripheral->address() << "]"
+                      << std::endl;
+            return 1;
+        }
+
+        // Store all services and characteristics in a vector.
+        std::vector<std::pair<std::shared_ptr<SimpleBluez::Service>, std::shared_ptr<SimpleBluez::Characteristic>>>
+            char_list;
+        for (auto service : peripheral->services()) {
+            for (auto characteristic : service->characteristics()) {
+                char_list.push_back(std::make_pair(service, characteristic));
+            }
+        }
+
+        std::cout << "The following services and characteristics were found:" << std::endl;
+        for (int i = 0; i < char_list.size(); i++) {
+            std::cout << "[" << i << "] " << char_list[i].first->uuid() << " " << char_list[i].second->uuid()
+                      << std::endl;
+        }
+
+        std::cout << "Please select a characteristic to read: ";
+        std::cin >> selection;
+
+        if (selection >= 0 && selection < char_list.size()) {
+            SimpleBluez::ByteArray value = char_list[selection].second->read();
+            std::cout << "Characteristic contents were: ";
+            print_byte_array(value);
+        }
+
+        peripheral->disconnect();
+
+        // Sleep for an additional second before returning.
+        // If there are any unexpected events, this example will help debug them.
+        millisecond_delay(1000);
+    }
+
+    async_thread_active = false;
+    while (!async_thread->joinable()) {
+        millisecond_delay(10);
+    }
+    async_thread->join();
+    delete async_thread;
+
+    return 0;
+}
diff --git a/third_party/SimpleBLE/examples/connect/scan/CMakeLists.txt b/third_party/SimpleBluez/examples/scan/CMakeLists.txt
similarity index 71%
rename from third_party/SimpleBLE/examples/connect/scan/CMakeLists.txt
rename to third_party/SimpleBluez/examples/scan/CMakeLists.txt
index b5115ed25..6f9ddf4e7 100644
--- a/third_party/SimpleBLE/examples/connect/scan/CMakeLists.txt
+++ b/third_party/SimpleBluez/examples/scan/CMakeLists.txt
@@ -4,4 +4,4 @@ project(EXAMPLE_SCAN)
 
 message("-- [INFO] Building Example")
 add_executable(example_scan scan.cpp)
-target_link_libraries(example_scan simpleble-static)
+target_link_libraries(example_scan simplebluez-static)
diff --git a/third_party/SimpleBluez/examples/scan/scan.cpp b/third_party/SimpleBluez/examples/scan/scan.cpp
new file mode 100644
index 000000000..b63cd53d6
--- /dev/null
+++ b/third_party/SimpleBluez/examples/scan/scan.cpp
@@ -0,0 +1,80 @@
+#include <simplebluez/Bluez.h>
+
+#include <cstdlib>
+#include <atomic>
+#include <chrono>
+#include <iomanip>
+#include <iostream>
+#include <thread>
+
+SimpleBluez::Bluez bluez;
+
+std::atomic_bool async_thread_active = true;
+void async_thread_function() {
+    while (async_thread_active) {
+        bluez.run_async();
+        std::this_thread::sleep_for(std::chrono::microseconds(100));
+    }
+}
+
+void millisecond_delay(int ms) {
+    for (int i = 0; i < ms; i++) {
+        std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    }
+}
+
+int main(int argc, char* argv[]) {
+    int selection = -1;
+
+    bluez.init();
+    std::thread* async_thread = new std::thread(async_thread_function);
+
+    auto adapters = bluez.get_adapters();
+    std::cout << "The following adapters were found:" << std::endl;
+    for (int i = 0; i < adapters.size(); i++) {
+        std::cout << "[" << i << "] " << adapters[i]->identifier() << " [" << adapters[i]->address() << "]"
+                  << std::endl;
+    }
+
+    std::cout << "Please select an adapter to scan: ";
+    std::cin >> selection;
+    if (selection < 0 || selection >= adapters.size()) {
+        std::cout << "Invalid selection" << std::endl;
+        return 1;
+    }
+
+    auto adapter = adapters[selection];
+    std::cout << "Scanning " << adapter->identifier() << " [" << adapter->address() << "]" << std::endl;
+
+    adapter->discovery_filter(SimpleBluez::Adapter::DiscoveryFilter::LE);
+
+    adapter->set_on_device_updated([](std::shared_ptr<SimpleBluez::Device> device) {
+        std::cout << "Update received for " << device->address() << std::endl;
+        std::cout << "\tName " << device->name() << std::endl;
+        auto manufacturer_data = device->manufacturer_data();
+        for (auto& [manufacturer_id, value_array] : manufacturer_data) {
+            std::cout << "\tManuf ID 0x" << std::setfill('0') << std::setw(4) << std::hex << (int)manufacturer_id;
+            std::cout << ": ";
+            for (int i = 0; i < value_array.size(); i++) {
+                std::cout << std::setfill('0') << std::setw(2) << std::hex << ((int)value_array[i]) << " ";
+            }
+            std::cout << std::endl;
+        }
+    });
+
+    adapter->discovery_start();
+    millisecond_delay(3000);
+    adapter->discovery_stop();
+
+    // Sleep for a bit to allow the adapter to stop discovering.
+    millisecond_delay(3000);
+
+    async_thread_active = false;
+    while (!async_thread->joinable()) {
+        millisecond_delay(10);
+    }
+    async_thread->join();
+    delete async_thread;
+
+    return 0;
+}
diff --git a/third_party/SimpleBluez/include/simplebluez/Adapter.h b/third_party/SimpleBluez/include/simplebluez/Adapter.h
new file mode 100644
index 000000000..ddb4cf620
--- /dev/null
+++ b/third_party/SimpleBluez/include/simplebluez/Adapter.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <simpledbus/advanced/Proxy.h>
+
+#include <simplebluez/Device.h>
+#include <simplebluez/interfaces/Adapter1.h>
+
+#include <functional>
+
+namespace SimpleBluez {
+
+class Adapter : public SimpleDBus::Proxy {
+  public:
+    typedef Adapter1::DiscoveryFilter DiscoveryFilter;
+
+    Adapter(std::shared_ptr<SimpleDBus::Connection> conn, const std::string& bus_name, const std::string& path);
+    virtual ~Adapter();
+
+    std::string identifier() const;
+    std::string address();
+    bool discovering();
+
+    void discovery_filter(const DiscoveryFilter& filter);
+    void discovery_start();
+    void discovery_stop();
+
+    std::shared_ptr<Device> device_get(const std::string& path);
+    
+    void set_on_device_updated(std::function<void(std::shared_ptr<Device> device)> callback);
+    void clear_on_device_updated();
+
+  private:
+    std::shared_ptr<SimpleDBus::Proxy> path_create(const std::string& path) override;
+    std::shared_ptr<SimpleDBus::Interface> interfaces_create(const std::string& interface_name) override;
+
+    std::shared_ptr<Adapter1> adapter1();
+};
+
+}  // namespace SimpleBluez
diff --git a/third_party/SimpleBluez/include/simplebluez/Bluez.h b/third_party/SimpleBluez/include/simplebluez/Bluez.h
new file mode 100644
index 000000000..5ee6ab021
--- /dev/null
+++ b/third_party/SimpleBluez/include/simplebluez/Bluez.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <simpledbus/advanced/Proxy.h>
+#include <simpledbus/interfaces/ObjectManager.h>
+
+#include <simplebluez/Adapter.h>
+
+#include <vector>
+
+namespace SimpleBluez {
+
+class Bluez : public SimpleDBus::Proxy {
+  public:
+    Bluez();
+    virtual ~Bluez();
+
+    void init();
+    void run_async();
+
+    std::vector<std::shared_ptr<Adapter>> get_adapters();
+
+  private:
+    std::shared_ptr<SimpleDBus::Proxy> path_create(const std::string& path) override;
+    
+    std::shared_ptr<SimpleDBus::ObjectManager> object_manager();
+};
+
+}  // namespace SimpleBluez
diff --git a/third_party/SimpleBluez/include/simplebluez/Characteristic.h b/third_party/SimpleBluez/include/simplebluez/Characteristic.h
new file mode 100644
index 000000000..bc502ed94
--- /dev/null
+++ b/third_party/SimpleBluez/include/simplebluez/Characteristic.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <simpledbus/advanced/Proxy.h>
+
+#include <simplebluez/Types.h>
+#include <simplebluez/interfaces/GattCharacteristic1.h>
+
+#include <cstdlib>
+
+namespace SimpleBluez {
+
+class Characteristic : public SimpleDBus::Proxy {
+  public:
+    Characteristic(std::shared_ptr<SimpleDBus::Connection> conn, const std::string& bus_name, const std::string& path);
+    virtual ~Characteristic();
+
+    // ----- METHODS -----
+    ByteArray read();
+    void write_request(ByteArray value);
+    void write_command(ByteArray value);
+    void start_notify();
+    void stop_notify();
+
+    // ----- PROPERTIES -----
+    std::string uuid();
+    ByteArray value();
+    bool notifying();
+
+    // ----- CALLBACKS -----
+    void set_on_value_changed(std::function<void(ByteArray new_value)> callback);
+    void clear_on_value_changed();
+
+  private:
+    std::shared_ptr<SimpleDBus::Interface> interfaces_create(const std::string& interface_name) override;
+
+    std::shared_ptr<GattCharacteristic1> gattcharacteristic1();
+};
+
+}  // namespace SimpleBluez
diff --git a/third_party/SimpleBluez/include/simplebluez/Device.h b/third_party/SimpleBluez/include/simplebluez/Device.h
new file mode 100644
index 000000000..81258b7b0
--- /dev/null
+++ b/third_party/SimpleBluez/include/simplebluez/Device.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include <simpledbus/advanced/Proxy.h>
+
+#include <simplebluez/Characteristic.h>
+#include <simplebluez/Service.h>
+#include <simplebluez/interfaces/Device1.h>
+
+namespace SimpleBluez {
+
+class Device : public SimpleDBus::Proxy {
+  public:
+    Device(std::shared_ptr<SimpleDBus::Connection> conn, const std::string& bus_name, const std::string& path);
+    virtual ~Device();
+
+    std::shared_ptr<Service> get_service(const std::string& uuid);
+    std::shared_ptr<Characteristic> get_characteristic(const std::string& service_uuid,
+                                                       const std::string& characteristic_uuid);
+
+    // ----- PROPERTIES -----
+    std::vector<std::shared_ptr<Service>> services();
+
+    std::string address();
+    std::string name();
+    std::string alias();
+
+    std::map<uint16_t, std::vector<uint8_t>> manufacturer_data();
+
+    bool connected();
+    bool services_resolved();
+
+    // ----- METHODS -----
+    void connect();
+    void disconnect();
+
+    // ----- CALLBACKS -----
+    void set_on_services_resolved(std::function<void()> callback);
+    void clear_on_services_resolved();
+    void set_on_disconnected(std::function<void()> callback);
+    void clear_on_disconnected();
+
+  private:
+    std::shared_ptr<SimpleDBus::Proxy> path_create(const std::string& path) override;
+    std::shared_ptr<SimpleDBus::Interface> interfaces_create(const std::string& interface_name) override;
+
+    std::shared_ptr<Device1> device1();
+};
+
+}  // namespace SimpleBluez
diff --git a/third_party/SimpleBluez/include/simplebluez/Exceptions.h b/third_party/SimpleBluez/include/simplebluez/Exceptions.h
new file mode 100644
index 000000000..0bdeda78a
--- /dev/null
+++ b/third_party/SimpleBluez/include/simplebluez/Exceptions.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <stdexcept>
+#include <string>
+
+#include <simpledbus/base/Exceptions.h>
+
+namespace SimpleBluez {
+
+namespace Exception {
+
+class BaseException : public std::exception {};
+
+class ServiceNotFoundException : public BaseException {
+  public:
+    ServiceNotFoundException(const std::string& service);
+    const char* what() const noexcept override;
+
+  private:
+    std::string _message;
+};
+
+class CharacteristicNotFoundException : public BaseException {
+  public:
+    CharacteristicNotFoundException(const std::string& characteristic);
+    const char* what() const noexcept override;
+
+  private:
+    std::string _message;
+};
+
+}  // namespace Exception
+
+}  // namespace SimpleBluez
diff --git a/third_party/SimpleBluez/include/simplebluez/ProxyOrg.h b/third_party/SimpleBluez/include/simplebluez/ProxyOrg.h
new file mode 100644
index 000000000..067cc4d28
--- /dev/null
+++ b/third_party/SimpleBluez/include/simplebluez/ProxyOrg.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <simpledbus/advanced/Proxy.h>
+
+#include <simplebluez/Adapter.h>
+
+namespace SimpleBluez {
+
+class ProxyOrg : public SimpleDBus::Proxy {
+  public:
+    ProxyOrg(std::shared_ptr<SimpleDBus::Connection> conn, const std::string& bus_name, const std::string& path);
+    virtual ~ProxyOrg() = default;
+
+    std::vector<std::shared_ptr<Adapter>> get_adapters();
+
+  private:
+    std::shared_ptr<SimpleDBus::Proxy> path_create(const std::string& path) override;
+    std::shared_ptr<SimpleDBus::Interface> interfaces_create(const std::string& interface_name) override;
+};
+
+}  // namespace SimpleBluez
diff --git a/third_party/SimpleBluez/include/simplebluez/ProxyOrgBluez.h b/third_party/SimpleBluez/include/simplebluez/ProxyOrgBluez.h
new file mode 100644
index 000000000..b4301ec93
--- /dev/null
+++ b/third_party/SimpleBluez/include/simplebluez/ProxyOrgBluez.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <simpledbus/advanced/Proxy.h>
+
+#include <simplebluez/Adapter.h>
+
+namespace SimpleBluez {
+
+class ProxyOrgBluez : public SimpleDBus::Proxy {
+  public:
+    ProxyOrgBluez(std::shared_ptr<SimpleDBus::Connection> conn, const std::string& bus_name, const std::string& path);
+    virtual ~ProxyOrgBluez() = default;
+
+    std::vector<std::shared_ptr<Adapter>> get_adapters();
+
+  private:
+    std::shared_ptr<SimpleDBus::Proxy> path_create(const std::string& path) override;
+    std::shared_ptr<SimpleDBus::Interface> interfaces_create(const std::string& interface_name) override;
+};
+
+}  // namespace SimpleBluez
diff --git a/third_party/SimpleBluez/include/simplebluez/Service.h b/third_party/SimpleBluez/include/simplebluez/Service.h
new file mode 100644
index 000000000..9abf55a79
--- /dev/null
+++ b/third_party/SimpleBluez/include/simplebluez/Service.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <simpledbus/advanced/Proxy.h>
+
+#include <simplebluez/Characteristic.h>
+#include <simplebluez/interfaces/GattService1.h>
+
+namespace SimpleBluez {
+
+class Service : public SimpleDBus::Proxy {
+  public:
+    Service(std::shared_ptr<SimpleDBus::Connection> conn, const std::string& bus_name, const std::string& path);
+    virtual ~Service() = default;
+
+    std::shared_ptr<Characteristic> get_characteristic(const std::string& uuid);
+
+    // ----- PROPERTIES -----
+    std::vector<std::shared_ptr<Characteristic>> characteristics();
+
+    std::string uuid();
+
+  private:
+    std::shared_ptr<SimpleDBus::Proxy> path_create(const std::string& path) override;
+    std::shared_ptr<SimpleDBus::Interface> interfaces_create(const std::string& interface_name) override;
+
+    std::shared_ptr<GattService1> gattservice1();
+};
+
+}  // namespace SimpleBluez
diff --git a/third_party/SimpleBluez/include/simplebluez/Types.h b/third_party/SimpleBluez/include/simplebluez/Types.h
new file mode 100644
index 000000000..346600291
--- /dev/null
+++ b/third_party/SimpleBluez/include/simplebluez/Types.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include <string>
+
+namespace SimpleBluez {
+
+typedef std::string ByteArray;
+
+}
diff --git a/third_party/SimpleBluez/include/simplebluez/interfaces/Adapter1.h b/third_party/SimpleBluez/include/simplebluez/interfaces/Adapter1.h
new file mode 100644
index 000000000..e9d432db5
--- /dev/null
+++ b/third_party/SimpleBluez/include/simplebluez/interfaces/Adapter1.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <simpledbus/advanced/Interface.h>
+
+#include <string>
+
+namespace SimpleBluez {
+
+class Adapter1 : public SimpleDBus::Interface {
+  public:
+    typedef enum { AUTO, BREDR, LE } DiscoveryFilter;
+
+    Adapter1(std::shared_ptr<SimpleDBus::Connection> conn, std::string path);
+    virtual ~Adapter1() = default;
+
+    // ----- METHODS -----
+    void StartDiscovery();
+    void StopDiscovery();
+    void SetDiscoveryFilter(DiscoveryFilter filter);
+    SimpleDBus::Holder GetDiscoveryFilters();
+    // void RemoveDevice(SimpleDBus::Holder device); // TODO: Implement
+
+    // ----- PROPERTIES -----
+    bool Discovering(bool refresh = true);
+    std::string Address();
+
+  protected:
+    bool _discovering;
+    std::string _address;
+
+    void property_changed(std::string option_name) override;
+};
+
+}  // namespace SimpleBluez
diff --git a/third_party/SimpleBluez/include/simplebluez/interfaces/Device1.h b/third_party/SimpleBluez/include/simplebluez/interfaces/Device1.h
new file mode 100644
index 000000000..cca46c4e7
--- /dev/null
+++ b/third_party/SimpleBluez/include/simplebluez/interfaces/Device1.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <simpledbus/advanced/Callback.h>
+#include <simpledbus/advanced/Interface.h>
+
+#include <string>
+
+namespace SimpleBluez {
+
+class Device1 : public SimpleDBus::Interface {
+  public:
+    Device1(std::shared_ptr<SimpleDBus::Connection> conn, std::string path);
+    virtual ~Device1();
+
+    // ----- METHODS -----
+    void Connect();
+    void Disconnect();
+
+    // ----- PROPERTIES -----
+    int16_t RSSI();
+    uint16_t Appearance();  // On Bluez 5.53, this always returns 0.
+    std::string Address();
+    std::string Alias();
+    std::string Name();
+    std::map<uint16_t, std::vector<uint8_t>> ManufacturerData(bool refresh = true);
+    bool Connected(bool refresh = true);
+    bool ServicesResolved(bool refresh = true);
+
+    // ----- CALLBACKS -----
+    SimpleDBus::Callback<std::function<void()>> OnServicesResolved;
+    SimpleDBus::Callback<std::function<void()>> OnDisconnected;
+
+  protected:
+    void property_changed(std::string option_name) override;
+
+    int16_t _rssi;
+    std::string _name;
+    std::string _alias;
+    std::string _address;
+    bool _connected;
+    bool _services_resolved;
+    std::map<uint16_t, std::vector<uint8_t>> _manufacturer_data;
+};
+
+}  // namespace SimpleBluez
diff --git a/third_party/SimpleBluez/include/simplebluez/interfaces/GattCharacteristic1.h b/third_party/SimpleBluez/include/simplebluez/interfaces/GattCharacteristic1.h
new file mode 100644
index 000000000..701eedc5a
--- /dev/null
+++ b/third_party/SimpleBluez/include/simplebluez/interfaces/GattCharacteristic1.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <simpledbus/advanced/Callback.h>
+#include <simpledbus/advanced/Interface.h>
+
+#include <simplebluez/Types.h>
+
+#include <string>
+
+namespace SimpleBluez {
+
+class GattCharacteristic1 : public SimpleDBus::Interface {
+  public:
+    typedef enum { REQUEST = 0, COMMAND } WriteType;
+
+    GattCharacteristic1(std::shared_ptr<SimpleDBus::Connection> conn, std::string path);
+    virtual ~GattCharacteristic1();
+
+    // ----- METHODS -----
+    void StartNotify();
+    void StopNotify();
+    void WriteValue(const ByteArray& value, WriteType type);
+    ByteArray ReadValue();
+
+    // ----- PROPERTIES -----
+    std::string UUID();
+    ByteArray Value();
+    bool Notifying(bool refresh = true);
+
+    // ----- CALLBACKS -----
+    SimpleDBus::Callback<std::function<void()>> OnValueChanged;
+
+  protected:
+    void property_changed(std::string option_name) override;
+    void update_value(SimpleDBus::Holder& new_value);
+
+    std::string _uuid;
+    ByteArray _value;
+};
+
+}  // namespace SimpleBluez
diff --git a/third_party/SimpleBluez/include/simplebluez/interfaces/GattService1.h b/third_party/SimpleBluez/include/simplebluez/interfaces/GattService1.h
new file mode 100644
index 000000000..555ea8d9d
--- /dev/null
+++ b/third_party/SimpleBluez/include/simplebluez/interfaces/GattService1.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <simpledbus/advanced/Interface.h>
+
+#include <string>
+
+namespace SimpleBluez {
+
+class GattService1 : public SimpleDBus::Interface {
+  public:
+    GattService1(std::shared_ptr<SimpleDBus::Connection> conn, std::string path);
+    virtual ~GattService1() = default;
+
+    // ----- METHODS -----
+
+    // ----- PROPERTIES -----
+    std::string UUID();
+
+  protected:
+    void property_changed(std::string option_name) override;
+
+    std::string _uuid;
+};
+
+}  // namespace SimpleBluez
diff --git a/third_party/SimpleBluez/src/Adapter.cpp b/third_party/SimpleBluez/src/Adapter.cpp
new file mode 100644
index 000000000..c324c9778
--- /dev/null
+++ b/third_party/SimpleBluez/src/Adapter.cpp
@@ -0,0 +1,65 @@
+#include <simplebluez/Adapter.h>
+#include <simplebluez/Device.h>
+
+#include <simplebluez/interfaces/Adapter1.h>
+
+using namespace SimpleBluez;
+
+Adapter::Adapter(std::shared_ptr<SimpleDBus::Connection> conn, const std::string& bus_name, const std::string& path)
+    : Proxy(conn, bus_name, path) {}
+
+Adapter::~Adapter() {}
+
+std::shared_ptr<SimpleDBus::Proxy> Adapter::path_create(const std::string& path) {
+    auto child = std::make_shared<Device>(_conn, _bus_name, path);
+    return std::static_pointer_cast<SimpleDBus::Proxy>(child);
+}
+
+std::shared_ptr<SimpleDBus::Interface> Adapter::interfaces_create(const std::string& interface_name) {
+    if (interface_name == "org.bluez.Adapter1") {
+        return std::static_pointer_cast<SimpleDBus::Interface>(std::make_shared<Adapter1>(_conn, _path));
+    }
+
+    auto interface = std::make_shared<SimpleDBus::Interface>(_conn, _bus_name, _path, interface_name);
+    return std::static_pointer_cast<SimpleDBus::Interface>(interface);
+}
+
+std::shared_ptr<Adapter1> Adapter::adapter1() {
+    return std::dynamic_pointer_cast<Adapter1>(interface_get("org.bluez.Adapter1"));
+}
+
+std::string Adapter::identifier() const {
+    std::size_t start = _path.find_last_of("/");
+    return _path.substr(start + 1);
+}
+
+std::string Adapter::address() { return adapter1()->Address(); }
+
+bool Adapter::discovering() { return adapter1()->Discovering(); }
+
+void Adapter::discovery_filter(const DiscoveryFilter& filter) { adapter1()->SetDiscoveryFilter(filter); }
+
+void Adapter::discovery_start() { adapter1()->StartDiscovery(); }
+
+void Adapter::discovery_stop() { adapter1()->StopDiscovery(); }
+
+std::shared_ptr<Device> Adapter::device_get(const std::string& path) {
+    return std::dynamic_pointer_cast<Device>(path_get(path));
+}
+
+void Adapter::set_on_device_updated(std::function<void(std::shared_ptr<Device> device)> callback) {
+    auto on_device_updated = [this, callback](std::string child_path) {
+        auto device = device_get(child_path);
+        if (device) {
+            callback(device);
+        }
+    };
+
+    on_child_created.load(on_device_updated);
+    on_child_signal_received.load(on_device_updated);
+}
+
+void Adapter::clear_on_device_updated() {
+    on_child_created.unload();
+    on_child_signal_received.unload();
+}
diff --git a/third_party/SimpleBluez/src/Bluez.cpp b/third_party/SimpleBluez/src/Bluez.cpp
new file mode 100644
index 000000000..4c2459355
--- /dev/null
+++ b/third_party/SimpleBluez/src/Bluez.cpp
@@ -0,0 +1,51 @@
+#include <simplebluez/Bluez.h>
+#include <simplebluez/ProxyOrg.h>
+#include <simpledbus/interfaces/ObjectManager.h>
+
+using namespace SimpleBluez;
+
+Bluez::Bluez() : Proxy(std::make_shared<SimpleDBus::Connection>(DBUS_BUS_SYSTEM), "org.bluez", "/") {
+    _interfaces["org.freedesktop.DBus.ObjectManager"] = std::static_pointer_cast<SimpleDBus::Interface>(
+        std::make_shared<SimpleDBus::ObjectManager>(_conn, "org.bluez", "/"));
+
+    object_manager()->InterfacesAdded = [&](std::string path, SimpleDBus::Holder options) { path_add(path, options); };
+    object_manager()->InterfacesRemoved = [&](std::string path, SimpleDBus::Holder options) {
+        path_remove(path, options);
+    };
+}
+
+Bluez::~Bluez() { _conn->remove_match("type='signal',sender='org.bluez'"); }
+
+void Bluez::init() {
+    _conn->init();
+
+    // Load all managed objects
+    SimpleDBus::Holder managed_objects = object_manager()->GetManagedObjects();
+    for (auto& [path, managed_interfaces] : managed_objects.get_dict_object_path()) {
+        path_add(path, managed_interfaces);
+    }
+
+    _conn->add_match("type='signal',sender='org.bluez'");
+}
+
+void Bluez::run_async() {
+    _conn->read_write();
+    SimpleDBus::Message message = _conn->pop_message();
+    while (message.is_valid()) {
+        message_forward(message);
+        message = _conn->pop_message();
+    }
+}
+
+std::vector<std::shared_ptr<Adapter>> Bluez::get_adapters() {
+    return std::dynamic_pointer_cast<ProxyOrg>(path_get("/org"))->get_adapters();
+}
+
+std::shared_ptr<SimpleDBus::Proxy> Bluez::path_create(const std::string& path) {
+    auto child = std::make_shared<ProxyOrg>(_conn, _bus_name, path);
+    return std::static_pointer_cast<SimpleDBus::Proxy>(child);
+}
+
+std::shared_ptr<SimpleDBus::ObjectManager> Bluez::object_manager() {
+    return std::dynamic_pointer_cast<SimpleDBus::ObjectManager>(interface_get("org.freedesktop.DBus.ObjectManager"));
+}
diff --git a/third_party/SimpleBluez/src/BluezAdapter.cpp b/third_party/SimpleBluez/src/BluezAdapter.cpp
deleted file mode 100644
index 10b6f140c..000000000
--- a/third_party/SimpleBluez/src/BluezAdapter.cpp
+++ /dev/null
@@ -1,139 +0,0 @@
-#include "BluezAdapter.h"
-
-#include "simpledbus/base/Logger.h"
-
-#include <iostream>
-
-BluezAdapter::BluezAdapter(SimpleDBus::Connection* conn, std::string path, SimpleDBus::Holder managed_interfaces)
-    : _conn(conn), _path(path), Adapter1{conn, path}, Introspectable{conn, "org.bluez", path} {
-    // std::cout << "Creating BluezAdapter" << std::endl;
-
-    PropertyHandler::PropertiesChanged = [&](std::string interface, SimpleDBus::Holder changed_properties,
-                                        SimpleDBus::Holder invalidated_properties) {
-        if (interface == "org.bluez.Adapter1") {
-            Adapter1::set_options(changed_properties, invalidated_properties);
-        } else {
-        }
-    };
-
-    auto managed_interface = managed_interfaces.get_dict_string();
-    for (auto& [iface, options] : managed_interface) {
-        add_interface(iface, options);
-    }
-}
-
-BluezAdapter::~BluezAdapter() {
-    // std::cout << "Destroying BluezAdapter" << std::endl;
-}
-
-bool BluezAdapter::process_received_signal(SimpleDBus::Message& message) {
-    if (message.get_path() == _path) {
-        if (PropertyHandler::process_received_signal(message)) return true;
-    } else {
-        for (auto& [device_path, device] : _devices) {
-            if (device->process_received_signal(message)) {
-                if (OnDeviceUpdated) {
-                    OnDeviceUpdated(device);
-                }
-                return true;
-            }
-        }
-    }
-    return false;
-}
-
-void BluezAdapter::add_interface(std::string interface_name, SimpleDBus::Holder options) {
-    // std::cout << interface_name << std::endl;
-
-    if (interface_name == "org.bluez.Adapter1") {
-        Adapter1::set_options(options);
-    } else {
-    }
-}
-
-bool BluezAdapter::add_path(std::string path, SimpleDBus::Holder options) {
-    int path_elements = std::count(path.begin(), path.end(), '/');
-    if (path.rfind(_path, 0) == 0) {
-        if (path_elements == 4) {
-            // Corresponds to a device
-            _devices.emplace(std::make_pair(path, new BluezDevice(_conn, path, options)));
-            if (OnDeviceFound) {
-                OnDeviceFound(_devices[path]);
-            }
-            if (OnDeviceUpdated) {
-                OnDeviceUpdated(_devices[path]);
-            }
-            return true;
-        } else {
-            // Corresponds to a device component
-            for (auto& [device_path, device] : _devices) {
-                if (device->add_path(path, options)) return true;
-            }
-        }
-    }
-    return false;
-}
-
-bool BluezAdapter::remove_path(std::string path, SimpleDBus::Holder options) {
-    int path_elements = std::count(path.begin(), path.end(), '/');
-    if (path.rfind(_path, 0) == 0) {
-        if (path_elements == 4) {
-            // HACK: Due to the library architecture, the only way to make sure if one
-            // of the underlying objects has to be deleted is to check if the options
-            // list contains `org.bluez.Device1`, `org.freedesktop.DBus.Introspectable`,
-            // or `org.freedesktop.DBus.Properties`.
-            // A BluezDevice might also extend from `org.bluez.Battery1`, for example.
-            // Eventually this structure will have to be revisited.
-            std::vector<std::string> interface_list;
-            for (auto& option : options.get_array()) {
-                interface_list.push_back(option.get_string());
-            }
-            bool must_erase = std::any_of(interface_list.begin(), interface_list.end(), [](const std::string& str) {
-                return str == "org.freedesktop.DBus.Properties" || str == "org.freedesktop.DBus.Introspectable" ||
-                       str == "org.bluez.Device1";
-            });
-            if (must_erase) {
-                _devices.erase(path);
-            }
-            return true;
-        } else {
-            // Propagate the paths downwards until someone claims it.
-            for (auto& [device_path, device] : _devices) {
-                if (device->remove_path(path, options)) return true;
-            }
-        }
-    }
-    return false;
-}
-
-void BluezAdapter::discovery_filter_transport_set(std::string value) {
-    /*
-    Possible values:
-        "auto"	- interleaved scan
-        "bredr"	- BR/EDR inquiry
-        "le"	- LE scan only
-    */
-    // TODO: Validate input
-
-    SimpleDBus::Holder argument = SimpleDBus::Holder::create_dict();
-    argument.dict_append(SimpleDBus::Holder::Type::STRING, "Transport", SimpleDBus::Holder::create_string(value.c_str()));
-    SetDiscoveryFilter(argument);
-}
-
-std::shared_ptr<BluezDevice> BluezAdapter::get_device(std::string mac_address) {
-    // TODO: Apply proper mac address formatting
-    // TODO: How do I know which adapter I should check?
-    std::shared_ptr<BluezDevice> return_value = nullptr;
-
-    for (auto& [path, device] : _devices) {
-        if (device->get_address() == mac_address) {
-            return_value = device;
-        }
-    }
-    return return_value;
-}
-
-std::string BluezAdapter::get_identifier() {
-    std::size_t start = _path.find_last_of("/");
-    return _path.substr(start + 1);
-}
diff --git a/third_party/SimpleBluez/src/BluezAdapter.h b/third_party/SimpleBluez/src/BluezAdapter.h
deleted file mode 100644
index 63f4864e6..000000000
--- a/third_party/SimpleBluez/src/BluezAdapter.h
+++ /dev/null
@@ -1,37 +0,0 @@
-#pragma once
-
-#include "simpledbus/SimpleDBus.h"
-
-#include "interfaces/Adapter1.h"
-
-#include "BluezDevice.h"
-
-#include <memory>
-#include <string>
-
-class BluezAdapter : public Adapter1, public SimpleDBus::Introspectable {
-  private:
-    SimpleDBus::Connection* _conn;
-    std::string _path;
-
-    std::map<std::string, std::shared_ptr<BluezDevice>> _devices;
-    void add_interface(std::string interface_name, SimpleDBus::Holder options);
-
-  public:
-    BluezAdapter(SimpleDBus::Connection* conn, std::string path, SimpleDBus::Holder managed_interfaces);
-    ~BluezAdapter();
-
-    std::string get_identifier();
-    std::shared_ptr<BluezDevice> get_device(std::string mac_address);
-
-    bool add_path(std::string path, SimpleDBus::Holder options);
-    bool remove_path(std::string path, SimpleDBus::Holder options);
-    bool process_received_signal(SimpleDBus::Message& message);
-
-    // TODO: Add support for more complex filter types.
-    void discovery_filter_transport_set(std::string value);
-
-    std::function<void(std::shared_ptr<BluezDevice>)> OnDeviceFound;
-    std::function<void(std::shared_ptr<BluezDevice>)> OnDeviceUpdated;
-
-};
diff --git a/third_party/SimpleBluez/src/BluezAgent.cpp b/third_party/SimpleBluez/src/BluezAgent.cpp
deleted file mode 100644
index 7f99ac80d..000000000
--- a/third_party/SimpleBluez/src/BluezAgent.cpp
+++ /dev/null
@@ -1,12 +0,0 @@
-#include "BluezAgent.h"
-
-BluezAgent::BluezAgent(std::string path, SimpleDBus::Holder options) : _path(path) {}
-
-BluezAgent::~BluezAgent() {}
-
-bool BluezAgent::process_received_signal(SimpleDBus::Message& message) {
-    if (message.get_path() == _path) {
-        return true;
-    }
-    return false;
-}
\ No newline at end of file
diff --git a/third_party/SimpleBluez/src/BluezAgent.h b/third_party/SimpleBluez/src/BluezAgent.h
deleted file mode 100644
index 27e705a15..000000000
--- a/third_party/SimpleBluez/src/BluezAgent.h
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma once
-
-#include "simpledbus/SimpleDBus.h"
-
-#include <string>
-
-class BluezAgent {
-  private:
-    std::string _path;
-
-  public:
-    BluezAgent(std::string path, SimpleDBus::Holder options);
-    ~BluezAgent();
-
-    bool process_received_signal(SimpleDBus::Message& message);
-};
diff --git a/third_party/SimpleBluez/src/BluezDevice.cpp b/third_party/SimpleBluez/src/BluezDevice.cpp
deleted file mode 100644
index 5471acb73..000000000
--- a/third_party/SimpleBluez/src/BluezDevice.cpp
+++ /dev/null
@@ -1,116 +0,0 @@
-#include "BluezDevice.h"
-
-#include "simpledbus/base/Logger.h"
-
-#include <iostream>
-
-BluezDevice::BluezDevice(SimpleDBus::Connection* conn, std::string path, SimpleDBus::Holder managed_interfaces)
-    : _conn(conn), _path(path), Device1{conn, path} {
-    PropertyHandler::PropertiesChanged = [&](std::string interface, SimpleDBus::Holder changed_properties,
-                                        SimpleDBus::Holder invalidated_properties) {
-        if (interface == "org.bluez.Device1") {
-            Device1::set_options(changed_properties, invalidated_properties);
-        } else {
-        }
-    };
-
-    auto managed_interface = managed_interfaces.get_dict_string();
-    for (auto& [iface, options] : managed_interface) {
-        add_interface(iface, options);
-    }
-}
-
-BluezDevice::~BluezDevice() {}
-
-bool BluezDevice::process_received_signal(SimpleDBus::Message& message) {
-    if (message.get_path() == _path) {
-        if (PropertyHandler::process_received_signal(message)) return true;
-    } else {
-        for (auto& [gatt_service_path, gatt_service] : gatt_services) {
-            if (gatt_service->process_received_signal(message)) return true;
-        }
-    }
-    return false;
-}
-
-void BluezDevice::add_interface(std::string interface_name, SimpleDBus::Holder options) {
-    if (interface_name == "org.bluez.Device1") {
-        Device1::set_options(options);
-    } else {
-    }
-}
-
-bool BluezDevice::add_path(std::string path, SimpleDBus::Holder options) {
-    int path_elements = std::count(path.begin(), path.end(), '/');
-    if (path.rfind(_path, 0) == 0) {
-        if (path_elements == 5) {
-            if (path.find("service") != std::string::npos) {
-                // We're adding a service
-                gatt_services.emplace(std::make_pair(path, new BluezGattService(_conn, path, options)));
-            }
-        } else {
-            // Corresponds to a device component
-            for (auto& [gatt_service_path, gatt_service] : gatt_services) {
-                if (gatt_service->add_path(path, options)) return true;
-            }
-        }
-        return true;
-    }
-    return false;
-}
-
-bool BluezDevice::remove_path(std::string path, SimpleDBus::Holder options) {
-    int path_elements = std::count(path.begin(), path.end(), '/');
-    if (path.rfind(_path, 0) == 0) {
-        if (path_elements == 5) {
-            gatt_services.erase(path);
-            return true;
-        } else {
-            // Propagate the paths downwards until someone claims it.
-            for (auto& [gatt_service_path, gatt_service] : gatt_services) {
-                if (gatt_service->remove_path(path, options)) return true;
-            }
-        }
-    }
-    return false;
-}
-
-std::vector<std::string> BluezDevice::get_service_list() {
-    std::vector<std::string> service_list;
-    for (auto& [gatt_service_path, gatt_service] : gatt_services) {
-        service_list.push_back(gatt_service->get_uuid());
-    }
-    return service_list;
-}
-
-std::vector<std::string> BluezDevice::get_characteristic_list(std::string service_uuid) {
-    std::shared_ptr<BluezGattService> service = get_service(service_uuid);
-    if (service != nullptr) {
-        return service->get_characteristic_list();
-    }
-    return std::vector<std::string>();
-}
-
-std::shared_ptr<BluezGattService> BluezDevice::get_service(std::string service_uuid) {
-    std::shared_ptr<BluezGattService> return_value = nullptr;
-
-    for (auto& [gatt_service_path, gatt_service] : gatt_services) {
-        if (gatt_service->get_uuid() == service_uuid) {
-            return_value = gatt_service;
-            break;
-        }
-    }
-
-    return return_value;
-}
-
-std::shared_ptr<BluezGattCharacteristic> BluezDevice::get_characteristic(std::string service_uuid,
-                                                                         std::string char_uuid) {
-    std::shared_ptr<BluezGattCharacteristic> return_value = nullptr;
-    std::shared_ptr<BluezGattService> service = get_service(service_uuid);
-
-    if (service != nullptr) {
-        return_value = service->get_characteristic(char_uuid);
-    }
-    return return_value;
-}
diff --git a/third_party/SimpleBluez/src/BluezDevice.h b/third_party/SimpleBluez/src/BluezDevice.h
deleted file mode 100644
index 73a5ab8ac..000000000
--- a/third_party/SimpleBluez/src/BluezDevice.h
+++ /dev/null
@@ -1,37 +0,0 @@
-#pragma once
-
-#include "simpledbus/SimpleDBus.h"
-
-#include "interfaces/Device1.h"
-
-#include "BluezGattService.h"
-
-#include <memory>
-#include <string>
-#include <vector>
-
-class BluezDevice : public Device1 {
-  private:
-    SimpleDBus::Connection* _conn;
-    std::string _path;
-
-    std::map<std::string, std::shared_ptr<BluezGattService>> gatt_services;
-    void add_interface(std::string interface_name, SimpleDBus::Holder options);
-
-  public:
-    BluezDevice(SimpleDBus::Connection* conn, std::string path, SimpleDBus::Holder options);
-    ~BluezDevice();
-
-    bool add_path(std::string path, SimpleDBus::Holder options);
-    bool remove_path(std::string path, SimpleDBus::Holder options);
-    bool process_received_signal(SimpleDBus::Message& message);
-
-    void connect();
-    void disconnect();
-
-    std::vector<std::string> get_service_list();
-    std::vector<std::string> get_characteristic_list(std::string service_uuid);
-
-    std::shared_ptr<BluezGattService> get_service(std::string service_uuid);
-    std::shared_ptr<BluezGattCharacteristic> get_characteristic(std::string service_uuid, std::string char_uuid);
-};
diff --git a/third_party/SimpleBluez/src/BluezGattCharacteristic.cpp b/third_party/SimpleBluez/src/BluezGattCharacteristic.cpp
deleted file mode 100644
index c22e259f4..000000000
--- a/third_party/SimpleBluez/src/BluezGattCharacteristic.cpp
+++ /dev/null
@@ -1,59 +0,0 @@
-#include "BluezGattCharacteristic.h"
-
-#include "simpledbus/base/Logger.h"
-
-BluezGattCharacteristic::BluezGattCharacteristic(SimpleDBus::Connection* conn, std::string path,
-                                                 SimpleDBus::Holder managed_interfaces)
-    : _conn(conn), _path(path), GattCharacteristic1{conn, path} {
-    PropertyHandler::PropertiesChanged = [&](std::string interface, SimpleDBus::Holder changed_properties,
-                                        SimpleDBus::Holder invalidated_properties) {
-        if (interface == "org.bluez.GattCharacteristic1") {
-            GattCharacteristic1::set_options(changed_properties, invalidated_properties);
-        } else {
-        }
-    };
-
-    auto managed_interface = managed_interfaces.get_dict_string();
-    for (auto& [iface, options] : managed_interface) {
-        add_interface(iface, options);
-    }
-}
-
-BluezGattCharacteristic::~BluezGattCharacteristic() {
-    // std::cout << "Destroying BluezGattCharacteristic" << std::endl;
-}
-
-bool BluezGattCharacteristic::process_received_signal(SimpleDBus::Message& message) {
-    if (message.get_path() == _path) {
-        if (PropertyHandler::process_received_signal(message)) return true;
-        // TODO: Add any remaining signal receivers.
-    }
-    return false;
-}
-
-void BluezGattCharacteristic::add_interface(std::string interface_name, SimpleDBus::Holder options) {
-    // std::cout << interface_name << std::endl;
-    if (interface_name == "org.bluez.GattCharacteristic1") {
-        GattCharacteristic1::set_options(options);
-    } else {
-    }
-}
-
-bool BluezGattCharacteristic::add_path(std::string path, SimpleDBus::Holder options) {
-    int path_elements = std::count(path.begin(), path.end(), '/');
-    if (path.rfind(_path, 0) == 0) {
-        if (path_elements == 7) {
-            // TODO: Characteristics also have Descriptors, although I'm not sure we need them.
-            // std::cout << "New path: " << path << std::endl << options.represent() << std::endl;
-        } else {
-        }
-
-        return true;
-    }
-    return false;
-}
-
-bool BluezGattCharacteristic::remove_path(std::string path, SimpleDBus::Holder options) {
-    LOG_F(DEBUG, "remove_path not implemented (%s needed to remove %s)", _path.c_str(), _path.c_str());
-    return false;
-}
diff --git a/third_party/SimpleBluez/src/BluezGattCharacteristic.h b/third_party/SimpleBluez/src/BluezGattCharacteristic.h
deleted file mode 100644
index 5e4982dcd..000000000
--- a/third_party/SimpleBluez/src/BluezGattCharacteristic.h
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma once
-
-#include "simpledbus/SimpleDBus.h"
-
-#include "interfaces/GattCharacteristic1.h"
-
-#include <string>
-
-class BluezGattCharacteristic : public GattCharacteristic1 {
-  private:
-    SimpleDBus::Connection* _conn;
-    std::string _path;
-
-    void add_interface(std::string interface_name, SimpleDBus::Holder options);
-
-  public:
-    BluezGattCharacteristic(SimpleDBus::Connection* conn, std::string path, SimpleDBus::Holder options);
-    ~BluezGattCharacteristic();
-
-    bool add_path(std::string path, SimpleDBus::Holder options);
-    bool remove_path(std::string path, SimpleDBus::Holder options);
-    bool process_received_signal(SimpleDBus::Message& message);
-};
diff --git a/third_party/SimpleBluez/src/BluezGattService.cpp b/third_party/SimpleBluez/src/BluezGattService.cpp
deleted file mode 100644
index ba21fff87..000000000
--- a/third_party/SimpleBluez/src/BluezGattService.cpp
+++ /dev/null
@@ -1,97 +0,0 @@
-#include "BluezGattService.h"
-
-#include "simpledbus/base/Logger.h"
-
-BluezGattService::BluezGattService(SimpleDBus::Connection* conn, std::string path,
-                                   SimpleDBus::Holder managed_interfaces)
-    : _conn(conn), _path(path), GattService1{conn, path} {
-    PropertyHandler::PropertiesChanged = [&](std::string interface, SimpleDBus::Holder changed_properties,
-                                        SimpleDBus::Holder invalidated_properties) {
-        if (interface == "org.bluez.GattService1") {
-            GattService1::set_options(changed_properties, invalidated_properties);
-        } else {
-        }
-    };
-
-    auto managed_interface = managed_interfaces.get_dict_string();
-    for (auto& [iface, options] : managed_interface) {
-        add_interface(iface, options);
-    }
-}
-
-BluezGattService::~BluezGattService() {}
-
-bool BluezGattService::process_received_signal(SimpleDBus::Message& message) {
-    if (message.get_path() == _path) {
-        if (PropertyHandler::process_received_signal(message)) return true;
-    } else {
-        for (auto& [gatt_char_path, gatt_characteristic] : gatt_characteristics) {
-            if (gatt_characteristic->process_received_signal(message)) return true;
-        }
-    }
-    return false;
-}
-
-void BluezGattService::add_interface(std::string interface_name, SimpleDBus::Holder options) {
-    // std::cout << interface_name << std::endl;
-    if (interface_name == "org.bluez.GattService1") {
-        GattService1::set_options(options);
-    } else {
-    }
-}
-
-bool BluezGattService::add_path(std::string path, SimpleDBus::Holder options) {
-    int path_elements = std::count(path.begin(), path.end(), '/');
-    if (path.rfind(_path, 0) == 0) {
-        if (path_elements == 6) {
-            if (path.find("char") != std::string::npos) {
-                // We're adding a service
-                gatt_characteristics.emplace(std::make_pair(path, new BluezGattCharacteristic(_conn, path, options)));
-            }
-        } else {
-            // Corresponds to a device component
-            for (auto& [gatt_char_path, gatt_characteristic] : gatt_characteristics) {
-                if (gatt_characteristic->add_path(path, options)) return true;
-            }
-        }
-        return true;
-    }
-    return false;
-}
-
-bool BluezGattService::remove_path(std::string path, SimpleDBus::Holder options) {
-    int path_elements = std::count(path.begin(), path.end(), '/');
-    if (path.rfind(_path, 0) == 0) {
-        if (path_elements == 6) {
-            gatt_characteristics.erase(path);
-            return true;
-        } else {
-            // Propagate the paths downwards until someone claims it.
-            for (auto& [gatt_char_path, gatt_characteristic] : gatt_characteristics) {
-                if (gatt_characteristic->remove_path(path, options)) return true;
-            }
-        }
-    }
-    return false;
-}
-
-std::vector<std::string> BluezGattService::get_characteristic_list() {
-    std::vector<std::string> characteristic_list;
-    for (auto& [gatt_char_path, gatt_characteristic] : gatt_characteristics) {
-        characteristic_list.push_back(gatt_characteristic->get_uuid());
-    }
-    return characteristic_list;
-}
-
-std::shared_ptr<BluezGattCharacteristic> BluezGattService::get_characteristic(std::string char_uuid) {
-    std::shared_ptr<BluezGattCharacteristic> return_value = nullptr;
-
-    for (auto& [gatt_char_path, gatt_characteristic] : gatt_characteristics) {
-        if (gatt_characteristic->get_uuid() == char_uuid) {
-            return_value = gatt_characteristic;
-            break;
-        }
-    }
-
-    return return_value;
-}
diff --git a/third_party/SimpleBluez/src/BluezGattService.h b/third_party/SimpleBluez/src/BluezGattService.h
deleted file mode 100644
index e3a4464fc..000000000
--- a/third_party/SimpleBluez/src/BluezGattService.h
+++ /dev/null
@@ -1,31 +0,0 @@
-#pragma once
-
-#include "simpledbus/SimpleDBus.h"
-
-#include "interfaces/GattService1.h"
-
-#include "BluezGattCharacteristic.h"
-
-#include <memory>
-#include <string>
-#include <vector>
-
-class BluezGattService : public GattService1 {
-  private:
-    SimpleDBus::Connection* _conn;
-    std::string _path;
-
-    std::map<std::string, std::shared_ptr<BluezGattCharacteristic>> gatt_characteristics;
-    void add_interface(std::string interface_name, SimpleDBus::Holder options);
-
-  public:
-    BluezGattService(SimpleDBus::Connection* conn, std::string path, SimpleDBus::Holder options);
-    ~BluezGattService();
-
-    bool add_path(std::string path, SimpleDBus::Holder options);
-    bool remove_path(std::string path, SimpleDBus::Holder options);
-    bool process_received_signal(SimpleDBus::Message& message);
-
-    std::vector<std::string> get_characteristic_list();
-    std::shared_ptr<BluezGattCharacteristic> get_characteristic(std::string char_uuid);
-};
diff --git a/third_party/SimpleBluez/src/BluezService.cpp b/third_party/SimpleBluez/src/BluezService.cpp
deleted file mode 100644
index 3338551b6..000000000
--- a/third_party/SimpleBluez/src/BluezService.cpp
+++ /dev/null
@@ -1,130 +0,0 @@
-#include "BluezService.h"
-
-#include <algorithm>
-
-BluezService::BluezService()
-    : conn(DBUS_BUS_SYSTEM), object_manager(&conn, "org.bluez", "/"), Introspectable{&conn, "org.bluez", "/"} {
-    object_manager.InterfacesAdded = [&](std::string path, SimpleDBus::Holder options) { add_path(path, options); };
-    object_manager.InterfacesRemoved = [&](std::string path, SimpleDBus::Holder options) {
-        remove_path(path, options);
-    };
-}
-
-BluezService::~BluezService() { conn.remove_match("type='signal',sender='org.bluez'"); }
-
-void BluezService::init() {
-    conn.init();
-    object_manager.GetManagedObjects(true);  // Feed the objects via callback.
-
-    conn.add_match("type='signal',sender='org.bluez'");
-}
-
-void BluezService::run_async() {
-    conn.read_write();
-    SimpleDBus::Message message = conn.pop_message();
-    while (message.is_valid()) {
-        switch (message.get_type()) {
-            case SimpleDBus::MessageType::SIGNAL:
-                process_received_signal(message);
-                break;
-            default:
-                break;
-        }
-        message = conn.pop_message();
-    }
-}
-
-void BluezService::process_received_signal(SimpleDBus::Message& message) {
-    std::string path = message.get_path();
-
-    if (path == "/org/freedesktop/DBus") return;
-
-    if (object_manager.process_received_signal(message)) return;
-
-    for (auto& [path, adapter] : adapters) {
-        if (adapter->process_received_signal(message)) return;
-    }
-}
-
-void BluezService::add_path(std::string path, SimpleDBus::Holder options) {
-    int path_elements = std::count(path.begin(), path.end(), '/');
-
-    switch (path_elements) {
-        case 2:
-            agent.reset(new BluezAgent(path, options));
-            break;
-        case 3:
-            adapters.emplace(std::make_pair(path, new BluezAdapter(&conn, path, options)));
-            break;
-        default:
-            // Propagate the paths downwards until someone claims it.
-            for (auto& [adapter_path, adapter] : adapters) {
-                if (adapter->add_path(path, options)) return;
-            }
-            break;
-    }
-}
-
-void BluezService::remove_path(std::string path, SimpleDBus::Holder options) {
-    int path_elements = std::count(path.begin(), path.end(), '/');
-
-    switch (path_elements) {
-        case 2:
-            break;
-        case 3: {
-            // HACK: Due to the library architecture, the only way to make sure if one
-            // of the underlying objects has to be deleted is to check if the options
-            // list contains `org.bluez.Adapter1`, `org.freedesktop.DBus.Introspectable`,
-            // or `org.freedesktop.DBus.Properties`.
-            // Eventually this structure will have to be revisited.
-            std::vector<std::string> interface_list;
-            for (auto& option : options.get_array()) {
-                interface_list.push_back(option.get_string());
-            }
-            bool must_erase = std::any_of(interface_list.begin(), interface_list.end(), [](const std::string& str) {
-                return str == "org.freedesktop.DBus.Properties" || str == "org.freedesktop.DBus.Introspectable" ||
-                       str == "org.bluez.Adapter1";
-            });
-            if (must_erase) {
-                adapters.erase(path);
-            }
-            break;
-        }
-        default:
-            // Propagate the paths downwards until someone claims it.
-            for (auto& [adapter_path, adapter] : adapters) {
-                if (adapter->remove_path(path, options)) return;
-            }
-            break;
-    }
-}
-
-std::shared_ptr<BluezAdapter> BluezService::get_first_adapter() {
-    std::shared_ptr<BluezAdapter> return_value = nullptr;
-    if (!adapters.empty()) {
-        return_value = adapters.begin()->second;
-    }
-
-    return return_value;
-}
-
-std::shared_ptr<BluezAdapter> BluezService::get_adapter(std::string adapter_name) {
-    std::shared_ptr<BluezAdapter> return_value = nullptr;
-    std::string expected_path = "/org/bluez/" + adapter_name;
-
-    for (auto& [adapter_path, adapter] : adapters) {
-        if (adapter_path == expected_path) {
-            return_value = adapter;
-            break;
-        }
-    }
-    return return_value;
-}
-
-std::vector<std::shared_ptr<BluezAdapter>> BluezService::get_all_adapters() {
-    std::vector<std::shared_ptr<BluezAdapter>> adapter_list;
-    for (auto& [adapter_path, adapter] : adapters) {
-        adapter_list.push_back(adapter);
-    }
-    return adapter_list;
-}
diff --git a/third_party/SimpleBluez/src/BluezService.h b/third_party/SimpleBluez/src/BluezService.h
deleted file mode 100644
index e962ad6e4..000000000
--- a/third_party/SimpleBluez/src/BluezService.h
+++ /dev/null
@@ -1,35 +0,0 @@
-#pragma once
-#include "simpledbus/SimpleDBus.h"
-
-#include <iostream>
-#include <map>
-#include <memory>
-#include <vector>
-
-#include "BluezAdapter.h"
-#include "BluezAgent.h"
-
-class BluezService : public SimpleDBus::Introspectable {
-  private:
-    SimpleDBus::Connection conn;
-    SimpleDBus::ObjectManager object_manager;
-
-    void add_path(std::string path, SimpleDBus::Holder options);
-    void remove_path(std::string path, SimpleDBus::Holder options);
-
-    std::shared_ptr<BluezAgent> agent;
-    std::map<std::string, std::shared_ptr<BluezAdapter>> adapters;
-
-    void process_received_signal(SimpleDBus::Message& message);
-
-  public:
-    BluezService();
-    ~BluezService();
-
-    void init();
-    void run_async();
-
-    std::shared_ptr<BluezAdapter> get_first_adapter();
-    std::shared_ptr<BluezAdapter> get_adapter(std::string adapter_name);
-    std::vector<std::shared_ptr<BluezAdapter>> get_all_adapters();
-};
diff --git a/third_party/SimpleBluez/src/Characteristic.cpp b/third_party/SimpleBluez/src/Characteristic.cpp
new file mode 100644
index 000000000..e36af3b79
--- /dev/null
+++ b/third_party/SimpleBluez/src/Characteristic.cpp
@@ -0,0 +1,48 @@
+#include <simplebluez/Characteristic.h>
+
+using namespace SimpleBluez;
+
+Characteristic::Characteristic(std::shared_ptr<SimpleDBus::Connection> conn, const std::string& bus_name,
+                               const std::string& path)
+    : Proxy(conn, bus_name, path) {}
+
+Characteristic::~Characteristic() {}
+
+std::shared_ptr<SimpleDBus::Interface> Characteristic::interfaces_create(const std::string& interface_name) {
+    if (interface_name == "org.bluez.GattCharacteristic1") {
+        return std::static_pointer_cast<SimpleDBus::Interface>(std::make_shared<GattCharacteristic1>(_conn, _path));
+    }
+
+    auto interface = std::make_shared<SimpleDBus::Interface>(_conn, _bus_name, _path, interface_name);
+    return std::static_pointer_cast<SimpleDBus::Interface>(interface);
+}
+
+std::shared_ptr<GattCharacteristic1> Characteristic::gattcharacteristic1() {
+    return std::dynamic_pointer_cast<GattCharacteristic1>(interface_get("org.bluez.GattCharacteristic1"));
+}
+
+bool Characteristic::notifying() { return gattcharacteristic1()->Notifying(); }
+
+std::string Characteristic::uuid() { return gattcharacteristic1()->UUID(); }
+
+ByteArray Characteristic::value() { return gattcharacteristic1()->Value(); }
+
+ByteArray Characteristic::read() { return gattcharacteristic1()->ReadValue(); }
+
+void Characteristic::write_request(ByteArray value) {
+    gattcharacteristic1()->WriteValue(value, GattCharacteristic1::WriteType::REQUEST);
+}
+
+void Characteristic::write_command(ByteArray value) {
+    gattcharacteristic1()->WriteValue(value, GattCharacteristic1::WriteType::COMMAND);
+}
+
+void Characteristic::start_notify() { gattcharacteristic1()->StartNotify(); }
+
+void Characteristic::stop_notify() { gattcharacteristic1()->StopNotify(); }
+
+void Characteristic::set_on_value_changed(std::function<void(ByteArray new_value)> callback) {
+    gattcharacteristic1()->OnValueChanged.load([this, callback]() { callback(gattcharacteristic1()->Value()); });
+}
+
+void Characteristic::clear_on_value_changed() { gattcharacteristic1()->OnValueChanged.unload(); }
diff --git a/third_party/SimpleBluez/src/Device.cpp b/third_party/SimpleBluez/src/Device.cpp
new file mode 100644
index 000000000..cc16d7403
--- /dev/null
+++ b/third_party/SimpleBluez/src/Device.cpp
@@ -0,0 +1,72 @@
+#include <simplebluez/Device.h>
+#include <simplebluez/Exceptions.h>
+#include <simplebluez/Service.h>
+
+using namespace SimpleBluez;
+
+Device::Device(std::shared_ptr<SimpleDBus::Connection> conn, const std::string& bus_name, const std::string& path)
+    : Proxy(conn, bus_name, path) {}
+
+Device::~Device() {}
+
+std::shared_ptr<SimpleDBus::Proxy> Device::path_create(const std::string& path) {
+    auto child = std::make_shared<Service>(_conn, _bus_name, path);
+    return std::static_pointer_cast<SimpleDBus::Proxy>(child);
+}
+
+std::shared_ptr<SimpleDBus::Interface> Device::interfaces_create(const std::string& interface_name) {
+    if (interface_name == "org.bluez.Device1") {
+        return std::static_pointer_cast<SimpleDBus::Interface>(std::make_shared<Device1>(_conn, _path));
+    }
+
+    auto interface = std::make_shared<SimpleDBus::Interface>(_conn, _bus_name, _path, interface_name);
+    return std::static_pointer_cast<SimpleDBus::Interface>(interface);
+}
+
+std::shared_ptr<Device1> Device::device1() {
+    return std::dynamic_pointer_cast<Device1>(interface_get("org.bluez.Device1"));
+}
+
+std::vector<std::shared_ptr<Service>> Device::services() { return children_casted<Service>(); }
+
+std::shared_ptr<Service> Device::get_service(const std::string& uuid) {
+    auto services_all = services();
+
+    for (auto& service : services_all) {
+        if (service->uuid() == uuid) {
+            return service;
+        }
+    }
+
+    throw Exception::ServiceNotFoundException(uuid);
+}
+
+std::shared_ptr<Characteristic> Device::get_characteristic(const std::string& service_uuid,
+                                                           const std::string& characteristic_uuid) {
+    auto service = get_service(service_uuid);
+    return service->get_characteristic(characteristic_uuid);
+}
+
+void Device::connect() { device1()->Connect(); }
+
+void Device::disconnect() { device1()->Disconnect(); }
+
+std::string Device::address() { return device1()->Address(); }
+
+std::string Device::name() { return device1()->Name(); }
+
+std::string Device::alias() { return device1()->Alias(); }
+
+std::map<uint16_t, std::vector<uint8_t>> Device::manufacturer_data() { return device1()->ManufacturerData(); }
+
+bool Device::connected() { return device1()->Connected(); }
+
+bool Device::services_resolved() { return device1()->ServicesResolved(); }
+
+void Device::set_on_disconnected(std::function<void()> callback) { device1()->OnDisconnected.load(callback); }
+
+void Device::clear_on_disconnected() { device1()->OnDisconnected.unload(); }
+
+void Device::set_on_services_resolved(std::function<void()> callback) { device1()->OnServicesResolved.load(callback); }
+
+void Device::clear_on_services_resolved() { device1()->OnServicesResolved.unload(); }
diff --git a/third_party/SimpleBluez/src/Exceptions.cpp b/third_party/SimpleBluez/src/Exceptions.cpp
new file mode 100644
index 000000000..d0586bdef
--- /dev/null
+++ b/third_party/SimpleBluez/src/Exceptions.cpp
@@ -0,0 +1,23 @@
+#include <simplebluez/Exceptions.h>
+
+#include <fmt/core.h>
+
+namespace SimpleBluez {
+
+namespace Exception {
+
+ServiceNotFoundException::ServiceNotFoundException(const std::string& service) {
+    _message = fmt::format("Service {} not found.", service);
+}
+
+const char* ServiceNotFoundException::what() const noexcept { return _message.c_str(); }
+
+CharacteristicNotFoundException::CharacteristicNotFoundException(const std::string& characteristic) {
+    _message = fmt::format("Characteristic {} not found.", characteristic);
+}
+
+const char* CharacteristicNotFoundException::what() const noexcept { return _message.c_str(); }
+
+}  // namespace Exception
+
+}  // namespace SimpleBluez
diff --git a/third_party/SimpleBluez/src/ProxyOrg.cpp b/third_party/SimpleBluez/src/ProxyOrg.cpp
new file mode 100644
index 000000000..64b4b0636
--- /dev/null
+++ b/third_party/SimpleBluez/src/ProxyOrg.cpp
@@ -0,0 +1,21 @@
+#include <simplebluez/ProxyOrg.h>
+#include <simplebluez/ProxyOrgBluez.h>
+
+using namespace SimpleBluez;
+
+ProxyOrg::ProxyOrg(std::shared_ptr<SimpleDBus::Connection> conn, const std::string& bus_name, const std::string& path)
+    : Proxy(conn, bus_name, path) {}
+
+std::vector<std::shared_ptr<Adapter>> ProxyOrg::get_adapters() {
+    return std::dynamic_pointer_cast<ProxyOrgBluez>(path_get("/org/bluez"))->get_adapters();
+}
+
+std::shared_ptr<SimpleDBus::Proxy> ProxyOrg::path_create(const std::string& path) {
+    auto child = std::make_shared<ProxyOrgBluez>(_conn, _bus_name, path);
+    return std::static_pointer_cast<SimpleDBus::Proxy>(child);
+}
+
+std::shared_ptr<SimpleDBus::Interface> ProxyOrg::interfaces_create(const std::string& interface_name) {
+    auto interface = std::make_shared<SimpleDBus::Interface>(_conn, _bus_name, _path, interface_name);
+    return std::static_pointer_cast<SimpleDBus::Interface>(interface);
+}
diff --git a/third_party/SimpleBluez/src/ProxyOrgBluez.cpp b/third_party/SimpleBluez/src/ProxyOrgBluez.cpp
new file mode 100644
index 000000000..ffbbe3224
--- /dev/null
+++ b/third_party/SimpleBluez/src/ProxyOrgBluez.cpp
@@ -0,0 +1,20 @@
+#include <simplebluez/Adapter.h>
+#include <simplebluez/ProxyOrgBluez.h>
+
+using namespace SimpleBluez;
+
+ProxyOrgBluez::ProxyOrgBluez(std::shared_ptr<SimpleDBus::Connection> conn, const std::string& bus_name,
+                             const std::string& path)
+    : Proxy(conn, bus_name, path) {}
+
+std::shared_ptr<SimpleDBus::Proxy> ProxyOrgBluez::path_create(const std::string& path) {
+    auto child = std::make_shared<Adapter>(_conn, _bus_name, path);
+    return std::static_pointer_cast<SimpleDBus::Proxy>(child);
+}
+
+std::shared_ptr<SimpleDBus::Interface> ProxyOrgBluez::interfaces_create(const std::string& interface_name) {
+    auto interface = std::make_shared<SimpleDBus::Interface>(_conn, _bus_name, _path, interface_name);
+    return std::static_pointer_cast<SimpleDBus::Interface>(interface);
+}
+
+std::vector<std::shared_ptr<Adapter>> ProxyOrgBluez::get_adapters() { return children_casted<Adapter>(); }
diff --git a/third_party/SimpleBluez/src/Service.cpp b/third_party/SimpleBluez/src/Service.cpp
new file mode 100644
index 000000000..f640ea2d7
--- /dev/null
+++ b/third_party/SimpleBluez/src/Service.cpp
@@ -0,0 +1,42 @@
+#include <simplebluez/Characteristic.h>
+#include <simplebluez/Exceptions.h>
+#include <simplebluez/Service.h>
+
+using namespace SimpleBluez;
+
+Service::Service(std::shared_ptr<SimpleDBus::Connection> conn, const std::string& bus_name, const std::string& path)
+    : Proxy(conn, bus_name, path) {}
+
+std::shared_ptr<SimpleDBus::Proxy> Service::path_create(const std::string& path) {
+    auto child = std::make_shared<Characteristic>(_conn, _bus_name, path);
+    return std::static_pointer_cast<SimpleDBus::Proxy>(child);
+}
+
+std::shared_ptr<SimpleDBus::Interface> Service::interfaces_create(const std::string& interface_name) {
+    if (interface_name == "org.bluez.GattService1") {
+        return std::static_pointer_cast<SimpleDBus::Interface>(std::make_shared<GattService1>(_conn, _path));
+    }
+
+    auto interface = std::make_shared<SimpleDBus::Interface>(_conn, _bus_name, _path, interface_name);
+    return std::static_pointer_cast<SimpleDBus::Interface>(interface);
+}
+
+std::shared_ptr<GattService1> Service::gattservice1() {
+    return std::dynamic_pointer_cast<GattService1>(interface_get("org.bluez.GattService1"));
+}
+
+std::vector<std::shared_ptr<Characteristic>> Service::characteristics() { return children_casted<Characteristic>(); }
+
+std::shared_ptr<Characteristic> Service::get_characteristic(const std::string& uuid) {
+    auto characteristics_all = characteristics();
+
+    for (auto& characteristic : characteristics_all) {
+        if (characteristic->uuid() == uuid) {
+            return characteristic;
+        }
+    }
+
+    throw Exception::CharacteristicNotFoundException(uuid);
+}
+
+std::string Service::uuid() { return gattservice1()->UUID(); }
diff --git a/third_party/SimpleBluez/src/interfaces/Adapter1.cpp b/third_party/SimpleBluez/src/interfaces/Adapter1.cpp
index a71e65af0..a8b055e31 100644
--- a/third_party/SimpleBluez/src/interfaces/Adapter1.cpp
+++ b/third_party/SimpleBluez/src/interfaces/Adapter1.cpp
@@ -1,85 +1,66 @@
-#include "Adapter1.h"
+#include "simplebluez/interfaces/Adapter1.h"
 
-#include "simpledbus/base/Logger.h"
+using namespace SimpleBluez;
 
-const std::string Adapter1::_interface_name = "org.bluez.Adapter1";
-
-Adapter1::Adapter1(SimpleDBus::Connection* conn, std::string path)
-    : _conn(conn), _path(path), _discovering(false), Properties{conn, "org.bluez", path}, PropertyHandler(path) {}
-
-Adapter1::~Adapter1() {}
-
-void Adapter1::add_option(std::string option_name, SimpleDBus::Holder value) {
-    if (option_name == "Discovering") {
-        _discovering = value.get_boolean();
-        if (_discovering && OnDiscoveryStarted) {
-            LOG_F(VERBOSE_0, "%s -> OnDiscoveryStarted", _path.c_str());
-            OnDiscoveryStarted();
-        } else if (!_discovering && OnDiscoveryStopped) {
-            LOG_F(VERBOSE_0, "%s -> OnDiscoveryStopped", _path.c_str());
-            OnDiscoveryStopped();
-        }
-    } else if (option_name == "Address") {
-        _address = value.get_string();
-    }
-}
-void Adapter1::remove_option(std::string option_name) {}
+Adapter1::Adapter1(std::shared_ptr<SimpleDBus::Connection> conn, std::string path)
+    : SimpleDBus::Interface(conn, "org.bluez", path, "org.bluez.Adapter1") {}
 
 void Adapter1::StartDiscovery() {
-    if (!_discovering) {
-        LOG_F(DEBUG, "%s -> StartDiscovery", _path.c_str());
-        auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "StartDiscovery");
-        _conn->send_with_reply_and_block(msg);
-    } else {
-        LOG_F(WARN, "%s is already discoverying...", _path.c_str());
-    }
-}
-void Adapter1::StopDiscovery() {
-    if (_discovering) {
-        LOG_F(DEBUG, "%s -> StopDiscovery", _path.c_str());
-        auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "StopDiscovery");
-        _conn->send_with_reply_and_block(msg);
-        // NOTE: It might take a few seconds until the peripheral reports that is has actually stopped discovering.
-    } else {
-        LOG_F(WARN, "%s was not discoverying...", _path.c_str());
-    }
-}
-
-void Adapter1::Action_StartDiscovery() {
-    LOG_F(DEBUG, "%s -> StartDiscovery", _path.c_str());
-    auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "StartDiscovery");
+    auto msg = create_method_call("StartDiscovery");
     _conn->send_with_reply_and_block(msg);
 }
 
-void Adapter1::Action_StopDiscovery() {
-    LOG_F(DEBUG, "%s -> StopDiscovery", _path.c_str());
-    auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "StopDiscovery");
+void Adapter1::StopDiscovery() {
+    auto msg = create_method_call("StopDiscovery");
     _conn->send_with_reply_and_block(msg);
     // NOTE: It might take a few seconds until the peripheral reports that is has actually stopped discovering.
 }
 
-bool Adapter1::Property_Discovering() {
-    auto value = Get(_interface_name, "Discovering");
-    add_option("Discovering", value);
-    return _discovering;
-}
-
 SimpleDBus::Holder Adapter1::GetDiscoveryFilters() {
-    LOG_F(DEBUG, "%s -> GetDiscoveryFilters", _path.c_str());
-    auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "GetDiscoveryFilters");
+    auto msg = create_method_call("GetDiscoveryFilters");
     SimpleDBus::Message reply_msg = _conn->send_with_reply_and_block(msg);
     SimpleDBus::Holder discovery_filters = reply_msg.extract();
-    // std::cout << discovery_filters.represent() << std::endl;
     return discovery_filters;
 }
 
-void Adapter1::SetDiscoveryFilter(SimpleDBus::Holder properties) {
-    LOG_F(DEBUG, "%s -> SetDiscoveryFilters", _path.c_str());
-    auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "SetDiscoveryFilter");
+void Adapter1::SetDiscoveryFilter(DiscoveryFilter filter) {
+    SimpleDBus::Holder properties = SimpleDBus::Holder::create_dict();
+
+    switch (filter) {
+        case DiscoveryFilter::AUTO: {
+            properties.dict_append(SimpleDBus::Holder::Type::STRING, "Transport",
+                                   SimpleDBus::Holder::create_string("auto"));
+            break;
+        }
+        case DiscoveryFilter::BREDR: {
+            properties.dict_append(SimpleDBus::Holder::Type::STRING, "Transport",
+                                   SimpleDBus::Holder::create_string("bredr"));
+            break;
+        }
+        case DiscoveryFilter::LE: {
+            properties.dict_append(SimpleDBus::Holder::Type::STRING, "Transport",
+                                   SimpleDBus::Holder::create_string("le"));
+            break;
+        }
+    }
+
+    auto msg = create_method_call("SetDiscoveryFilter");
     msg.append_argument(properties, "a{sv}");
     _conn->send_with_reply_and_block(msg);
 }
 
-std::string Adapter1::Address() { return _address; }
+bool Adapter1::Discovering(bool refresh) {
+    if (refresh) {
+        property_refresh("Discovering");
+    }
+
+    std::scoped_lock lock(_property_update_mutex);
+    return _properties["Discovering"].get_boolean();
+}
+
+std::string Adapter1::Address() {
+    std::scoped_lock lock(_property_update_mutex);
+    return _properties["Address"].get_string();
+}
 
-bool Adapter1::is_discovering() { return _discovering; }
+void Adapter1::property_changed(std::string option_name) {}
diff --git a/third_party/SimpleBluez/src/interfaces/Adapter1.h b/third_party/SimpleBluez/src/interfaces/Adapter1.h
deleted file mode 100644
index 4b5f5a232..000000000
--- a/third_party/SimpleBluez/src/interfaces/Adapter1.h
+++ /dev/null
@@ -1,40 +0,0 @@
-#pragma once
-
-#include "simpledbus/SimpleDBus.h"
-
-#include <string>
-
-class Adapter1 : public SimpleDBus::Interfaces::PropertyHandler, public SimpleDBus::Properties {
-  private:
-    static const std::string _interface_name;
-
-    SimpleDBus::Connection* _conn;
-    std::string _path;
-
-    bool _discovering;
-    std::string _address;
-
-    void add_option(std::string option_name, SimpleDBus::Holder value);
-    void remove_option(std::string option_name);
-
-  public:
-    Adapter1(SimpleDBus::Connection* conn, std::string path);
-    ~Adapter1();
-
-    // DBus Methods
-    void StartDiscovery();
-    void StopDiscovery();
-    void SetDiscoveryFilter(SimpleDBus::Holder properties);
-    std::string Address();
-    SimpleDBus::Holder GetDiscoveryFilters();
-
-    void Action_StartDiscovery();
-    void Action_StopDiscovery();
-
-    bool Property_Discovering();
-
-    bool is_discovering();
-
-    std::function<void(void)> OnDiscoveryStarted;
-    std::function<void(void)> OnDiscoveryStopped;
-};
diff --git a/third_party/SimpleBluez/src/interfaces/Device1.cpp b/third_party/SimpleBluez/src/interfaces/Device1.cpp
index e9d235016..c5241d119 100644
--- a/third_party/SimpleBluez/src/interfaces/Device1.cpp
+++ b/third_party/SimpleBluez/src/interfaces/Device1.cpp
@@ -1,133 +1,99 @@
-#include "Device1.h"
-
-#include "simpledbus/base/Logger.h"
-
-#include <iostream>
-
-const std::string Device1::_interface_name = "org.bluez.Device1";
-
-Device1::Device1(SimpleDBus::Connection* conn, std::string path)
-    : _conn(conn),
-      _path(path),
-      _address(""),
-      _name(""),
-      _services_resolved(false),
-      Properties{conn, "org.bluez", path},
-      PropertyHandler(path) {}
-
-Device1::~Device1() {}
-
-void Device1::add_option(std::string option_name, SimpleDBus::Holder value) {
-    if (option_name == "Address") {
-        _address = value.get_string();
-    } else if (option_name == "Name") {
-        _name = value.get_string();
-    } else if (option_name == "Alias") {
-        _alias = value.get_string();
-    } else if (option_name == "RSSI") {
-        _rssi = value.get_int16();
-    } else if (option_name == "ManufacturerData") {
-        std::map<uint16_t, SimpleDBus::Holder> manuf_data = value.get_dict_uint16();
-        // Loop through all received keys and store them.
-        for (auto& [key, value_array] : manuf_data) {
-            std::vector<uint8_t> raw_manuf_data;
-            for (auto& elem : value_array.get_array()) {
-                raw_manuf_data.push_back(elem.get_byte());
-            }
-            _manufacturer_data[key] = raw_manuf_data;
-        }
-    } else if (option_name == "Connected") {
-        bool connected = value.get_boolean();
-        if (connected && OnConnected) {
-            LOG_F(VERBOSE_0, "%s -> OnConnected", _path.c_str());
-            OnConnected();
-        } else if (!connected && OnDisconnected) {
-            LOG_F(VERBOSE_0, "%s -> OnDisconnected", _path.c_str());
-            OnDisconnected();
-        }
-    } else if (option_name == "ServicesResolved") {
-        bool callback_required = value.get_boolean() != _services_resolved;
-        _services_resolved = value.get_boolean();
-        if (callback_required && _services_resolved && OnServicesResolved) {
-            LOG_F(VERBOSE_0, "%s -> OnServicesResolved", _path.c_str());
-            OnServicesResolved();
-        }
-    }
-}
+#include "simplebluez/interfaces/Device1.h"
 
-void Device1::remove_option(std::string option_name) {}
+using namespace SimpleBluez;
+
+Device1::Device1(std::shared_ptr<SimpleDBus::Connection> conn, std::string path)
+    : SimpleDBus::Interface(conn, "org.bluez", path, "org.bluez.Device1") {}
+
+Device1::~Device1() {
+    OnDisconnected.unload();
+    OnServicesResolved.unload();
+}
 
 void Device1::Connect() {
-    if (!Property_Connected()) {
-        // Only attempt connection if disconnected.
-        LOG_F(DEBUG, "%s -> Connect", _path.c_str());
-        auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "Connect");
-        _conn->send_with_reply_and_block(msg);
-    } else {
-        LOG_F(WARN, "%s is already connected...", _path.c_str());
-        // If already connected, run callbacks to emulate a successful connection.
-        if (OnConnected) {
-            LOG_F(VERBOSE_0, "%s -> OnConnected (fake)", _path.c_str());
-            OnConnected();
-        }
-        if (_services_resolved && OnServicesResolved) {
-            LOG_F(VERBOSE_0, "%s -> OnServicesResolved (fake)", _path.c_str());
-            OnServicesResolved();
-        }
-    }
+    auto msg = create_method_call("Connect");
+    _conn->send_with_reply_and_block(msg);
 }
 
 void Device1::Disconnect() {
-    if (Property_Connected()) {
-        // Only attempt disconnection if connected.
-        LOG_F(DEBUG, "%s -> Disconnect", _path.c_str());
-        auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "Disconnect");
-        _conn->send_with_reply_and_block(msg);
-    } else {
-        LOG_F(WARN, "%s is already disconnected...", _path.c_str());
-        // If already disconnected, run callbacks to emulate a successful disconnection.
-        if (OnDisconnected) {
-            LOG_F(VERBOSE_0, "%s -> OnDisconnected (fake)", _path.c_str());
-            OnDisconnected();
-        }
-    }
+    auto msg = create_method_call("Disconnect");
+    _conn->send_with_reply_and_block(msg);
 }
 
-void Device1::Action_Connect() {
-    // Only attempt connection if disconnected.
-    LOG_F(DEBUG, "%s -> Connect", _path.c_str());
-    auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "Connect");
-    _conn->send_with_reply_and_block(msg);
+int16_t Device1::RSSI() {
+    std::scoped_lock lock(_property_update_mutex);
+    return _properties["RSSI"].get_int16();
 }
 
-void Device1::Action_Disconnect() {
-    // Only attempt disconnection if connected.
-    LOG_F(DEBUG, "%s -> Disconnect", _path.c_str());
-    auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "Disconnect");
-    _conn->send_with_reply_and_block(msg);
+uint16_t Device1::Appearance() {
+    std::scoped_lock lock(_property_update_mutex);
+    return _properties["Appearance"].get_uint16();
 }
 
-bool Device1::Property_Connected() {
-    auto value = Get(_interface_name, "Connected");
-    return value.get_boolean();
+std::string Device1::Address() {
+    std::scoped_lock lock(_property_update_mutex);
+    return _properties["Address"].get_string();
 }
 
-bool Device1::Property_ServicesResolved() {
-    auto value = Get(_interface_name, "ServicesResolved");
-    add_option("ServicesResolved", value);
-    return _services_resolved;
+std::string Device1::Alias() {
+    std::scoped_lock lock(_property_update_mutex);
+    return _properties["Alias"].get_string();
 }
 
-int16_t Device1::get_rssi() { return _rssi; }
+std::string Device1::Name() {
+    std::scoped_lock lock(_property_update_mutex);
+    return _properties["Name"].get_string();
+}
 
-std::string Device1::get_name() { return _name; }
+std::map<uint16_t, std::vector<uint8_t>> Device1::ManufacturerData(bool refresh) {
+    if (refresh) {
+        property_refresh("ManufacturerData");
+    }
 
-std::string Device1::get_alias() { return _alias; }
+    // Use the locally cached version to avoid parsing the map multiple times.
+    std::scoped_lock lock(_property_update_mutex);
+    return _manufacturer_data;
+}
+
+bool Device1::Connected(bool refresh) {
+    if (refresh) {
+        property_refresh("Connected");
+    }
 
-std::string Device1::get_address() { return _address; }
+    std::scoped_lock lock(_property_update_mutex);
+    return _properties["Connected"].get_boolean();
+}
 
-std::map<uint16_t, std::vector<uint8_t>> Device1::get_manufacturer_data() { return _manufacturer_data; }
+bool Device1::ServicesResolved(bool refresh) {
+    if (refresh) {
+        property_refresh("ServicesResolved");
+    }
 
-bool Device1::is_connected() { return Property_Connected(); }
+    std::scoped_lock lock(_property_update_mutex);
+    return _properties["ServicesResolved"].get_boolean();
+}
 
-bool Device1::is_services_resolved() { return _services_resolved; }
+void Device1::property_changed(std::string option_name) {
+    if (option_name == "Connected") {
+        if (!Connected(false)) {
+            OnDisconnected();
+        }
+    } else if (option_name == "ServicesResolved") {
+        if (ServicesResolved(false)) {
+            OnServicesResolved();
+        }
+    } else if (option_name == "ManufacturerData") {
+        std::scoped_lock lock(_property_update_mutex);
+
+        _manufacturer_data.clear();
+        std::map<uint16_t, SimpleDBus::Holder> manuf_data = _properties["ManufacturerData"].get_dict_uint16();
+        // Loop through all received keys and store them.
+        for (auto& [key, value_array] : manuf_data) {
+            std::vector<uint8_t> raw_manuf_data;
+            for (auto& elem : value_array.get_array()) {
+                raw_manuf_data.push_back(elem.get_byte());
+            }
+            _manufacturer_data[key] = raw_manuf_data;
+        }
+    }
+}
diff --git a/third_party/SimpleBluez/src/interfaces/Device1.h b/third_party/SimpleBluez/src/interfaces/Device1.h
deleted file mode 100644
index 5c247b74e..000000000
--- a/third_party/SimpleBluez/src/interfaces/Device1.h
+++ /dev/null
@@ -1,50 +0,0 @@
-#pragma once
-
-#include "simpledbus/SimpleDBus.h"
-
-#include <string>
-#include <map>
-#include <vector>
-
-class Device1 : public SimpleDBus::Interfaces::PropertyHandler, public SimpleDBus::Properties {
-  private:
-    static const std::string _interface_name;
-
-    SimpleDBus::Connection* _conn;
-    std::string _path;
-
-    int16_t _rssi;
-    std::string _name;
-    std::string _alias;
-    std::string _address;
-    bool _services_resolved;
-    std::map<uint16_t, std::vector<uint8_t>> _manufacturer_data;
-
-    void add_option(std::string option_name, SimpleDBus::Holder value);
-    void remove_option(std::string option_name);
-
-  public:
-    Device1(SimpleDBus::Connection* conn, std::string path);
-    ~Device1();
-
-    void Connect();
-    void Disconnect();
-
-    int16_t get_rssi();
-    std::string get_name();
-    std::string get_alias();
-    std::string get_address();
-    std::map<uint16_t, std::vector<uint8_t>> get_manufacturer_data();
-    bool is_connected();
-    bool is_services_resolved();
-
-    void Action_Connect();
-    void Action_Disconnect();
-
-    bool Property_Connected();
-    bool Property_ServicesResolved();
-
-    std::function<void(void)> OnConnected;
-    std::function<void(void)> OnDisconnected;
-    std::function<void(void)> OnServicesResolved;
-};
diff --git a/third_party/SimpleBluez/src/interfaces/GattCharacteristic1.cpp b/third_party/SimpleBluez/src/interfaces/GattCharacteristic1.cpp
index 9a4f5cf50..3516e484c 100644
--- a/third_party/SimpleBluez/src/interfaces/GattCharacteristic1.cpp
+++ b/third_party/SimpleBluez/src/interfaces/GattCharacteristic1.cpp
@@ -1,115 +1,93 @@
-#include "GattCharacteristic1.h"
+#include "simplebluez/interfaces/GattCharacteristic1.h"
 
-#include "simpledbus/base/Logger.h"
+using namespace SimpleBluez;
 
-const std::string GattCharacteristic1::_interface_name = "org.bluez.GattCharacteristic1";
+GattCharacteristic1::GattCharacteristic1(std::shared_ptr<SimpleDBus::Connection> conn, std::string path)
+    : SimpleDBus::Interface(conn, "org.bluez", path, "org.bluez.GattCharacteristic1") {}
 
-GattCharacteristic1::GattCharacteristic1(SimpleDBus::Connection* conn, std::string path)
-    : _conn(conn), _path(path), _notifying(false), Properties{conn, "org.bluez", path}, PropertyHandler(path) {}
+GattCharacteristic1::~GattCharacteristic1() { OnValueChanged.unload(); }
 
-GattCharacteristic1::~GattCharacteristic1() {}
-
-void GattCharacteristic1::add_option(std::string option_name, SimpleDBus::Holder value) {
-    if (option_name == "UUID") {
-        _uuid = value.get_string();
-    } else if (option_name == "Value") {
-        _value.clear();
-        auto value_array = value.get_array();
-        LOG_F(VERBOSE_2, "%s -> Value Length: %d", _path.c_str(), value_array.size());
-        for (auto& elem : value_array) {
-            _value.push_back(elem.get_byte());
-        }
-        if (ValueChanged) {
-            LOG_F(VERBOSE_2, "%s -> ValueChanged\n%s", _path.c_str(), value.represent().c_str());
-            ValueChanged(_value);
-        }
-    } else if (option_name == "Notifying") {
-        _notifying = value.get_boolean();
-    }
+void GattCharacteristic1::StartNotify() {
+    auto msg = create_method_call("StartNotify");
+    _conn->send_with_reply_and_block(msg);
 }
 
-void GattCharacteristic1::remove_option(std::string option_name) {}
-
-std::string GattCharacteristic1::get_uuid() { return _uuid; }
-
-std::vector<uint8_t> GattCharacteristic1::get_value() { return _value; }
+void GattCharacteristic1::StopNotify() {
+    auto msg = create_method_call("StopNotify");
+    _conn->send_with_reply_and_block(msg);
+}
 
-void GattCharacteristic1::StartNotify() {
-    if (!_notifying) {
-        LOG_F(DEBUG, "%s -> StartNotify", _path.c_str());
-        auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "StartNotify");
-        _conn->send_with_reply_and_block(msg);
-    } else {
-        LOG_F(WARN, "%s is already notifying...", _path.c_str());
+void GattCharacteristic1::WriteValue(const ByteArray& value, WriteType type) {
+    SimpleDBus::Holder value_data = SimpleDBus::Holder::create_array();
+    for (size_t i = 0; i < value.size(); i++) {
+        value_data.array_append(SimpleDBus::Holder::create_byte(value[i]));
     }
-}
-void GattCharacteristic1::StopNotify() {
-    if (_notifying) {
-        LOG_F(DEBUG, "%s -> StopNotify", _path.c_str());
-        auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "StopNotify");
-        _conn->send_with_reply_and_block(msg);
-    } else {
-        LOG_F(WARN, "%s was not notifying...", _path.c_str());
+
+    SimpleDBus::Holder options = SimpleDBus::Holder::create_dict();
+    if (type == WriteType::REQUEST) {
+        options.dict_append(SimpleDBus::Holder::Type::STRING, "type", SimpleDBus::Holder::create_string("request"));
+    } else if (type == WriteType::COMMAND) {
+        options.dict_append(SimpleDBus::Holder::Type::STRING, "type", SimpleDBus::Holder::create_string("command"));
     }
-}
 
-void GattCharacteristic1::WriteValue(SimpleDBus::Holder value, SimpleDBus::Holder options) {
-    LOG_F(DEBUG, "%s -> WriteValue", _path.c_str());
-    auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "WriteValue");
-    msg.append_argument(value, "ay");
+    auto msg = create_method_call("WriteValue");
+    msg.append_argument(value_data, "ay");
     msg.append_argument(options, "a{sv}");
     _conn->send_with_reply_and_block(msg);
 }
 
-SimpleDBus::Holder GattCharacteristic1::ReadValue(SimpleDBus::Holder options) {
-    LOG_F(DEBUG, "%s -> ReadValue", _path.c_str());
-    auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "ReadValue");
+ByteArray GattCharacteristic1::ReadValue() {
+    auto msg = create_method_call("ReadValue");
+
+    // NOTE: ReadValue requires an additional argument, which currently is not supported
+    SimpleDBus::Holder options = SimpleDBus::Holder::create_dict();
     msg.append_argument(options, "a{sv}");
+
     SimpleDBus::Message reply_msg = _conn->send_with_reply_and_block(msg);
     SimpleDBus::Holder value = reply_msg.extract();
-    return value;
-}
+    update_value(value);
 
-void GattCharacteristic1::write_request(const uint8_t* data, uint16_t length) {
-    SimpleDBus::Holder value = SimpleDBus::Holder::create_array();
-    for (uint16_t i = 0; i < length; i++) {
-        value.array_append(SimpleDBus::Holder::create_byte(data[i]));
-    }
-    SimpleDBus::Holder options = SimpleDBus::Holder::create_dict();
-    options.dict_append(SimpleDBus::Holder::Type::STRING, "type", SimpleDBus::Holder::create_string("request"));
-    WriteValue(value, options);
+    return Value();
 }
 
-void GattCharacteristic1::write_command(const uint8_t* data, uint16_t length) {
-    SimpleDBus::Holder value = SimpleDBus::Holder::create_array();
-    for (uint16_t i = 0; i < length; i++) {
-        value.array_append(SimpleDBus::Holder::create_byte(data[i]));
-    }
-    SimpleDBus::Holder options = SimpleDBus::Holder::create_dict();
-    options.dict_append(SimpleDBus::Holder::Type::STRING, "type", SimpleDBus::Holder::create_string("command"));
-    WriteValue(value, options);
+std::string GattCharacteristic1::UUID() {
+    // As the UUID property doesn't change, we can cache it
+    std::scoped_lock lock(_property_update_mutex);
+    return _uuid;
 }
 
-bool GattCharacteristic1::Property_Notifying() {
-    auto value = Get(_interface_name, "Notifying");
-    add_option("Notifying", value);
-    return _notifying;
+ByteArray GattCharacteristic1::Value() {
+    std::scoped_lock lock(_property_update_mutex);
+    return _value;
 }
 
-std::vector<uint8_t> GattCharacteristic1::Property_Value() {
-    auto value = Get(_interface_name, "Value");
-    add_option("Value", value);
-    return _value;
+bool GattCharacteristic1::Notifying(bool refresh) {
+    if (refresh) {
+        property_refresh("Notifying");
+    }
+
+    std::scoped_lock lock(_property_update_mutex);
+    return _properties["Notifying"].get_boolean();
 }
 
-void GattCharacteristic1::Action_StartNotify() {
-    LOG_F(DEBUG, "%s -> StartNotify", _path.c_str());
-    auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "StartNotify");
-    _conn->send_with_reply_and_block(msg);
+void GattCharacteristic1::property_changed(std::string option_name) {
+    if (option_name == "UUID") {
+        std::scoped_lock lock(_property_update_mutex);
+        _uuid = _properties["UUID"].get_string();
+    } else if (option_name == "Value") {
+        update_value(_properties["Value"]);
+        OnValueChanged();
+    }
 }
 
-void GattCharacteristic1::Action_StopNotify() {
-    LOG_F(DEBUG, "%s -> StopNotify", _path.c_str());
-    auto msg = SimpleDBus::Message::create_method_call("org.bluez", _path, _interface_name, "StopNotify");
-    _conn->send_with_reply_and_block(msg);
+void GattCharacteristic1::update_value(SimpleDBus::Holder& new_value) {
+    std::scoped_lock lock(_property_update_mutex);
+    auto value_array = new_value.get_array();
+
+    char* value_data = new char[value_array.size()];
+    for (unsigned int i = 0; i < value_array.size(); i++) {
+        value_data[i] = value_array[i].get_byte();
+    }
+    _value = ByteArray(value_data, value_array.size());
+    delete[] value_data;
 }
diff --git a/third_party/SimpleBluez/src/interfaces/GattCharacteristic1.h b/third_party/SimpleBluez/src/interfaces/GattCharacteristic1.h
deleted file mode 100644
index 8e9b82929..000000000
--- a/third_party/SimpleBluez/src/interfaces/GattCharacteristic1.h
+++ /dev/null
@@ -1,45 +0,0 @@
-#pragma once
-
-#include "simpledbus/SimpleDBus.h"
-
-#include <string>
-#include <vector>
-
-class GattCharacteristic1 : public SimpleDBus::Interfaces::PropertyHandler, public SimpleDBus::Properties {
-  private:
-    static const std::string _interface_name;
-
-    SimpleDBus::Connection* _conn;
-    std::string _path;
-
-    std::string _uuid;
-    std::vector<uint8_t> _value;
-    bool _notifying;
-
-    void add_option(std::string option_name, SimpleDBus::Holder value);
-    void remove_option(std::string option_name);
-
-  public:
-    GattCharacteristic1(SimpleDBus::Connection* conn, std::string path);
-    ~GattCharacteristic1();
-
-    void StartNotify();
-    void StopNotify();
-
-    void WriteValue(SimpleDBus::Holder value, SimpleDBus::Holder options);
-    SimpleDBus::Holder ReadValue(SimpleDBus::Holder options);
-
-    std::function<void(std::vector<uint8_t> new_value)> ValueChanged;
-
-    void write_request(const uint8_t* data, uint16_t length);
-    void write_command(const uint8_t* data, uint16_t length);
-
-    std::string get_uuid();
-    std::vector<uint8_t> get_value();
-
-    void Action_StartNotify();
-    void Action_StopNotify();
-
-    bool Property_Notifying();
-    std::vector<uint8_t> Property_Value();
-};
diff --git a/third_party/SimpleBluez/src/interfaces/GattService1.cpp b/third_party/SimpleBluez/src/interfaces/GattService1.cpp
index c922feb8b..abb7c998a 100644
--- a/third_party/SimpleBluez/src/interfaces/GattService1.cpp
+++ b/third_party/SimpleBluez/src/interfaces/GattService1.cpp
@@ -1,19 +1,19 @@
-#include "GattService1.h"
+#include "simplebluez/interfaces/GattService1.h"
 
-#include <iostream>
+using namespace SimpleBluez;
 
-GattService1::GattService1(SimpleDBus::Connection* conn, std::string path) : _conn(conn), _path(path), Properties{conn, "org.bluez", path}, PropertyHandler(path) {
-    // std::cout << "Creating org.bluez.GattService1: " << path << std::endl;
-}
+GattService1::GattService1(std::shared_ptr<SimpleDBus::Connection> conn, std::string path)
+    : SimpleDBus::Interface(conn, "org.bluez", path, "org.bluez.GattService1") {}
 
-GattService1::~GattService1() {}
+std::string GattService1::UUID() {
+    // As the UUID property doesn't change, we can cache it
+    std::scoped_lock lock(_property_update_mutex);
+    return _uuid;
+}
 
-void GattService1::add_option(std::string option_name, SimpleDBus::Holder value) {
+void GattService1::property_changed(std::string option_name) {
     if (option_name == "UUID") {
-        _uuid = value.get_string();
+        std::scoped_lock lock(_property_update_mutex);
+        _uuid = _properties["UUID"].get_string();
     }
 }
-
-void GattService1::remove_option(std::string option_name) {}
-
-std::string GattService1::get_uuid() { return _uuid; }
diff --git a/third_party/SimpleBluez/src/interfaces/GattService1.h b/third_party/SimpleBluez/src/interfaces/GattService1.h
deleted file mode 100644
index f15b9dad8..000000000
--- a/third_party/SimpleBluez/src/interfaces/GattService1.h
+++ /dev/null
@@ -1,22 +0,0 @@
-#pragma once
-
-#include "simpledbus/SimpleDBus.h"
-
-#include <string>
-
-class GattService1 : public SimpleDBus::Interfaces::PropertyHandler, public SimpleDBus::Properties {
-  private:
-    SimpleDBus::Connection* _conn;
-    std::string _path;
-
-    std::string _uuid;
-
-    void add_option(std::string option_name, SimpleDBus::Holder value);
-    void remove_option(std::string option_name);
-
-  public:
-    GattService1(SimpleDBus::Connection* conn, std::string path);
-    ~GattService1();
-
-    std::string get_uuid();
-};
diff --git a/third_party/SimpleDBus/CHANGELOG.md b/third_party/SimpleDBus/CHANGELOG.md
index c49d12dc6..5fef098c3 100644
--- a/third_party/SimpleDBus/CHANGELOG.md
+++ b/third_party/SimpleDBus/CHANGELOG.md
@@ -4,13 +4,24 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [2.0.0] - 2021-XX-XX
+## [2.0.0] - 2021-12-28
 
 ### Added
 - Testing framework.
 - Exceptions for error detection and handling.
+- Proxy object to generalize path and message handling.
+- Interface object with advanced features to generalize common functionality.
+- Generic callback class to generalize callback functionality.
 
 ### Changed
 - Log levels use now a sane naming convention.
 - Default logging level can be set during build time.
 - Holders containing dictionaries can now support any generic key type.
+- Message types are now part of the class and not a separate enum.
+
+### Removed
+- Legacy implementations of Property and Introspection.
+
+### Fixed
+- Interface access is now thread-safe.
+- Proxy access is now thread-safe.
diff --git a/third_party/SimpleDBus/README.md b/third_party/SimpleDBus/README.md
index e95683494..66ca3d25c 100644
--- a/third_party/SimpleDBus/README.md
+++ b/third_party/SimpleDBus/README.md
@@ -45,7 +45,33 @@ make -j
 
 ### Build tests
 To build and run unit and integration tests, the following packages are required:
-`sudo apt install libgtest-dev python3-dev`
+`sudo apt install libgtest-dev libgmock-dev python3-dev`
+
+#### Address Sanitizer
+In order to run tests with Address Sanitizer, CMake needs to be called with
+the following option: `-DSIMPLEDBUS_SANITIZE=Address`. It is also important to
+set the environment variable `PYTHONMALLOC=malloc` to prevent Python's memory
+allocator from triggering false positives.
+
+#### Thread Sanitizer
+In order to run tests with Thread Sanitizer, CMake needs to be called with
+the following option: `-DSIMPLEDBUS_SANITIZE=Thread`.
+
+## Architecture
+The following notes provide an overview of the architecture of some of the higher-level
+classes in the library, as to facilitate their understanding.
+
+### Interface
+- In order to simplify the routing of messages, all interfaces are assumed to have
+  properties, thus skipping the need to have a special implementation of 
+  org.freedesktop.DBus.Properties.
+- All properties are stored in the holder in which they came from. This is not the
+  most efficient way of handling properties, but it is the one that minimizes the
+  necessary code for children of the Interface class.
+
+### Proxy
+- Messages for org.freedesktop.DBus.Properties are automatically handled by the
+  Proxy class.
 
 ## Security
 
diff --git a/third_party/SimpleDBus/examples/notification/main.cpp b/third_party/SimpleDBus/examples/notification/main.cpp
index 3581163db..17750e991 100644
--- a/third_party/SimpleDBus/examples/notification/main.cpp
+++ b/third_party/SimpleDBus/examples/notification/main.cpp
@@ -1,4 +1,5 @@
-#include <simpledbus/SimpleDBus.h>
+#include <simpledbus/base/Connection.h>
+#include <simpledbus/base/Message.h>
 
 int main(int argc, char* argv[]) {
     SimpleDBus::Connection conn(DBUS_BUS_SESSION);
diff --git a/third_party/SimpleDBus/include/simpledbus/SimpleDBus.h b/third_party/SimpleDBus/include/simpledbus/SimpleDBus.h
deleted file mode 100644
index 891d4bcf5..000000000
--- a/third_party/SimpleDBus/include/simpledbus/SimpleDBus.h
+++ /dev/null
@@ -1,10 +0,0 @@
-#pragma once
-
-#include "base/Connection.h"
-#include "base/Message.h"
-
-#include "interfaces/PropertyHandler.h"
-
-#include "common/ObjectManager.h"
-#include "common/Properties.h"
-#include "common/Introspectable.h"
\ No newline at end of file
diff --git a/third_party/SimpleDBus/include/simpledbus/advanced/Callback.h b/third_party/SimpleDBus/include/simpledbus/advanced/Callback.h
new file mode 100644
index 000000000..c8ec92e76
--- /dev/null
+++ b/third_party/SimpleDBus/include/simpledbus/advanced/Callback.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <atomic>
+#include <functional>
+#include <memory>
+#include <mutex>
+
+namespace SimpleDBus {
+
+template <typename T, typename... params>
+class Callback {
+  public:
+    void load(T callback);
+    void unload();
+
+    bool is_loaded();
+    bool is_running();
+    bool is_delete_requested();
+    
+    void operator()(params... args);
+
+  protected:
+    T _callback;
+    std::recursive_mutex _mutex;
+    
+    std::atomic_bool _is_loaded;
+    std::atomic_bool _is_running{false};
+    std::atomic_bool _delete_requested{false};
+};
+
+}  // namespace SimpleDBus
diff --git a/third_party/SimpleDBus/include/simpledbus/advanced/Interface.h b/third_party/SimpleDBus/include/simpledbus/advanced/Interface.h
new file mode 100644
index 000000000..fa69479ce
--- /dev/null
+++ b/third_party/SimpleDBus/include/simpledbus/advanced/Interface.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include <simpledbus/base/Connection.h>
+
+#include <atomic>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <string>
+
+namespace SimpleDBus {
+
+class Interface {
+  public:
+    Interface(std::shared_ptr<Connection> conn, const std::string& bus_name, const std::string& path,
+              const std::string& interface_name);
+
+    virtual ~Interface() = default;
+
+    // ----- LIFE CYCLE -----
+    void load(Holder options);
+    void unload();
+    bool is_loaded() const;
+
+    // ----- METHODS -----
+    Message create_method_call(const std::string& method_name);
+
+    // ----- PROPERTIES -----
+    virtual void property_changed(std::string option_name);
+
+    Holder property_get_all();
+    Holder property_get(const std::string& property_name);
+    void property_set(const std::string& property_name, const Holder& value);
+    void property_refresh(const std::string& property_name);
+
+    // ----- SIGNALS -----
+    void signal_property_changed(Holder changed_properties, Holder invalidated_properties);
+
+    // ----- MESSAGES -----
+    virtual void message_handle(Message msg);
+
+  protected:
+    std::atomic_bool _loaded{true};
+
+    std::string _path;
+    std::string _bus_name;
+    std::string _interface_name;
+    std::shared_ptr<Connection> _conn;
+
+    std::recursive_mutex _property_update_mutex;
+    std::map<std::string, bool> _property_valid_map;
+
+    /**
+     * @brief Dictionary containing all properties.
+     *
+     * @note: When accessing this object, the _property_update_mutex must be locked.
+     */
+    std::map<std::string, Holder> _properties;
+};
+
+}  // namespace SimpleDBus
diff --git a/third_party/SimpleDBus/include/simpledbus/advanced/Proxy.h b/third_party/SimpleDBus/include/simpledbus/advanced/Proxy.h
new file mode 100644
index 000000000..71ad28fd2
--- /dev/null
+++ b/third_party/SimpleDBus/include/simpledbus/advanced/Proxy.h
@@ -0,0 +1,77 @@
+#pragma once
+
+#include <simpledbus/advanced/Callback.h>
+#include <simpledbus/advanced/Interface.h>
+
+#include <memory>
+#include <mutex>
+#include <string>
+
+namespace SimpleDBus {
+
+class Proxy {
+  public:
+    Proxy(std::shared_ptr<Connection> conn, const std::string& bus_name, const std::string& path);
+    virtual ~Proxy();
+
+    std::string path() const;
+
+    bool path_exists(const std::string& path);
+    std::shared_ptr<Proxy> path_get(const std::string& path);
+
+    bool interface_exists(const std::string& name);
+    std::shared_ptr<Interface> interface_get(const std::string& name);
+
+    const std::map<std::string, std::shared_ptr<Proxy>>& children();
+    const std::map<std::string, std::shared_ptr<Interface>>& interfaces();
+
+    virtual std::shared_ptr<Interface> interfaces_create(const std::string& name);
+    virtual std::shared_ptr<Proxy> path_create(const std::string& path);
+
+    // ----- INTROSPECTION -----
+    std::string introspect();
+
+    // ----- INTERFACE HANDLING -----
+    size_t interfaces_count();
+    bool interfaces_loaded();
+    void interfaces_load(Holder managed_interfaces);
+    void interfaces_reload(Holder managed_interfaces);
+    void interfaces_unload(Holder removed_interfaces);
+
+    // ----- CHILD HANDLING -----
+    void path_add(const std::string& path, Holder managed_interfaces);
+    bool path_remove(const std::string& path, Holder removed_interfaces);
+    bool path_prune();
+
+    // ----- MESSAGE HANDLING -----
+    void message_forward(Message& msg);
+
+    // ----- CALLBACKS -----
+    Callback<std::function<void(std::string)>, std::string> on_child_created;
+    Callback<std::function<void(std::string)>, std::string> on_child_signal_received;
+
+    // ----- TEMPLATE METHODS -----
+    template <typename T>
+    std::vector<std::shared_ptr<T>> children_casted() {
+        std::vector<std::shared_ptr<T>> result;
+        std::scoped_lock lock(_child_access_mutex);
+        for (auto& [path, child] : _children) {
+            result.push_back(std::dynamic_pointer_cast<T>(child));
+        }
+        return result;
+    }
+
+  protected:
+    std::string _path;
+    std::string _bus_name;
+
+    std::shared_ptr<Connection> _conn;
+
+    std::map<std::string, std::shared_ptr<Interface>> _interfaces;
+    std::map<std::string, std::shared_ptr<Proxy>> _children;
+
+    std::recursive_mutex _interface_access_mutex;
+    std::recursive_mutex _child_access_mutex;
+};
+
+}  // namespace SimpleDBus
diff --git a/third_party/SimpleDBus/include/simpledbus/base/Exceptions.h b/third_party/SimpleDBus/include/simpledbus/base/Exceptions.h
index 65b9968d9..f1f4f46a7 100644
--- a/third_party/SimpleDBus/include/simpledbus/base/Exceptions.h
+++ b/third_party/SimpleDBus/include/simpledbus/base/Exceptions.h
@@ -1,6 +1,5 @@
 #pragma once
 
-#include <fmt/core.h>
 #include <stdexcept>
 #include <string>
 
@@ -34,6 +33,24 @@ class SendFailed : public BaseException {
     std::string _message;
 };
 
+class InterfaceNotFoundException : public BaseException {
+  public:
+    InterfaceNotFoundException(const std::string& path, const std::string& interface);
+    const char* what() const noexcept override;
+
+  private:
+    std::string _message;
+};
+
+class PathNotFoundException : public BaseException {
+  public:
+    PathNotFoundException(const std::string& path, const std::string& subpath);
+    const char* what() const noexcept override;
+
+  private:
+    std::string _message;
+};
+
 }  // namespace Exception
 
 }  // namespace SimpleDBus
diff --git a/third_party/SimpleDBus/include/simpledbus/base/Holder.h b/third_party/SimpleDBus/include/simpledbus/base/Holder.h
index 6b9e947e2..177c0ea67 100644
--- a/third_party/SimpleDBus/include/simpledbus/base/Holder.h
+++ b/third_party/SimpleDBus/include/simpledbus/base/Holder.h
@@ -15,6 +15,9 @@ class Holder {
     Holder();
     ~Holder();
 
+    bool operator!=(const Holder& rhs) const;
+    bool operator==(const Holder& rhs) const;
+
     typedef enum {
         NONE,
         BYTE,
@@ -33,7 +36,7 @@ class Holder {
         DICT
     } Type;
 
-    Type type();
+    Type type() const;
     std::string represent();
     std::string signature();
 
@@ -46,37 +49,36 @@ class Holder {
     static Holder create_int64(int64_t value);
     static Holder create_uint64(uint64_t value);
     static Holder create_double(double value);
-    static Holder create_string(const char* str);
-    static Holder create_object_path(const char* str);
-    static Holder create_signature(const char* str);
+    static Holder create_string(const std::string& str);
+    static Holder create_object_path(const std::string& str);
+    static Holder create_signature(const std::string& str);
     static Holder create_array();
     static Holder create_dict();
 
-    std::any get_contents();
-    bool get_boolean();
-    uint8_t get_byte();
-    int16_t get_int16();
-    uint16_t get_uint16();
-    int32_t get_int32();
-    uint32_t get_uint32();
-    int64_t get_int64();
-    uint64_t get_uint64();
-    double get_double();
-    std::string get_string();
-    std::string get_object_path();
-    std::string get_signature();
-    std::vector<Holder> get_array();
-    std::map<uint8_t, Holder> get_dict_uint8();
-    std::map<uint16_t, Holder> get_dict_uint16();
-    std::map<uint32_t, Holder> get_dict_uint32();
-    std::map<uint64_t, Holder> get_dict_uint64();
-    std::map<int8_t, Holder> get_dict_int8();
-    std::map<int16_t, Holder> get_dict_int16();
-    std::map<int32_t, Holder> get_dict_int32();
-    std::map<int64_t, Holder> get_dict_int64();
-    std::map<std::string, Holder> get_dict_string();
-    std::map<std::string, Holder> get_dict_object_path();
-    std::map<std::string, Holder> get_dict_signature();
+    std::any get_contents() const;
+    bool get_boolean() const;
+    uint8_t get_byte() const;
+    int16_t get_int16() const;
+    uint16_t get_uint16() const;
+    int32_t get_int32() const;
+    uint32_t get_uint32() const;
+    int64_t get_int64() const;
+    uint64_t get_uint64() const;
+    double get_double() const;
+    std::string get_string() const;
+    std::string get_object_path() const;
+    std::string get_signature() const;
+    std::vector<Holder> get_array() const;
+    std::map<uint8_t, Holder> get_dict_uint8() const;
+    std::map<uint16_t, Holder> get_dict_uint16() const;
+    std::map<uint32_t, Holder> get_dict_uint32() const;
+    std::map<uint64_t, Holder> get_dict_uint64() const;
+    std::map<int16_t, Holder> get_dict_int16() const;
+    std::map<int32_t, Holder> get_dict_int32() const;
+    std::map<int64_t, Holder> get_dict_int64() const;
+    std::map<std::string, Holder> get_dict_string() const;
+    std::map<std::string, Holder> get_dict_object_path() const;
+    std::map<std::string, Holder> get_dict_signature() const;
 
     void dict_append(Type key_type, std::any key, Holder value);
     void array_append(Holder holder);
@@ -99,7 +101,7 @@ class Holder {
     std::string _signature_simple();
 
     template <typename T>
-    std::map<T, Holder> _get_dict(Type key_type);
+    std::map<T, Holder> _get_dict(Type key_type) const;
 
     static std::string _signature_type(Type type);
     static std::string _represent_type(Type type, std::any value);
diff --git a/third_party/SimpleDBus/include/simpledbus/base/Message.h b/third_party/SimpleDBus/include/simpledbus/base/Message.h
index 5a9466e97..4a763e022 100644
--- a/third_party/SimpleDBus/include/simpledbus/base/Message.h
+++ b/third_party/SimpleDBus/include/simpledbus/base/Message.h
@@ -2,6 +2,7 @@
 
 #include <dbus/dbus.h>
 
+#include <atomic>
 #include <stack>
 #include <string>
 #include <vector>
@@ -14,38 +15,16 @@ namespace SimpleDBus {
 class Connection;
 class Interface;
 
-typedef enum {
-    INVALID = DBUS_MESSAGE_TYPE_INVALID,
-    METHOD_CALL = DBUS_MESSAGE_TYPE_METHOD_CALL,
-    METHOD_RETURN = DBUS_MESSAGE_TYPE_METHOD_RETURN,
-    ERROR = DBUS_MESSAGE_TYPE_ERROR,
-    SIGNAL = DBUS_MESSAGE_TYPE_SIGNAL,
-} MessageType;
-
 class Message {
-  private:
-    friend class Connection;
-
-    static int creation_counter;
-    int indent;
-
-    int _unique_id;
-    DBusMessageIter _iter;
-    bool _iter_initialized;
-    bool _is_extracted;
-    Holder _extracted;
-    DBusMessage* _msg;
-
-    Holder _extract_bytearray(DBusMessageIter* iter);
-    Holder _extract_array(DBusMessageIter* iter);
-    Holder _extract_dict(DBusMessageIter* iter);
-    Holder _extract_generic(DBusMessageIter* iter);
-    void _append_argument(DBusMessageIter* iter, Holder& argument, std::string signature);
-
-    void _invalidate();
-    void _safe_delete();
-
   public:
+    typedef enum {
+        INVALID = DBUS_MESSAGE_TYPE_INVALID,
+        METHOD_CALL = DBUS_MESSAGE_TYPE_METHOD_CALL,
+        METHOD_RETURN = DBUS_MESSAGE_TYPE_METHOD_RETURN,
+        ERROR = DBUS_MESSAGE_TYPE_ERROR,
+        SIGNAL = DBUS_MESSAGE_TYPE_SIGNAL,
+    } Type;
+
     Message();
     Message(DBusMessage* msg);
     Message(Message&& other);                  // Custom move constructor
@@ -68,12 +47,43 @@ class Message {
     std::string get_signature();
     std::string get_interface();
     std::string get_path();
-    MessageType get_type();
+    Type get_type() const;
 
     bool is_signal(std::string interface, std::string signal_name);
 
     static Message create_method_call(std::string bus_name, std::string path, std::string interface,
                                       std::string method);
+
+  private:
+    friend class Connection;
+
+    static std::atomic_int32_t creation_counter;
+    int indent;
+
+    int _unique_id;
+    DBusMessageIter _iter;
+    bool _iter_initialized;
+    bool _is_extracted;
+    Holder _extracted;
+    DBusMessage* _msg;
+
+    Holder _extract_bytearray(DBusMessageIter* iter);
+    Holder _extract_array(DBusMessageIter* iter);
+    Holder _extract_dict(DBusMessageIter* iter);
+    Holder _extract_generic(DBusMessageIter* iter);
+
+    /**
+     * @brief Append argument to the DBus message iterator.
+     * @param iter      DBus message iterator.
+     * @param argument  Argument to append.
+     * @param signature Signature of the argument.
+     */
+    void _append_argument(DBusMessageIter* iter, Holder& argument, std::string signature);
+
+    void _invalidate();
+    void _safe_delete();
+
+    std::vector<Holder> _arguments;
 };
 
-}  // namespace SimpleDBus
\ No newline at end of file
+}  // namespace SimpleDBus
diff --git a/third_party/SimpleDBus/include/simpledbus/base/Path.h b/third_party/SimpleDBus/include/simpledbus/base/Path.h
new file mode 100644
index 000000000..549f3ffcb
--- /dev/null
+++ b/third_party/SimpleDBus/include/simpledbus/base/Path.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <string>
+#include <vector>
+
+namespace SimpleDBus {
+
+class Path {
+  public:
+    static size_t count_elements(const std::string& path);
+    static std::string fetch_elements(const std::string& path, size_t count);
+    static std::vector<std::string> split_elements(const std::string& path);
+
+    static bool is_descendant(const std::string& base, const std::string& path);
+    static bool is_ascendant(const std::string& base, const std::string& path);
+
+    static bool is_child(const std::string& base, const std::string& path);
+    static bool is_parent(const std::string& base, const std::string& path);
+
+    static std::string next_child(const std::string& base, const std::string& path);
+};
+
+}  // namespace SimpleDBus
diff --git a/third_party/SimpleDBus/include/simpledbus/common/Introspectable.h b/third_party/SimpleDBus/include/simpledbus/common/Introspectable.h
deleted file mode 100644
index cac12ba07..000000000
--- a/third_party/SimpleDBus/include/simpledbus/common/Introspectable.h
+++ /dev/null
@@ -1,30 +0,0 @@
-#pragma once
-#include <functional>
-#include <string>
-
-#include <simpledbus/base/Connection.h>
-#include <simpledbus/base/Holder.h>
-#include <simpledbus/base/Message.h>
-
-namespace SimpleDBus {
-
-class Holder;
-class Connection;
-
-class Introspectable {
-  private:
-    const std::string _interface;
-
-    std::string _path;
-    std::string _service;
-    Connection* _conn;
-
-  public:
-    Introspectable(Connection* conn, std::string service, std::string path);
-    virtual ~Introspectable();
-
-    // Names are made matching the ones from the DBus specification
-    Holder Introspect();
-};
-
-}  // namespace SimpleDBus
diff --git a/third_party/SimpleDBus/include/simpledbus/common/Properties.h b/third_party/SimpleDBus/include/simpledbus/common/Properties.h
deleted file mode 100644
index 8c39857aa..000000000
--- a/third_party/SimpleDBus/include/simpledbus/common/Properties.h
+++ /dev/null
@@ -1,32 +0,0 @@
-#pragma once
-#include <functional>
-#include <string>
-
-#include <simpledbus/base/Connection.h>
-#include <simpledbus/base/Holder.h>
-#include <simpledbus/base/Message.h>
-
-namespace SimpleDBus {
-
-class Holder;
-class Connection;
-
-class Properties {
-  private:
-    const std::string _interface;
-
-    std::string _path;
-    std::string _service;
-    Connection* _conn;
-
-  public:
-    Properties(Connection* conn, std::string service, std::string path);
-    virtual ~Properties();
-
-    // Names are made matching the ones from the DBus specification
-    Holder Get(std::string interface, std::string name);
-    Holder GetAll(std::string interface);
-    void Set(std::string interface, std::string name, Holder value);
-};
-
-}  // namespace SimpleDBus
diff --git a/third_party/SimpleDBus/include/simpledbus/common/ObjectManager.h b/third_party/SimpleDBus/include/simpledbus/interfaces/ObjectManager.h
similarity index 52%
rename from third_party/SimpleDBus/include/simpledbus/common/ObjectManager.h
rename to third_party/SimpleDBus/include/simpledbus/interfaces/ObjectManager.h
index ceada4a84..7dc22c7e6 100644
--- a/third_party/SimpleDBus/include/simpledbus/common/ObjectManager.h
+++ b/third_party/SimpleDBus/include/simpledbus/interfaces/ObjectManager.h
@@ -1,27 +1,15 @@
 #pragma once
-#include <functional>
-#include <string>
-
-#include <simpledbus/base/Connection.h>
-#include <simpledbus/base/Holder.h>
-#include <simpledbus/base/Message.h>
-
-namespace SimpleDBus {
 
-class Holder;
-class Connection;
+#include <simpledbus/advanced/Interface.h>
 
-class ObjectManager {
-  private:
-    const std::string _interface;
+#include <functional>
 
-    std::string _path;
-    std::string _service;
-    Connection* _conn;
+namespace SimpleDBus {
 
+class ObjectManager : public Interface {
   public:
-    ObjectManager(Connection* conn, std::string service, std::string path);
-    ~ObjectManager();
+    ObjectManager(std::shared_ptr<Connection> conn, std::string bus_name, std::string path);
+    virtual ~ObjectManager() = default;
 
     // Names are made matching the ones from the DBus specification
     Holder GetManagedObjects(bool use_callbacks = false);
@@ -29,6 +17,8 @@ class ObjectManager {
     std::function<void(std::string path, Holder options)> InterfacesRemoved;
 
     bool process_received_signal(Message& message);
+
+    void message_handle(Message msg) override;
 };
 
 }  // namespace SimpleDBus
diff --git a/third_party/SimpleDBus/include/simpledbus/interfaces/PropertyHandler.h b/third_party/SimpleDBus/include/simpledbus/interfaces/PropertyHandler.h
deleted file mode 100644
index a02a136dd..000000000
--- a/third_party/SimpleDBus/include/simpledbus/interfaces/PropertyHandler.h
+++ /dev/null
@@ -1,34 +0,0 @@
-#pragma once
-
-#include <simpledbus/base/Holder.h>
-#include <simpledbus/base/Message.h>
-
-#include <functional>
-#include <string>
-
-namespace SimpleDBus {
-
-namespace Interfaces {
-
-class PropertyHandler {
-  private:
-    std::string _path;
-
-  protected:
-    virtual void add_option(std::string option_name, Holder value) = 0;
-    virtual void remove_option(std::string option_name) = 0;
-
-  public:
-    PropertyHandler(std::string path);
-    virtual ~PropertyHandler();
-
-    void set_options(Holder changed_properties);
-    void set_options(Holder changed_properties, Holder invalidated_properties);
-
-    std::function<void(std::string interface, Holder changed, Holder invalidated)> PropertiesChanged;
-    bool process_received_signal(Message& message);
-
-};
-}  // namespace Interfaces
-
-}  // namespace SimpleDBus
diff --git a/third_party/SimpleDBus/src/advanced/Callback.cpp b/third_party/SimpleDBus/src/advanced/Callback.cpp
new file mode 100644
index 000000000..8c20e1862
--- /dev/null
+++ b/third_party/SimpleDBus/src/advanced/Callback.cpp
@@ -0,0 +1,70 @@
+#include <simpledbus/advanced/Callback.h>
+
+using namespace SimpleDBus;
+
+template <typename T, typename... params>
+void Callback<T, params...>::load(T callback) {
+    _mutex.lock();
+    _callback = callback;
+    _mutex.unlock();
+    if (callback) {
+        _is_loaded = true;
+    }
+}
+
+template <typename T, typename... params>
+void Callback<T, params...>::unload() {
+    _is_loaded = false;
+    if (_is_running) {
+        _delete_requested = true;
+    } else {
+        _mutex.lock();
+        _callback = nullptr;
+        _delete_requested = false;
+        _mutex.unlock();
+    }
+}
+
+template <typename T, typename... params>
+void Callback<T, params...>::operator()(params... args) {
+    _is_running = true;
+    if (_is_loaded && !_delete_requested) {
+        _mutex.lock();
+        if (_callback) {
+            _callback(args...);
+        }
+        _mutex.unlock();
+    }
+    if (_delete_requested) {
+        _mutex.lock();
+        _callback = nullptr;
+        _mutex.unlock();
+        _delete_requested = false;
+    }
+
+    _is_running = false;
+}
+
+template <typename T, typename... params>
+bool Callback<T, params...>::is_loaded() {
+    return _is_loaded;
+}
+
+template <typename T, typename... params>
+bool Callback<T, params...>::is_running() {
+    return _is_running;
+}
+
+template <typename T, typename... params>
+bool Callback<T, params...>::is_delete_requested() {
+    return _delete_requested;
+}
+
+// Instantiations
+
+namespace SimpleDBus {
+
+template class Callback<std::function<void()>>;
+template class Callback<std::function<void(std::string)>, std::string>;
+
+}  // namespace SimpleDBus
diff --git a/third_party/SimpleDBus/src/advanced/Interface.cpp b/third_party/SimpleDBus/src/advanced/Interface.cpp
new file mode 100644
index 000000000..be73fa05d
--- /dev/null
+++ b/third_party/SimpleDBus/src/advanced/Interface.cpp
@@ -0,0 +1,134 @@
+#include <simpledbus/advanced/Interface.h>
+#include <simpledbus/base/Exceptions.h>
+
+using namespace SimpleDBus;
+
+Interface::Interface(std::shared_ptr<Connection> conn, const std::string& bus_name, const std::string& path,
+                     const std::string& interface_name)
+    : _conn(conn), _bus_name(bus_name), _path(path), _interface_name(interface_name), _loaded(true) {}
+
+// ----- LIFE CYCLE -----
+
+void Interface::load(Holder options) {
+    _property_update_mutex.lock();
+    auto changed_options = options.get_dict_string();
+    for (auto& [name, value] : changed_options) {
+        _properties[name] = value;
+        _property_valid_map[name] = true;
+    }
+    _property_update_mutex.unlock();
+
+    // Notify the user of all properties that have been created.
+    for (auto& [name, value] : changed_options) {
+        property_changed(name);
+    }
+
+    _loaded = true;
+}
+
+void Interface::unload() { _loaded = false; }
+
+bool Interface::is_loaded() const { return _loaded; }
+
+// ----- METHODS -----
+
+Message Interface::create_method_call(const std::string& method_name) {
+    return Message::create_method_call(_bus_name, _path, _interface_name, method_name);
+}
+
+// ----- PROPERTIES -----
+
+Holder Interface::property_get_all() {
+    Message query_msg = Message::create_method_call(_bus_name, _path, "org.freedesktop.DBus.Properties", "GetAll");
+
+    Holder h_interface = Holder::create_string(_interface_name);
+    query_msg.append_argument(h_interface, "s");
+
+    Message reply_msg = _conn->send_with_reply_and_block(query_msg);
+    Holder result = reply_msg.extract();
+    return result;
+}
+
+Holder Interface::property_get(const std::string& property_name) {
+    Message query_msg = Message::create_method_call(_bus_name, _path, "org.freedesktop.DBus.Properties", "Get");
+
+    Holder h_interface = Holder::create_string(_interface_name);
+    query_msg.append_argument(h_interface, "s");
+
+    Holder h_name = Holder::create_string(property_name);
+    query_msg.append_argument(h_name, "s");
+
+    Message reply_msg = _conn->send_with_reply_and_block(query_msg);
+    Holder result = reply_msg.extract();
+    return result;
+}
+
+void Interface::property_set(const std::string& property_name, const Holder& value) {
+    Message query_msg = Message::create_method_call(_bus_name, _path, "org.freedesktop.DBus.Properties", "Set");
+
+    Holder h_interface = Holder::create_string(_interface_name);
+    query_msg.append_argument(h_interface, "s");
+
+    Holder h_name = Holder::create_string(property_name);
+    query_msg.append_argument(h_name, "s");
+
+    query_msg.append_argument(value, "v");
+
+    _conn->send_with_reply_and_block(query_msg);
+}
+
+void Interface::property_refresh(const std::string& property_name) {
+    if (!_loaded || !_property_valid_map[property_name]) {
+        return;
+    }
+
+    bool cb_property_changed_required = false;
+    _property_update_mutex.lock();
+    try {
+        // NOTE: Due to the way Bluez handles underlying devices and the fact that
+        //       they can be removed before the callback reaches back (race condition),
+        //       `property_get` can sometimes fail. Because of this, the update
+        //       statement is surrounded by a try-catch statement.
+        Holder property_latest = property_get(property_name);
+        _property_valid_map[property_name] = true;
+        if (_properties[property_name] != property_latest) {
+            _properties[property_name] = property_latest;
+            cb_property_changed_required = true;
+        }
+    } catch (const Exception::SendFailed& e) {
+        _property_valid_map[property_name] = true;
+    }
+    _property_update_mutex.unlock();
+
+    if (cb_property_changed_required) {
+        property_changed(property_name);
+    }
+}
+
+void Interface::property_changed(std::string option_name) {}
+
+// ----- SIGNALS -----
+
+void Interface::signal_property_changed(Holder changed_properties, Holder invalidated_properties) {
+    _property_update_mutex.lock();
+    auto changed_options = changed_properties.get_dict_string();
+    for (auto& [name, value] : changed_options) {
+        _properties[name] = value;
+        _property_valid_map[name] = true;
+    }
+
+    auto removed_options = invalidated_properties.get_array();
+    for (auto& removed_option : removed_options) {
+        _property_valid_map[removed_option.get_string()] = false;
+    }
+    _property_update_mutex.unlock();
+
+    // Once all properties have been updated, notify the user.
+    for (auto& [name, value] : changed_options) {
+        property_changed(name);
+    }
+}
+
+// ----- MESSAGES -----
+
+void Interface::message_handle(Message msg) {}
diff --git a/third_party/SimpleDBus/src/advanced/Proxy.cpp b/third_party/SimpleDBus/src/advanced/Proxy.cpp
new file mode 100644
index 000000000..7b31198b3
--- /dev/null
+++ b/third_party/SimpleDBus/src/advanced/Proxy.cpp
@@ -0,0 +1,265 @@
+#include "simpledbus/advanced/Proxy.h"
+
+#include <simpledbus/base/Exceptions.h>
+#include <simpledbus/base/Path.h>
+#include <algorithm>
+
+using namespace SimpleDBus;
+
+Proxy::Proxy(std::shared_ptr<Connection> conn, const std::string& bus_name, const std::string& path)
+    : _conn(conn), _bus_name(bus_name), _path(path) {}
+
+Proxy::~Proxy() {
+    on_child_created.unload();
+    on_child_signal_received.unload();
+}
+
+std::shared_ptr<Interface> Proxy::interfaces_create(const std::string& name) {
+    return std::make_unique<Interface>(_conn, _bus_name, _path, name);
+}
+
+std::shared_ptr<Proxy> Proxy::path_create(const std::string& path) {
+    return std::make_shared<Proxy>(_conn, _bus_name, path);
+}
+
+std::string Proxy::path() const { return _path; }
+
+const std::map<std::string, std::shared_ptr<Proxy>>& Proxy::children() { return _children; }
+
+const std::map<std::string, std::shared_ptr<Interface>>& Proxy::interfaces() { return _interfaces; }
+
+// ----- INTROSPECTION -----
+std::string Proxy::introspect() {
+    auto query_msg = Message::create_method_call(_bus_name, _path, "org.freedesktop.DBus.Introspectable", "Introspect");
+    auto reply_msg = _conn->send_with_reply_and_block(query_msg);
+    return reply_msg.extract().get_string();
+}
+
+// ----- INTERFACE HANDLING -----
+
+bool Proxy::interface_exists(const std::string& name) {
+    std::scoped_lock lock(_interface_access_mutex);
+    return _interfaces.find(name) != _interfaces.end();
+}
+
+std::shared_ptr<Interface> Proxy::interface_get(const std::string& name) {
+    std::scoped_lock lock(_interface_access_mutex);
+    if (!interface_exists(name)) {
+        throw Exception::InterfaceNotFoundException(_path, name);
+    }
+    return _interfaces[name];
+}
+
+size_t Proxy::interfaces_count() {
+    size_t count = 0;
+    std::scoped_lock lock(_interface_access_mutex);
+    for (auto& [iface_name, interface] : _interfaces) {
+        if (interface->is_loaded()) {
+            count++;
+        }
+    }
+    return count;
+}
+
+void Proxy::interfaces_load(Holder managed_interfaces) {
+    auto managed_interface = managed_interfaces.get_dict_string();
+
+    std::scoped_lock lock(_interface_access_mutex);
+    for (auto& [iface_name, options] : managed_interface) {
+        // If the interface has not been loaded, load it
+        if (!interface_exists(iface_name)) {
+            _interfaces.emplace(std::make_pair(iface_name, interfaces_create(iface_name)));
+        }
+
+        _interfaces[iface_name]->load(options);
+    }
+}
+
+void Proxy::interfaces_reload(Holder managed_interfaces) {
+    std::scoped_lock lock(_interface_access_mutex);
+    for (auto& [iface_name, interface] : _interfaces) {
+        interface->unload();
+    }
+
+    interfaces_load(managed_interfaces);
+}
+
+void Proxy::interfaces_unload(SimpleDBus::Holder removed_interfaces) {
+    std::scoped_lock lock(_interface_access_mutex);
+    for (auto& option : removed_interfaces.get_array()) {
+        std::string iface_name = option.get_string();
+        if (interface_exists(iface_name)) {
+            _interfaces[iface_name]->unload();
+        }
+    }
+}
+
+bool Proxy::interfaces_loaded() {
+    std::scoped_lock lock(_interface_access_mutex);
+    for (auto& [iface_name, interface] : _interfaces) {
+        if (interface->is_loaded()) {
+            return true;
+        }
+    }
+    return false;
+}
+
+// ----- CHILD HANDLING -----
+
+bool Proxy::path_exists(const std::string& path) {
+    std::scoped_lock lock(_child_access_mutex);
+    return _children.find(path) != _children.end();
+}
+
+std::shared_ptr<Proxy> Proxy::path_get(const std::string& path) {
+    std::scoped_lock lock(_child_access_mutex);
+    if (!path_exists(path)) {
+        throw Exception::PathNotFoundException(_path, path);
+    }
+    return _children[path];
+}
+
+void Proxy::path_add(const std::string& path, SimpleDBus::Holder managed_interfaces) {
+    // If the path is not a child of the current path, then we can't add it.
+    if (!Path::is_descendant(_path, path)) {
+        // TODO: Should an exception be thrown here?
+        return;
+    }
+
+    // If the path is already in the map, perform a reload of all interfaces.
+    if (path_exists(path)) {
+        path_get(path)->interfaces_load(managed_interfaces);
+        return;
+    }
+
+    // As children will be extensively accessed, we need to lock the child access mutex.
+    std::scoped_lock lock(_child_access_mutex);
+
+    if (Path::is_child(_path, path)) {
+        // If the path is a direct child of the proxy path, create a new proxy for it.
+        std::shared_ptr<Proxy> child = path_create(path);
+        child->interfaces_load(managed_interfaces);
+        _children.emplace(std::make_pair(path, child));
+        on_child_created(path);
+    } else {
+        // If the new path is for a descendant of the current proxy, check if there is a child proxy for it.
+        auto child_result = std::find_if(
+            _children.begin(), _children.end(),
+            [path](const std::pair<std::string, std::shared_ptr<Proxy>>& child_data) -> bool {
+                return Path::is_descendant(child_data.first, path);
+            });
+
+        if (child_result != _children.end()) {
+            // If there is a child proxy for the new path, forward it to that child proxy.
+            child_result->second->path_add(path, managed_interfaces);
+        } else {
+            // If there is no child proxy for the new path, create the child and forward the path to it.
+            // This path will be taken if an empty proxy object needs to be created for an intermediate path.
+            std::string child_path = Path::next_child(_path, path);
+            std::shared_ptr<Proxy> child = path_create(child_path);
+            _children.emplace(std::make_pair(child_path, child));
+            child->path_add(path, managed_interfaces);
+            on_child_created(child_path);
+        }
+    }
+}
+
+bool Proxy::path_remove(const std::string& path, SimpleDBus::Holder options) {
+    // `options` contains an array of strings of the interfaces that need to be removed.
+
+    if (path == _path) {
+        interfaces_unload(options);
+        return path_prune();
+    }
+
+    // If the path is not the current path nor a descendant, then there's nothing to do
+    if (!Path::is_descendant(_path, path)) {
+        return false;
+    }
+
+    // As children will be extensively accessed, we need to lock the child access mutex.
+    std::scoped_lock lock(_child_access_mutex);
+
+    // If the path is a direct child of the proxy path, forward the request to the child proxy.
+    std::string child_path = Path::next_child(_path, path);
+    if (path_exists(child_path)) {
+        bool must_erase = _children.at(child_path)->path_remove(path, options);
+
+        // if the child proxy is no longer needed and there is only one active instance of the child proxy,
+        // then remove it.
+        if (must_erase && _children.at(child_path).use_count() == 1) {
+            _children.erase(child_path);
+        }
+    }
+
+    return false;
+}
+
+bool Proxy::path_prune() {
+    // As children will be extensively accessed, we need to lock the child access mutex.
+    std::scoped_lock lock(_child_access_mutex);
+
+    // For each child proxy, check if it can be pruned.
+    std::vector<std::string> to_remove;
+    for (auto& [child_path, child] : _children) {
+        if (child->path_prune() && _children.at(child_path).use_count() == 1) {
+            to_remove.push_back(child_path);
+        }
+    }
+    for (auto& child_path : to_remove) {
+        _children.erase(child_path);
+    }
+
+    // For self to be pruned, the following conditions must be met:
+    // 1. The proxy has no children
+    // 2. The proxy has no interfaces or all interfaces are disabled.
+    if (_children.empty() && !interfaces_loaded()) {
+        return true;
+    }
+
+    return false;
+}
+
+// ----- MESSAGE HANDLING -----
+void Proxy::message_forward(Message& msg) {
+    // If the message is for the current proxy, then forward it to the message handler.
+    if (msg.get_path() == _path) {
+        // If the message is involves a property change, forward it to the correct interface.
+        if (msg.is_signal("org.freedesktop.DBus.Properties", "PropertiesChanged")) {
+            Holder interface_h = msg.extract();
+            std::string iface_name = interface_h.get_string();
+            msg.extract_next();
+            Holder changed_properties = msg.extract();
+            msg.extract_next();
+            Holder invalidated_properties = msg.extract();
+
+            // If the interface is not loaded, then ignore the message.
+            if (!interface_exists(iface_name)) {
+                return;
+            }
+
+            interface_get(iface_name)->signal_property_changed(changed_properties, invalidated_properties);
+
+        } else if (interface_exists(msg.get_interface())) {
+            interface_get(msg.get_interface())->message_handle(msg);
+        }
+
+        return;
+    }
+
+    // If the message is for a child proxy or a descendant, forward it to that child proxy.
+    for (auto& [child_path, child] : _children) {
+        if (child_path == msg.get_path()) {
+            child->message_forward(msg);
+
+            if (msg.get_type() == Message::Type::SIGNAL) {
+                on_child_signal_received(child_path);
+            }
+
+            return;
+        } else if (Path::is_descendant(child_path, msg.get_path())) {
+            child->message_forward(msg);
+            return;
+        }
+    }
+}
diff --git a/third_party/SimpleDBus/src/base/Connection.cpp b/third_party/SimpleDBus/src/base/Connection.cpp
index 351fcbc8c..952796a43 100644
--- a/third_party/SimpleDBus/src/base/Connection.cpp
+++ b/third_party/SimpleDBus/src/base/Connection.cpp
@@ -2,7 +2,6 @@
 #include <simpledbus/base/Exceptions.h>
 #include <simpledbus/base/Logger.h>
 #include <chrono>
-#include <iostream>
 #include <thread>
 
 using namespace SimpleDBus;
diff --git a/third_party/SimpleDBus/src/base/Exceptions.cpp b/third_party/SimpleDBus/src/base/Exceptions.cpp
index 5aad25942..acb1fe733 100644
--- a/third_party/SimpleDBus/src/base/Exceptions.cpp
+++ b/third_party/SimpleDBus/src/base/Exceptions.cpp
@@ -1,5 +1,7 @@
 #include <simpledbus/base/Exceptions.h>
 
+#include <fmt/core.h>
+
 namespace SimpleDBus {
 
 namespace Exception {
@@ -20,6 +22,18 @@ SendFailed::SendFailed(const std::string& err_name, const std::string& err_messa
 
 const char* SendFailed::what() const noexcept { return _message.c_str(); }
 
+InterfaceNotFoundException::InterfaceNotFoundException(const std::string& path, const std::string& interface) {
+    _message = fmt::format("Path {} does not contain interface {}", path, interface);
+}
+
+const char* InterfaceNotFoundException::what() const noexcept { return _message.c_str(); }
+
+PathNotFoundException::PathNotFoundException(const std::string& path, const std::string& subpath) {
+    _message = fmt::format("Path {} does not contain sub-path {}", path, subpath);
+}
+
+const char* PathNotFoundException::what() const noexcept { return _message.c_str(); }
+
 }  // namespace Exception
 
 }  // namespace SimpleDBus
diff --git a/third_party/SimpleDBus/src/base/Holder.cpp b/third_party/SimpleDBus/src/base/Holder.cpp
index b9ed532c2..24af1344a 100644
--- a/third_party/SimpleDBus/src/base/Holder.cpp
+++ b/third_party/SimpleDBus/src/base/Holder.cpp
@@ -1,6 +1,5 @@
 #include <simpledbus/base/Holder.h>
 #include <iomanip>
-#include <iostream>
 #include <sstream>
 
 #include "dbus/dbus-protocol.h"
@@ -11,7 +10,55 @@ Holder::Holder() {}
 
 Holder::~Holder() {}
 
-Holder::Type Holder::type() { return _type; }
+bool Holder::operator!=(const Holder& other) const { return !(*this == other); }
+
+bool Holder::operator==(const Holder& other) const {
+    if (type() != other.type()) {
+        return false;
+    }
+
+    switch (type()) {
+        case NONE:
+            return true;
+        case BYTE:
+            return get_byte() == other.get_byte();
+        case BOOLEAN:
+            return get_boolean() == other.get_boolean();
+        case INT16:
+            return get_int16() == other.get_int16();
+        case UINT16:
+            return get_uint16() == other.get_uint16();
+        case INT32:
+            return get_int32() == other.get_int32();
+        case UINT32:
+            return get_uint32() == other.get_uint32();
+        case INT64:
+            return get_int64() == other.get_int64();
+        case UINT64:
+            return get_uint64() == other.get_uint64();
+        case DOUBLE:
+            return get_double() == other.get_double();
+        case STRING:
+            return get_string() == other.get_string();
+        case OBJ_PATH:
+            return get_object_path() == other.get_object_path();
+        case SIGNATURE:
+            return get_signature() == other.get_signature();
+        case ARRAY:
+            return get_array() == other.get_array();
+        case DICT:
+            return (get_dict_uint8() == other.get_dict_uint8()) && (get_dict_uint16() == other.get_dict_uint16()) &&
+                   (get_dict_int16() == other.get_dict_int16()) && (get_dict_uint32() == other.get_dict_uint32()) &&
+                   (get_dict_int32() == other.get_dict_int32()) && (get_dict_uint64() == other.get_dict_uint64()) &&
+                   (get_dict_int64() == other.get_dict_int64()) && (get_dict_string() == other.get_dict_string()) &&
+                   (get_dict_object_path() == other.get_dict_object_path()) &&
+                   (get_dict_signature() == other.get_dict_signature());
+        default:
+            return false;
+    }
+}
+
+Holder::Type Holder::type() const { return _type; }
 
 std::string Holder::_represent_simple() {
     std::ostringstream output;
@@ -363,22 +410,22 @@ Holder Holder::create_double(double value) {
     h.holder_double = value;
     return h;
 }
-Holder Holder::create_string(const char* str) {
+Holder Holder::create_string(const std::string& str) {
     Holder h;
     h._type = STRING;
-    h.holder_string = std::string(str);
+    h.holder_string = str;
     return h;
 }
-Holder Holder::create_object_path(const char* str) {
+Holder Holder::create_object_path(const std::string& str) {
     Holder h;
     h._type = OBJ_PATH;
-    h.holder_string = std::string(str);
+    h.holder_string = str;
     return h;
 }
-Holder Holder::create_signature(const char* str) {
+Holder Holder::create_signature(const std::string& str) {
     Holder h;
     h._type = SIGNATURE;
-    h.holder_string = std::string(str);
+    h.holder_string = str;
     return h;
 }
 Holder Holder::create_array() {
@@ -394,7 +441,7 @@ Holder Holder::create_dict() {
     return h;
 }
 
-std::any Holder::get_contents() {
+std::any Holder::get_contents() const {
     // Only return the contents for simple types
     switch (_type) {
         case BOOLEAN:
@@ -426,51 +473,51 @@ std::any Holder::get_contents() {
     }
 }
 
-bool Holder::get_boolean() { return holder_boolean; }
+bool Holder::get_boolean() const { return holder_boolean; }
 
-uint8_t Holder::get_byte() { return (uint8_t)(holder_integer & 0x00000000000000FFL); }
+uint8_t Holder::get_byte() const { return (uint8_t)(holder_integer & 0x00000000000000FFL); }
 
-int16_t Holder::get_int16() { return (int16_t)(holder_integer & 0x000000000000FFFFL); }
+int16_t Holder::get_int16() const { return (int16_t)(holder_integer & 0x000000000000FFFFL); }
 
-uint16_t Holder::get_uint16() { return (uint16_t)(holder_integer & 0x000000000000FFFFL); }
+uint16_t Holder::get_uint16() const { return (uint16_t)(holder_integer & 0x000000000000FFFFL); }
 
-int32_t Holder::get_int32() { return (int32_t)(holder_integer & 0x00000000FFFFFFFFL); }
+int32_t Holder::get_int32() const { return (int32_t)(holder_integer & 0x00000000FFFFFFFFL); }
 
-uint32_t Holder::get_uint32() { return (uint32_t)(holder_integer & 0x00000000FFFFFFFFL); }
+uint32_t Holder::get_uint32() const { return (uint32_t)(holder_integer & 0x00000000FFFFFFFFL); }
 
-int64_t Holder::get_int64() { return (int64_t)holder_integer; }
+int64_t Holder::get_int64() const { return (int64_t)holder_integer; }
 
-uint64_t Holder::get_uint64() { return holder_integer; }
+uint64_t Holder::get_uint64() const { return holder_integer; }
 
-double Holder::get_double() { return holder_double; }
+double Holder::get_double() const { return holder_double; }
 
-std::string Holder::get_string() { return holder_string; }
+std::string Holder::get_string() const { return holder_string; }
 
-std::string Holder::get_object_path() { return holder_string; }
+std::string Holder::get_object_path() const { return holder_string; }
 
-std::string Holder::get_signature() { return holder_string; }
+std::string Holder::get_signature() const { return holder_string; }
 
-std::vector<Holder> Holder::get_array() { return holder_array; }
+std::vector<Holder> Holder::get_array() const { return holder_array; }
 
-std::map<uint8_t, Holder> Holder::get_dict_uint8() { return _get_dict<uint8_t>(BYTE); }
+std::map<uint8_t, Holder> Holder::get_dict_uint8() const { return _get_dict<uint8_t>(BYTE); }
 
-std::map<uint16_t, Holder> Holder::get_dict_uint16() { return _get_dict<uint16_t>(UINT16); }
+std::map<uint16_t, Holder> Holder::get_dict_uint16() const { return _get_dict<uint16_t>(UINT16); }
 
-std::map<uint32_t, Holder> Holder::get_dict_uint32() { return _get_dict<uint32_t>(UINT32); }
+std::map<uint32_t, Holder> Holder::get_dict_uint32() const { return _get_dict<uint32_t>(UINT32); }
 
-std::map<uint64_t, Holder> Holder::get_dict_uint64() { return _get_dict<uint64_t>(UINT64); }
+std::map<uint64_t, Holder> Holder::get_dict_uint64() const { return _get_dict<uint64_t>(UINT64); }
 
-std::map<int16_t, Holder> Holder::get_dict_int16() { return _get_dict<int16_t>(INT16); }
+std::map<int16_t, Holder> Holder::get_dict_int16() const { return _get_dict<int16_t>(INT16); }
 
-std::map<int32_t, Holder> Holder::get_dict_int32() { return _get_dict<int32_t>(INT32); }
+std::map<int32_t, Holder> Holder::get_dict_int32() const { return _get_dict<int32_t>(INT32); }
 
-std::map<int64_t, Holder> Holder::get_dict_int64() { return _get_dict<int64_t>(INT64); }
+std::map<int64_t, Holder> Holder::get_dict_int64() const { return _get_dict<int64_t>(INT64); }
 
-std::map<std::string, Holder> Holder::get_dict_string() { return _get_dict<std::string>(STRING); }
+std::map<std::string, Holder> Holder::get_dict_string() const { return _get_dict<std::string>(STRING); }
 
-std::map<std::string, Holder> Holder::get_dict_object_path() { return _get_dict<std::string>(OBJ_PATH); }
+std::map<std::string, Holder> Holder::get_dict_object_path() const { return _get_dict<std::string>(OBJ_PATH); }
 
-std::map<std::string, Holder> Holder::get_dict_signature() { return _get_dict<std::string>(SIGNATURE); }
+std::map<std::string, Holder> Holder::get_dict_signature() const { return _get_dict<std::string>(SIGNATURE); }
 
 void Holder::array_append(Holder holder) { holder_array.push_back(holder); }
 
@@ -485,7 +532,7 @@ void Holder::dict_append(Type key_type, std::any key, Holder value) {
 }
 
 template <typename T>
-std::map<T, Holder> Holder::_get_dict(Type key_type) {
+std::map<T, Holder> Holder::_get_dict(Type key_type) const {
     std::map<T, Holder> output;
     for (auto& [key_type_internal, key, value] : holder_dict) {
         if (key_type_internal == key_type) {
diff --git a/third_party/SimpleDBus/src/base/Message.cpp b/third_party/SimpleDBus/src/base/Message.cpp
index 9e82bbd40..e201ca524 100644
--- a/third_party/SimpleDBus/src/base/Message.cpp
+++ b/third_party/SimpleDBus/src/base/Message.cpp
@@ -1,6 +1,5 @@
 #include <simpledbus/base/Message.h>
 
-#include <iostream>
 #include <sstream>
 
 using namespace SimpleDBus;
@@ -24,7 +23,7 @@ using namespace SimpleDBus;
         dbus_message_iter_close_container(&sub_iter, &entry_iter);                            \
     }
 
-int Message::creation_counter = 0;
+std::atomic_int32_t Message::creation_counter = 0;
 
 Message::Message() : Message(nullptr) {}
 
@@ -53,6 +52,7 @@ Message::Message(Message&& other) : Message() {
     this->_extracted = other._extracted;
     this->_msg = other._msg;
     this->_iter = other._iter;
+    this->_arguments = other._arguments;
 
     // Invalidate the old message.
     other._invalidate();
@@ -68,6 +68,7 @@ Message::Message(const Message& other) : Message() {
         this->_unique_id = creation_counter++;
         this->_is_extracted = other._is_extracted;
         this->_extracted = other._extracted;
+        this->_arguments = other._arguments;
         this->_msg = dbus_message_copy(other._msg);
     }
 }
@@ -85,6 +86,7 @@ Message& Message::operator=(Message&& other) {
         this->_extracted = other._extracted;
         this->_msg = other._msg;
         this->_iter = other._iter;
+        this->_arguments = other._arguments;
 
         // Invalidate the old message.
         other._invalidate();
@@ -105,6 +107,7 @@ Message& Message::operator=(const Message& other) {
             this->_unique_id = creation_counter++;
             this->_is_extracted = other._is_extracted;
             this->_extracted = other._extracted;
+            this->_arguments = other._arguments;
             this->_msg = dbus_message_copy(other._msg);
         }
     }
@@ -125,6 +128,7 @@ void Message::_invalidate() {
     // For older versions of DBus, DBUS_MESSAGE_ITER_INIT_CLOSED is not defined.
     this->_iter = DBusMessageIter();
 #endif
+    this->_arguments.clear();
 }
 
 void Message::_safe_delete() {
@@ -144,7 +148,7 @@ void Message::_append_argument(DBusMessageIter* iter, Holder& argument, std::str
             break;
         }
         case DBUS_TYPE_BOOLEAN: {
-            bool value = argument.get_boolean();
+            uint32_t value = static_cast<uint32_t>(argument.get_boolean());
             dbus_message_iter_append_basic(iter, DBUS_TYPE_BOOLEAN, &value);
             break;
         }
@@ -285,6 +289,7 @@ void Message::_append_argument(DBusMessageIter* iter, Holder& argument, std::str
 void Message::append_argument(Holder argument, std::string signature) {
     dbus_message_iter_init_append(_msg, &_iter);
     _append_argument(&_iter, argument, signature);
+    _arguments.push_back(argument);
 }
 
 int32_t Message::get_unique_id() { return _unique_id; }
@@ -305,16 +310,16 @@ std::string Message::get_signature() {
     }
 }
 
-MessageType Message::get_type() {
+Message::Type Message::get_type() const {
     if (is_valid()) {
-        return (MessageType)dbus_message_get_type(_msg);
+        return (Message::Type)dbus_message_get_type(_msg);
     } else {
-        return MessageType::INVALID;
+        return Message::Type::INVALID;
     }
 }
 
 std::string Message::get_path() {
-    if (is_valid() && get_type() == MessageType::SIGNAL) {
+    if (is_valid() && get_type() == Message::Type::SIGNAL) {
         return dbus_message_get_path(_msg);
     } else {
         return "";
@@ -366,8 +371,15 @@ std::string Message::to_string() const {
     oss << "[" << _unique_id << "] " << type_to_name(dbus_message_get_type(_msg));
     oss << "[" << sender << "->" << destination << "] ";
     oss << dbus_message_get_path(_msg) << " " << dbus_message_get_interface(_msg) << " "
-        << dbus_message_get_member(_msg) << " ";
+        << dbus_message_get_member(_msg);
 
+    if (get_type() == Message::Type::METHOD_CALL) {
+        oss << std::endl;
+        oss << "Arguments: " << std::endl;
+        for (auto arg : _arguments) {
+            oss << arg.represent();
+        }
+    }
     return oss.str();
 }
 
diff --git a/third_party/SimpleDBus/src/base/Path.cpp b/third_party/SimpleDBus/src/base/Path.cpp
new file mode 100644
index 000000000..4082fc90c
--- /dev/null
+++ b/third_party/SimpleDBus/src/base/Path.cpp
@@ -0,0 +1,124 @@
+#include "simpledbus/base/Path.h"
+
+#include <algorithm>
+
+namespace SimpleDBus {
+
+// TODO: Add path validation and throw exception if invalid
+
+size_t Path::count_elements(const std::string& path) {
+    if (path.empty() || path == "/") {
+        return 0;
+    }
+
+    return std::count(path.begin(), path.end(), '/');
+}
+
+std::vector<std::string> Path::split_elements(const std::string& path) {
+    std::vector<std::string> elements;
+
+    if (path.empty() || path == "/") {
+        return elements;
+    }
+
+    // Note: Skip the first element, which is the root
+    std::string::const_iterator it = path.begin() + 1;
+    std::string::const_iterator end = path.end();
+
+    while (it != end) {
+        std::string::const_iterator next = std::find(it, end, '/');
+        elements.push_back(std::string(it, next));
+        it = next;
+        if (it != end) {
+            ++it;
+        }
+    }
+
+    return elements;
+}
+
+std::string Path::fetch_elements(const std::string& path, size_t count) {
+    if (count == 0) {
+        return "/";
+    }
+
+    if (count > count_elements(path)) {
+        // TODO: Should we throw an exception?
+        return path;
+    }
+
+    std::vector<std::string> elements = split_elements(path);
+    std::string result;
+
+    for (size_t i = 0; i < count; i++) {
+        result += "/" + elements[i];
+    }
+
+    return result;
+}
+
+bool Path::is_descendant(const std::string& base, const std::string& path) {
+    if (base.empty() || path.empty()) {
+        return false;
+    }
+
+    if (base == path) {
+        return false;
+    }
+
+    if (base == "/") {
+        return true;
+    }
+
+    return path.rfind(base) == 0;
+}
+
+bool Path::is_ascendant(const std::string& base, const std::string& path) {
+    if (base.empty() || path.empty()) {
+        return false;
+    }
+
+    if (base == path) {
+        return false;
+    }
+
+    return !is_descendant(base, path);
+}
+
+bool Path::is_child(const std::string& base, const std::string& path) {
+    if (base.empty() || path.empty()) {
+        return false;
+    }
+
+    if (base == path) {
+        return false;
+    }
+
+    if (!is_descendant(base, path)) {
+        return false;
+    }
+
+    return count_elements(base) + 1 == count_elements(path);
+}
+
+bool Path::is_parent(const std::string& base, const std::string& path) {
+    if (base.empty() || path.empty()) {
+        return false;
+    }
+
+    if (base == path) {
+        return false;
+    }
+
+    if (!is_ascendant(base, path)) {
+        return false;
+    }
+
+    return count_elements(base) - 1 == count_elements(path);
+}
+
+std::string Path::next_child(const std::string& base, const std::string& path) {
+    return fetch_elements(path, count_elements(base) + 1);
+}
+
+}  // namespace SimpleDBus
diff --git a/third_party/SimpleDBus/src/common/Introspectable.cpp b/third_party/SimpleDBus/src/common/Introspectable.cpp
deleted file mode 100644
index e3e979c8f..000000000
--- a/third_party/SimpleDBus/src/common/Introspectable.cpp
+++ /dev/null
@@ -1,21 +0,0 @@
-#include <simpledbus/base/Logger.h>
-#include <simpledbus/common/Introspectable.h>
-
-#include <iostream>
-
-using namespace SimpleDBus;
-
-Introspectable::Introspectable(Connection* conn, std::string service, std::string path)
-    : _conn(conn), _service(service), _path(path), _interface("org.freedesktop.DBus.Introspectable") {}
-
-Introspectable::~Introspectable() {}
-
-// Names are made matching the ones from the DBus specification
-Holder Introspectable::Introspect() {
-    LOG_F(DEBUG, "%s -> Introspect()", _path.c_str());
-    Message query_msg = Message::create_method_call(_service, _path, _interface, "Introspect");
-
-    Message reply_msg = _conn->send_with_reply_and_block(query_msg);
-    Holder result = reply_msg.extract();
-    return result;
-}
diff --git a/third_party/SimpleDBus/src/common/Properties.cpp b/third_party/SimpleDBus/src/common/Properties.cpp
deleted file mode 100644
index 08317e0b9..000000000
--- a/third_party/SimpleDBus/src/common/Properties.cpp
+++ /dev/null
@@ -1,54 +0,0 @@
-#include <simpledbus/base/Logger.h>
-#include <simpledbus/common/Properties.h>
-
-#include <iostream>
-
-using namespace SimpleDBus;
-
-Properties::Properties(Connection* conn, std::string service, std::string path)
-    : _conn(conn), _service(service), _path(path), _interface("org.freedesktop.DBus.Properties") {}
-
-Properties::~Properties() {}
-
-// Names are made matching the ones from the DBus specification
-Holder Properties::Get(std::string interface, std::string name) {
-    LOG_F(DEBUG, "%s -> Get(%s, %s)", _path.c_str(), interface.c_str(), name.c_str());
-    Message query_msg = Message::create_method_call(_service, _path, _interface, "Get");
-
-    Holder h_interface = Holder::create_string(interface.c_str());
-    query_msg.append_argument(h_interface, "s");
-
-    Holder h_name = Holder::create_string(name.c_str());
-    query_msg.append_argument(h_name, "s");
-
-    Message reply_msg = _conn->send_with_reply_and_block(query_msg);
-    Holder result = reply_msg.extract();
-    return result;
-}
-
-Holder Properties::GetAll(std::string interface) {
-    LOG_F(DEBUG, "%s -> GetAll(%s)", _path.c_str(), interface.c_str());
-    Message query_msg = Message::create_method_call(_service, _path, _interface, "GetAll");
-
-    Holder h_interface = Holder::create_string(interface.c_str());
-    query_msg.append_argument(h_interface, "s");
-
-    Message reply_msg = _conn->send_with_reply_and_block(query_msg);
-    Holder result = reply_msg.extract();
-    return result;
-}
-
-void Properties::Set(std::string interface, std::string name, Holder value) {
-    LOG_F(DEBUG, "%s -> Set(%s, %s, %s)", _path.c_str(), interface.c_str(), name.c_str(), value.represent().c_str());
-    Message query_msg = Message::create_method_call(_service, _path, _interface, "Set");
-
-    Holder h_interface = Holder::create_string(interface.c_str());
-    query_msg.append_argument(h_interface, "s");
-
-    Holder h_name = Holder::create_string(name.c_str());
-    query_msg.append_argument(h_name, "s");
-
-    query_msg.append_argument(value, "v");
-
-    _conn->send_with_reply_and_block(query_msg);
-}
diff --git a/third_party/SimpleDBus/src/common/ObjectManager.cpp b/third_party/SimpleDBus/src/interfaces/ObjectManager.cpp
similarity index 51%
rename from third_party/SimpleDBus/src/common/ObjectManager.cpp
rename to third_party/SimpleDBus/src/interfaces/ObjectManager.cpp
index 94fbcf08f..e6c407817 100644
--- a/third_party/SimpleDBus/src/common/ObjectManager.cpp
+++ b/third_party/SimpleDBus/src/interfaces/ObjectManager.cpp
@@ -1,17 +1,16 @@
 #include <simpledbus/base/Logger.h>
-#include <simpledbus/common/ObjectManager.h>
+#include <simpledbus/interfaces/ObjectManager.h>
 
 using namespace SimpleDBus;
 
-ObjectManager::ObjectManager(Connection* conn, std::string service, std::string path)
-    : _conn(conn), _service(service), _path(path), _interface("org.freedesktop.DBus.ObjectManager") {}
-
-ObjectManager::~ObjectManager() {}
+ObjectManager::ObjectManager(std::shared_ptr<Connection> conn, std::string bus_name, std::string path)
+    : Interface(conn, bus_name, path, "org.freedesktop.DBus.ObjectManager") {}
 
 Holder ObjectManager::GetManagedObjects(bool use_callbacks) {
-    Message query_msg = Message::create_method_call(_service, _path, _interface, "GetManagedObjects");
+    Message query_msg = Message::create_method_call(_bus_name, _path, _interface_name, "GetManagedObjects");
     Message reply_msg = _conn->send_with_reply_and_block(query_msg);
     Holder managed_objects = reply_msg.extract();
+    // TODO: Remove immediate callback support.
     if (use_callbacks) {
         auto managed_object = managed_objects.get_dict_object_path();
         for (auto& [path, options] : managed_object) {
@@ -23,9 +22,27 @@ Holder ObjectManager::GetManagedObjects(bool use_callbacks) {
     return managed_objects;
 }
 
+void ObjectManager::message_handle(Message msg) {
+    if (msg.is_signal(_interface_name, "InterfacesAdded")) {
+        std::string path = msg.extract().get_string();
+        msg.extract_next();
+        Holder options = msg.extract();
+        if (InterfacesAdded) {
+            InterfacesAdded(path, options);
+        }
+    } else if (msg.is_signal(_interface_name, "InterfacesRemoved")) {
+        std::string path = msg.extract().get_string();
+        msg.extract_next();
+        Holder options = msg.extract();
+        if (InterfacesRemoved) {
+            InterfacesRemoved(path, options);
+        }
+    }
+}
+
 bool ObjectManager::process_received_signal(Message& message) {
     if (message.get_path() == _path) {
-        if (message.is_signal(_interface, "InterfacesAdded")) {
+        if (message.is_signal(_interface_name, "InterfacesAdded")) {
             std::string path = message.extract().get_string();
             message.extract_next();
             Holder options = message.extract();
@@ -33,7 +50,7 @@ bool ObjectManager::process_received_signal(Message& message) {
                 InterfacesAdded(path, options);
             }
             return true;
-        } else if (message.is_signal(_interface, "InterfacesRemoved")) {
+        } else if (message.is_signal(_interface_name, "InterfacesRemoved")) {
             std::string path = message.extract().get_string();
             message.extract_next();
             Holder options = message.extract();
diff --git a/third_party/SimpleDBus/src/interfaces/PropertyHandler.cpp b/third_party/SimpleDBus/src/interfaces/PropertyHandler.cpp
deleted file mode 100644
index bf69ea68a..000000000
--- a/third_party/SimpleDBus/src/interfaces/PropertyHandler.cpp
+++ /dev/null
@@ -1,38 +0,0 @@
-#include <simpledbus/interfaces/PropertyHandler.h>
-
-using namespace SimpleDBus::Interfaces;
-
-PropertyHandler::PropertyHandler(std::string path) : _path(path) {}
-
-PropertyHandler::~PropertyHandler() {}
-
-void PropertyHandler::set_options(SimpleDBus::Holder changed_properties) {
-    this->set_options(changed_properties, SimpleDBus::Holder());
-}
-
-void PropertyHandler::set_options(SimpleDBus::Holder changed_properties, SimpleDBus::Holder invalidated_properties) {
-    auto changed_options = changed_properties.get_dict_string();
-    for (auto& [name, value] : changed_options) {
-        this->add_option(name, value);
-    }
-
-    auto removed_options = invalidated_properties.get_array();
-    for (auto& removed_option : removed_options) {
-        this->remove_option(removed_option.get_string());
-    }
-}
-
-bool PropertyHandler::process_received_signal(Message& message) {
-    if (message.get_path() == _path && message.is_signal("org.freedesktop.DBus.Properties", "PropertiesChanged")) {
-        Holder interface = message.extract();
-        message.extract_next();
-        Holder changed_properties = message.extract();
-        message.extract_next();
-        Holder invalidated_properties = message.extract();
-        if (PropertiesChanged) {
-            PropertiesChanged(interface.get_string(), changed_properties, invalidated_properties);
-        }
-        return true;
-    }
-    return false;
-}
diff --git a/tools/build.py b/tools/build.py
index 70718309e..f1d4fb123 100644
--- a/tools/build.py
+++ b/tools/build.py
@@ -32,6 +32,9 @@ def get_generator(self):
     def get_arch(self):
         raise NotImplementedError
 
+    def get_sdk_version(self):
+        raise NotImplementedError
+
     def __lt__(self, other):
         return self.priority < other.priority
 
@@ -44,10 +47,25 @@ def __le__(self, other):
     def __ge__(self, other):
         return self.priority >= other.priority
 
+
+class VS2022(Generator):
+
+    def __init__(self):
+        super(VS2022, self).__init__(10)
+
+    def get_generator(self):
+        return 'Visual Studio 17 2022'
+
+    def get_arch(self):
+        return 'x64'
+
+    def get_sdk_version(self):
+        return '8.1'
+
 class VS2019(Generator):
 
     def __init__(self):
-        super(VS2019, self).__init__(10)
+        super(VS2019, self).__init__(15)
 
     def get_generator(self):
         return 'Visual Studio 16 2019'
@@ -55,6 +73,8 @@ def get_generator(self):
     def get_arch(self):
         return 'x64'
 
+    def get_sdk_version(self):
+        return '8.1'
 
 class VS2017(Generator):
 
@@ -67,6 +87,8 @@ def get_generator(self):
     def get_arch(self):
         return None
 
+    def get_sdk_version(self):
+        return '8.1'
 
 def get_win_generators():
     result = list()
@@ -74,6 +96,8 @@ def get_win_generators():
         output = subprocess.check_output(['C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe', '-property', 'displayName'])
         output = output.decode('utf-8', 'ignore')
         print(output)
+        if '2022' in output:
+            result.append(VS2022())
         if '2019' in output:
             result.append(VS2019())
         if '2017' in output:
@@ -105,6 +129,7 @@ def prepare_args():
         if not generators:
             parser.add_argument('--generator', type=str, help='generator for CMake', required=True)
             parser.add_argument('--arch', type=str, help='arch for CMake', required=False)
+            parser.add_argument('--cmake-system-version', type=str, help='system version for win', required=False)
         else:
             generator = generators[0]
             parser.add_argument('--generator', type=str, help='generator for CMake', required=False, default=generator.get_generator())
@@ -112,7 +137,10 @@ def prepare_args():
                 parser.add_argument('--arch', type=str, choices=['x64', 'Win32', 'ARM', 'ARM64'], help='arch for CMake', required=False, default=generator.get_arch())
             else:
                 parser.add_argument('--arch', type=str, choices=['x64', 'Win32', 'ARM', 'ARM64'], help='arch for CMake', required=False)
-        parser.add_argument('--cmake-system-version', type=str, help='system version for win', required=False, default='8.1')
+            if generator.get_sdk_version() is not None:
+                parser.add_argument('--cmake-system-version', type=str, help='system version for win', required=False, default=generator.get_sdk_version())
+            else:
+                parser.add_argument('--cmake-system-version', type=str, help='system version for win', required=False)
     elif platform.system() == 'Darwin':
         macos_ver = platform.mac_ver()[0]
         versions = [int(x) for x in macos_ver.split('.')]


Test patch:
diff --git a/tests/python/brainflow_get_data.py b/tests/python/brainflow_get_data.py
index 36cba7504..ae45737c0 100644
--- a/tests/python/brainflow_get_data.py
+++ b/tests/python/brainflow_get_data.py
@@ -41,7 +41,6 @@ def main():
 
     board = BoardShim(args.board_id, params)
     board.prepare_session()
-
     # board.start_stream () # use this for default options
     board.start_stream(45000, args.streamer_params)
     time.sleep(10)
diff --git a/third_party/SimpleBluez/.github/workflows/ci_test.yml b/third_party/SimpleBluez/.github/workflows/ci_test.yml
new file mode 100644
index 000000000..8c50cfa5e
--- /dev/null
+++ b/third_party/SimpleBluez/.github/workflows/ci_test.yml
@@ -0,0 +1,42 @@
+name: CI Test
+
+on: [push, pull_request]
+
+jobs:
+
+  test-linux:
+    runs-on: ${{ matrix.os }}
+
+    strategy:
+      fail-fast: false
+      max-parallel: 4
+      matrix:
+        os: [ubuntu-20.04]
+
+    steps:
+      - name: Clone Repository
+        uses: actions/checkout@v2
+      - name: Install Dependencies
+        env:
+            DEBIAN_FRONTEND: noninteractive  
+        run: |
+          sudo -H apt-get update -y
+          sudo -H apt-get install -y dbus libdbus-1-dev python3-dev libgtest-dev libgmock-dev
+          sudo -H pip3 install -r $GITHUB_WORKSPACE/test/requirements.txt
+      - name: Start DBus
+        run: |
+          echo "DBUS_SESSION_BUS_ADDRESS=$(dbus-daemon --config-file=/usr/share/dbus-1/session.conf --print-address --fork | cut -d, -f1)" >> $GITHUB_ENV
+      - name: Setup Cmake
+        uses: jwlawson/actions-setup-cmake@v1.4
+        with:
+          cmake-version: "3.16.x"
+      - name: Compile tests
+        run: |
+          mkdir $GITHUB_WORKSPACE/build
+          cd $GITHUB_WORKSPACE/build
+          cmake -DCMAKE_BUILD_TYPE=Release ../test
+          make -j4
+      - name: Run tests
+        run: |
+          cd $GITHUB_WORKSPACE/build
+          ./simplebluez_test
diff --git a/third_party/SimpleBluez/scripts/build_tests.sh b/third_party/SimpleBluez/scripts/build_tests.sh
new file mode 100644
index 000000000..a97c4184f
--- /dev/null
+++ b/third_party/SimpleBluez/scripts/build_tests.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+PROJECT_ROOT=$(realpath $(dirname `realpath $0`)/..)
+BUILD_PATH=$PROJECT_ROOT/build_test
+
+# Parse the received commands
+while :; do
+    case $1 in
+        -c|--clean) FLAG_CLEAN="SET"            
+        ;;
+        *) break
+    esac
+    shift
+done
+
+# Cleanup the existing files
+if [[ ! -z "$FLAG_CLEAN" ]]; then
+    rm -rf $BUILD_PATH
+fi
+
+# Compile!
+mkdir -p $BUILD_PATH
+cd $BUILD_PATH
+cmake -H$PROJECT_ROOT/test -DCMAKE_BUILD_TYPE=Debug
+make -j7
diff --git a/third_party/SimpleBluez/test/CMakeLists.txt b/third_party/SimpleBluez/test/CMakeLists.txt
new file mode 100644
index 000000000..96fbee7e8
--- /dev/null
+++ b/third_party/SimpleBluez/test/CMakeLists.txt
@@ -0,0 +1,31 @@
+cmake_minimum_required(VERSION 3.16)
+project(simplebluez_test)
+
+# Nice hack to automatically ignore the build directory
+file(WRITE ${CMAKE_BINARY_DIR}/.gitignore "*")
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_EXTENSIONS OFF)
+
+# Include SimpleBluez
+add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/.. ${CMAKE_BINARY_DIR}/simplebluez)
+include_directories(${SIMPLEBLUEZ_INCLUDES})
+
+find_package(Python3 COMPONENTS Development REQUIRED)
+
+enable_testing()
+find_package(GTest REQUIRED)
+
+set(
+  SRC_FILES
+  ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/src/helpers/PythonRunner.cpp
+)
+
+add_executable(simplebluez_test ${SRC_FILES})
+target_link_libraries(simplebluez_test simplebluez-static ${GTEST_LIBRARIES} ${Python3_LIBRARIES})
+target_include_directories(simplebluez_test PRIVATE ${GTEST_INCLUDE_DIRS} ${Python3_INCLUDE_DIRS})
+
+add_custom_command (TARGET simplebluez_test POST_BUILD
+  COMMAND "${CMAKE_COMMAND}" -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/python/ ${CMAKE_BINARY_DIR}
+)
diff --git a/third_party/SimpleBluez/test/python/interfaces/__init__.py b/third_party/SimpleBluez/test/python/interfaces/__init__.py
new file mode 100644
index 000000000..8d9aeffa3
--- /dev/null
+++ b/third_party/SimpleBluez/test/python/interfaces/__init__.py
@@ -0,0 +1,3 @@
+from interfaces.general import Emulator
+
+__all__ = ["Emulator"]
diff --git a/third_party/SimpleBluez/test/python/interfaces/general.py b/third_party/SimpleBluez/test/python/interfaces/general.py
new file mode 100644
index 000000000..a956bac3f
--- /dev/null
+++ b/third_party/SimpleBluez/test/python/interfaces/general.py
@@ -0,0 +1,16 @@
+from dbus_next.service import ServiceInterface, method
+
+class Emulator(ServiceInterface):
+    def __init__(self, bus):
+        self.bus = bus
+        super().__init__("simpledbus.tester")
+
+    def export(self):
+        self.bus.export("/", self)
+
+    @method()
+    def Exit(self):
+        """
+        Finishes the emulation session by disconnecting from dbus.
+        """
+        self.bus.disconnect()
diff --git a/third_party/SimpleBluez/test/python/test_fixture.py b/third_party/SimpleBluez/test/python/test_fixture.py
new file mode 100644
index 000000000..3fc53466b
--- /dev/null
+++ b/third_party/SimpleBluez/test/python/test_fixture.py
@@ -0,0 +1,41 @@
+import signal
+import asyncio
+import dbus_next
+from dbus_next.aio import MessageBus
+from interfaces import Emulator
+
+active = True
+bus = None
+
+
+def handler(signum, frame):
+    global bus
+    global active
+    bus.disconnect()
+    active = False
+
+
+async def setup():
+    global bus
+    bus = await MessageBus(bus_type=dbus_next.BusType.SESSION).connect()
+
+    # Create object instances
+    emulator = Emulator(bus)
+
+    # Register object instances
+    emulator.export()
+    
+    # Request bus name
+    await bus.request_name("simplebluez.tester.python")
+
+
+async def main():
+    global active
+    while active:
+        await asyncio.sleep(0.1)
+
+
+if __name__ == "__main__":
+    signal.signal(signal.SIGINT, handler)
+    asyncio.get_event_loop().run_until_complete(setup())
+    asyncio.get_event_loop().run_until_complete(main())
diff --git a/third_party/SimpleBluez/test/requirements.txt b/third_party/SimpleBluez/test/requirements.txt
new file mode 100644
index 000000000..9402fd04b
--- /dev/null
+++ b/third_party/SimpleBluez/test/requirements.txt
@@ -0,0 +1,1 @@
+dbus_next
diff --git a/third_party/SimpleBluez/test/src/helpers/PythonRunner.cpp b/third_party/SimpleBluez/test/src/helpers/PythonRunner.cpp
new file mode 100644
index 000000000..3f21fcf4c
--- /dev/null
+++ b/third_party/SimpleBluez/test/src/helpers/PythonRunner.cpp
@@ -0,0 +1,53 @@
+#include "PythonRunner.h"
+#include <Python.h>
+#include <mutex>
+
+#include <csignal>
+
+PythonRunner::PythonRunner(const std::string& script_path) : _script_path(script_path) {
+    // In order to get Python to properly find relative imports, the current location
+    // of the executable needs to be added to the Python path.
+    _import_path = std::filesystem::canonical("/proc/self/exe").parent_path();
+}
+
+PythonRunner::~PythonRunner() { uninit(); }
+
+void PythonRunner::init() {
+    _async_thread = new std::thread(&PythonRunner::_async_thread_function, this);
+    while (!_python_initialized) {
+        std::this_thread::sleep_for(std::chrono::milliseconds(10));
+    }
+    std::this_thread::sleep_for(std::chrono::seconds(1));
+}
+
+void PythonRunner::uninit() {
+    if (Py_IsInitialized()) {
+        // This is a hack to get the Python interpreter to exit cleanly.
+        raise(SIGINT);
+    }
+    if (_async_thread) {
+        while (!_async_thread->joinable()) {
+            std::this_thread::sleep_for(std::chrono::milliseconds(1));
+        }
+        _async_thread->join();
+        delete _async_thread;
+        _async_thread = nullptr;
+        _python_initialized = false;
+    }
+}
+
+void PythonRunner::_async_thread_function() {
+    Py_InitializeEx(0);
+
+    // Append the required import path to the Python path.
+    PyRun_SimpleString("import sys");
+    PyRun_SimpleString(("sys.path.append('" + _import_path.string() + "')").c_str());
+    _python_initialized = true;
+
+    // Run the script.
+    std::string full_script_path = (_import_path / _script_path).string();
+    FILE* fp = fopen(full_script_path.c_str(), "r");
+    PyRun_SimpleFile(fp, full_script_path.c_str());
+    fclose(fp);
+    Py_Finalize();
+}
diff --git a/third_party/SimpleBluez/test/src/helpers/PythonRunner.h b/third_party/SimpleBluez/test/src/helpers/PythonRunner.h
new file mode 100644
index 000000000..7c085366b
--- /dev/null
+++ b/third_party/SimpleBluez/test/src/helpers/PythonRunner.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <atomic>
+#include <filesystem>
+#include <string>
+#include <thread>
+
+class PythonRunner {
+  public:
+    PythonRunner(const std::string& script_path);
+    ~PythonRunner();
+    PythonRunner(PythonRunner& other) = delete;    // Remove the copy constructor
+    void operator=(const PythonRunner&) = delete;  // Remove the copy assignment
+
+    void init();
+    void uninit();
+
+  private:
+    std::atomic_bool _python_initialized = {false};
+    std::filesystem::path _import_path;
+    std::string _script_path;
+    std::thread* _async_thread;
+    void _async_thread_function(); 
+};
diff --git a/third_party/SimpleBluez/test/src/main.cpp b/third_party/SimpleBluez/test/src/main.cpp
new file mode 100644
index 000000000..de59082d2
--- /dev/null
+++ b/third_party/SimpleBluez/test/src/main.cpp
@@ -0,0 +1,17 @@
+
+#include <gtest/gtest.h>
+#include "helpers/PythonRunner.h"
+
+int main(int argc, char **argv) {
+
+    PythonRunner runner("test_fixture.py");
+
+    runner.init();
+
+    ::testing::InitGoogleTest(&argc, argv); 
+    auto test_result = RUN_ALL_TESTS();
+
+    runner.uninit();
+
+    return test_result;
+}
diff --git a/third_party/SimpleDBus/.github/workflows/ci_test.yml b/third_party/SimpleDBus/.github/workflows/ci_test.yml
index fd466ccab..875f3cf15 100644
--- a/third_party/SimpleDBus/.github/workflows/ci_test.yml
+++ b/third_party/SimpleDBus/.github/workflows/ci_test.yml
@@ -4,7 +4,7 @@ on: [push, pull_request]
 
 jobs:
 
-  test-linux:
+  test-all:
     runs-on: ${{ matrix.os }}
 
     strategy:
@@ -21,7 +21,7 @@ jobs:
             DEBIAN_FRONTEND: noninteractive  
         run: |
           sudo -H apt-get update -y
-          sudo -H apt-get install -y dbus libdbus-1-dev python3-dev libgtest-dev
+          sudo -H apt-get install -y dbus libdbus-1-dev python3-dev libgtest-dev libgmock-dev
           sudo -H pip3 install -r $GITHUB_WORKSPACE/test/requirements.txt
       - name: Start DBus
         run: |
@@ -30,13 +30,36 @@ jobs:
         uses: jwlawson/actions-setup-cmake@v1.4
         with:
           cmake-version: "3.16.x"
-      - name: Compile tests
+      - name: Compile Unit Tests
         run: |
-          mkdir $GITHUB_WORKSPACE/build
+          mkdir -p $GITHUB_WORKSPACE/build
           cd $GITHUB_WORKSPACE/build
+          rm -rf ./*
           cmake -DCMAKE_BUILD_TYPE=Release ../test
           make -j4
-      - name: Run tests
+      - name: Run Unit Tests
+        run: |
+          cd $GITHUB_WORKSPACE/build
+          ./simpledbus_test
+      - name: Compile Unit Tests with Address Sanitizer
+        run: |
+          mkdir -p $GITHUB_WORKSPACE/build
+          cd $GITHUB_WORKSPACE/build
+          rm -rf ./*
+          cmake -DCMAKE_BUILD_TYPE=Debug -DSIMPLEDBUS_SANITIZE=Address ../test 
+          make -j4
+      - name: Run Unit Tests with Address Sanitizer
+        run: |
+          cd $GITHUB_WORKSPACE/build
+          PYTHONMALLOC=malloc ./simpledbus_test
+      - name: Compile Unit Tests with Thread Sanitizer
+        run: |
+          mkdir -p $GITHUB_WORKSPACE/build
+          cd $GITHUB_WORKSPACE/build
+          rm -rf ./*
+          cmake -DCMAKE_BUILD_TYPE=Debug -DSIMPLEDBUS_SANITIZE=Thread ../test
+          make -j4
+      - name: Run Unit Tests with Thread Sanitizer
         run: |
           cd $GITHUB_WORKSPACE/build
           ./simpledbus_test
diff --git a/third_party/SimpleDBus/scripts/build_tests.sh b/third_party/SimpleDBus/scripts/build_tests.sh
index 15decd333..a97c4184f 100644
--- a/third_party/SimpleDBus/scripts/build_tests.sh
+++ b/third_party/SimpleDBus/scripts/build_tests.sh
@@ -22,5 +22,4 @@ fi
 mkdir -p $BUILD_PATH
 cd $BUILD_PATH
 cmake -H$PROJECT_ROOT/test -DCMAKE_BUILD_TYPE=Debug
-make clean
 make -j7
diff --git a/third_party/SimpleDBus/test/CMakeLists.txt b/third_party/SimpleDBus/test/CMakeLists.txt
index 3021fe809..5ab9b5004 100644
--- a/third_party/SimpleDBus/test/CMakeLists.txt
+++ b/third_party/SimpleDBus/test/CMakeLists.txt
@@ -16,8 +16,12 @@ find_package(GTest REQUIRED)
 set(
   SRC_FILES
   ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/src/test_callback.cpp
   ${CMAKE_CURRENT_SOURCE_DIR}/src/test_holder.cpp
   ${CMAKE_CURRENT_SOURCE_DIR}/src/test_message.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/src/test_proxy_interfaces.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/src/test_proxy_children.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/src/test_path.cpp
   ${CMAKE_CURRENT_SOURCE_DIR}/src/helpers/PythonRunner.cpp
 )
 
diff --git a/third_party/SimpleDBus/test/src/helpers/PythonRunner.cpp b/third_party/SimpleDBus/test/src/helpers/PythonRunner.cpp
index 4ca7d213e..3f21fcf4c 100644
--- a/third_party/SimpleDBus/test/src/helpers/PythonRunner.cpp
+++ b/third_party/SimpleDBus/test/src/helpers/PythonRunner.cpp
@@ -3,7 +3,6 @@
 #include <mutex>
 
 #include <csignal>
-#include <iostream>
 
 PythonRunner::PythonRunner(const std::string& script_path) : _script_path(script_path) {
     // In order to get Python to properly find relative imports, the current location
diff --git a/third_party/SimpleDBus/test/src/test_callback.cpp b/third_party/SimpleDBus/test/src/test_callback.cpp
new file mode 100644
index 000000000..fe4720a95
--- /dev/null
+++ b/third_party/SimpleDBus/test/src/test_callback.cpp
@@ -0,0 +1,12 @@
+#include <gtest/gtest.h>
+
+#include <simpledbus/advanced/Callback.h>
+
+#include <any>
+#include <map>
+#include <string>
+#include <variant>
+
+using namespace SimpleDBus;
+
+// TODO: Write unit tests for Callback.
diff --git a/third_party/SimpleDBus/test/src/test_holder.cpp b/third_party/SimpleDBus/test/src/test_holder.cpp
index 7f35205ee..533561cfc 100644
--- a/third_party/SimpleDBus/test/src/test_holder.cpp
+++ b/third_party/SimpleDBus/test/src/test_holder.cpp
@@ -180,3 +180,5 @@ TEST(Holder, DictionaryHeterogeneous) {
 
     // TODO: Expand this test to check with all remaining types.
 }
+
+// TODO: Add tests for equality comparison of Holders.
diff --git a/third_party/SimpleDBus/test/src/test_message.cpp b/third_party/SimpleDBus/test/src/test_message.cpp
index b1544f33a..d0f34ca84 100644
--- a/third_party/SimpleDBus/test/src/test_message.cpp
+++ b/third_party/SimpleDBus/test/src/test_message.cpp
@@ -1,6 +1,7 @@
 #include <gtest/gtest.h>
 
-#include <simpledbus/SimpleDBus.h>
+#include <simpledbus/base/Connection.h>
+#include <simpledbus/base/Message.h>
 
 using namespace SimpleDBus;
 
diff --git a/third_party/SimpleDBus/test/src/test_path.cpp b/third_party/SimpleDBus/test/src/test_path.cpp
new file mode 100644
index 000000000..3d45f4e13
--- /dev/null
+++ b/third_party/SimpleDBus/test/src/test_path.cpp
@@ -0,0 +1,76 @@
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <simpledbus/base/Path.h>
+
+using namespace SimpleDBus;
+
+TEST(Path, CountElements) {
+    EXPECT_EQ(0, Path::count_elements("/"));
+    EXPECT_EQ(1, Path::count_elements("/a"));
+    EXPECT_EQ(2, Path::count_elements("/a/b"));
+    EXPECT_EQ(3, Path::count_elements("/a/b/c"));
+}
+
+TEST(Path, FetchElements) {
+    EXPECT_EQ("/", Path::fetch_elements("/a/b/c/d", 0));
+    EXPECT_EQ("/a", Path::fetch_elements("/a/b/c/d", 1));
+    EXPECT_EQ("/a/b", Path::fetch_elements("/a/b/c/d", 2));
+    EXPECT_EQ("/a/b/c", Path::fetch_elements("/a/b/c/d", 3));
+    EXPECT_EQ("/a/b/c/d", Path::fetch_elements("/a/b/c/d", 4));
+}
+
+TEST(Path, SplitElements) {
+    EXPECT_THAT(Path::split_elements("/"), ::testing::SizeIs(0));
+    EXPECT_THAT(Path::split_elements("/a"), ::testing::ElementsAre("a"));
+    EXPECT_THAT(Path::split_elements("/a/b"), ::testing::ElementsAre("a", "b"));
+    EXPECT_THAT(Path::split_elements("/a/b/c"), ::testing::ElementsAre("a", "b", "c"));
+    EXPECT_THAT(Path::split_elements("/a/b/c/d"), ::testing::ElementsAre("a", "b", "c", "d"));
+}
+
+TEST(Path, RecognizeDescendant) {
+    EXPECT_FALSE(Path::is_descendant("/a/b", "/a/b"));
+    EXPECT_FALSE(Path::is_descendant("/a/b", "/a"));
+    EXPECT_FALSE(Path::is_descendant("/a/b", "/"));
+
+    EXPECT_TRUE(Path::is_descendant("/a/b", "/a/b/c"));
+    EXPECT_TRUE(Path::is_descendant("/a/b", "/a/b/c/d"));
+    EXPECT_TRUE(Path::is_descendant("/a/b", "/a/b/c/d/e"));
+}
+
+TEST(Path, RecognizeAscendant) {
+    EXPECT_FALSE(Path::is_ascendant("/a/b", "/a/b"));
+    EXPECT_TRUE(Path::is_ascendant("/a/b", "/a"));
+    EXPECT_TRUE(Path::is_ascendant("/a/b", "/"));
+
+    EXPECT_FALSE(Path::is_ascendant("/a/b", "/a/b/c"));
+    EXPECT_FALSE(Path::is_ascendant("/a/b", "/a/b/c/d"));
+    EXPECT_FALSE(Path::is_ascendant("/a/b", "/a/b/c/d/e"));
+}
+
+TEST(Path, RecognizeChild) {
+    EXPECT_FALSE(Path::is_child("/a/b", "/a/b"));
+    EXPECT_FALSE(Path::is_child("/a/b", "/a"));
+    EXPECT_FALSE(Path::is_child("/a/b", "/"));
+
+    EXPECT_TRUE(Path::is_child("/a/b", "/a/b/c"));
+    EXPECT_FALSE(Path::is_child("/a/b", "/a/b/c/d"));
+    EXPECT_FALSE(Path::is_child("/a/b", "/a/b/c/d/e"));
+}
+
+TEST(Path, RecognizeParent) {
+    EXPECT_FALSE(Path::is_parent("/a/b", "/a/b"));
+    EXPECT_TRUE(Path::is_parent("/a/b", "/a"));
+    EXPECT_FALSE(Path::is_parent("/a/b", "/"));
+
+    EXPECT_FALSE(Path::is_parent("/a/b", "/a/b/c"));
+    EXPECT_FALSE(Path::is_parent("/a/b", "/a/b/c/d"));
+    EXPECT_FALSE(Path::is_parent("/a/b", "/a/b/c/d/e"));
+}
+
+TEST(Path, GenerateNextChild) {
+    EXPECT_EQ("/a", Path::next_child("/", "/a/b/c/d/e"));
+    EXPECT_EQ("/a/b", Path::next_child("/a", "/a/b/c/d/e"));
+    EXPECT_EQ("/a/b/c", Path::next_child("/a/b", "/a/b/c/d/e"));
+    EXPECT_EQ("/a/b/c/d", Path::next_child("/a/b/c", "/a/b/c/d/e"));
+}
diff --git a/third_party/SimpleDBus/test/src/test_proxy_children.cpp b/third_party/SimpleDBus/test/src/test_proxy_children.cpp
new file mode 100644
index 000000000..ba0ccff19
--- /dev/null
+++ b/third_party/SimpleDBus/test/src/test_proxy_children.cpp
@@ -0,0 +1,109 @@
+#include <gtest/gtest.h>
+
+#include <simpledbus/advanced/Proxy.h>
+
+using namespace SimpleDBus;
+
+TEST(ProxyChildren, AppendChild) {
+    Proxy p = Proxy(nullptr, "", "/a/b");
+
+    p.path_add("/a/b/c", Holder());
+    EXPECT_EQ(1, p.children().size());
+    EXPECT_EQ(1, p.children().count("/a/b/c"));
+    EXPECT_EQ("/a/b/c", p.children().at("/a/b/c")->path());
+}
+
+TEST(ProxyChildren, AppendRepeatedChild) {
+    Proxy p = Proxy(nullptr, "", "/a/b");
+    p.path_add("/a/b/c", Holder());
+
+    // Attempting to add a child with the same name should be a no-op
+    p.path_add("/a/b/c", Holder());
+    EXPECT_EQ(1, p.children().size());
+}
+
+TEST(ProxyChildren, AppendExtendedChild) {
+    Proxy p = Proxy(nullptr, "", "/");
+    p.path_add("/a/b/c/d", Holder());
+
+    ASSERT_EQ(1, p.children().size());
+    ASSERT_EQ(1, p.children().count("/a"));
+
+    std::shared_ptr<Proxy> p_a = std::dynamic_pointer_cast<Proxy>(p.children().at("/a"));
+    ASSERT_EQ(1, p_a->children().size());
+    ASSERT_EQ(1, p_a->children().count("/a/b"));
+
+    std::shared_ptr<Proxy> p_a_b = std::dynamic_pointer_cast<Proxy>(p_a->children().at("/a/b"));
+    ASSERT_EQ(1, p_a_b->children().size());
+    ASSERT_EQ(1, p_a_b->children().count("/a/b/c"));
+
+    std::shared_ptr<Proxy> p_a_b_c = std::dynamic_pointer_cast<Proxy>(p_a_b->children().at("/a/b/c"));
+    ASSERT_EQ(1, p_a_b_c->children().size());
+    ASSERT_EQ(1, p_a_b_c->children().count("/a/b/c/d"));
+}
+
+TEST(ProxyChildren, RemoveSelf) {
+    Proxy p = Proxy(nullptr, "", "/");
+
+    // Should notify that the proxy can be safely deleted, as nothing worth keeping is left
+    ASSERT_TRUE(p.path_remove("/", Holder::create_array()));
+
+    // Add a child path without any interfaces
+    p.path_add("/a", Holder());
+
+    // Attempt to remove the path while holding a local copy of the child, should be a no-op
+    {
+        std::shared_ptr<Proxy> p_a = std::dynamic_pointer_cast<Proxy>(p.children().at("/a"));
+        // As there is another copy of the child, the proxy should not be deleted
+        ASSERT_FALSE(p.path_remove("/", Holder::create_array()));
+        ASSERT_EQ(1, p.children().size());
+    }
+
+    // Should notify that the proxy can be safely deleted, as nothing worth keeping is left
+    ASSERT_TRUE(p.path_remove("/", Holder::create_array()));
+    ASSERT_EQ(0, p.children().size());
+}
+
+TEST(ProxyChildren, RemoveChildNoInterfaces) {
+    Proxy p = Proxy(nullptr, "", "/");
+    p.path_add("/a", Holder());
+
+    // Attempt to remove the path while holding a local copy of the child, should be a no-op
+    {
+        std::shared_ptr<Proxy> p_a = std::dynamic_pointer_cast<Proxy>(p.children().at("/a"));
+        // As there is another copy of the child, the proxy should not be deleted
+        ASSERT_FALSE(p.path_remove("/a", Holder::create_array()));
+        ASSERT_EQ(1, p.children().size());
+    }
+
+    // As only the child was removed, the main proxy should not be deleted.
+    ASSERT_FALSE(p.path_remove("/a", Holder::create_array()));
+    ASSERT_EQ(0, p.children().size());
+}
+
+TEST(ProxyChildren, RemoveChildWithInterfaces) {
+    Proxy p = Proxy(nullptr, "", "/");
+
+    Holder managed_interfaces = Holder::create_dict();
+    managed_interfaces.dict_append(Holder::STRING, "i.1", Holder());
+    managed_interfaces.dict_append(Holder::STRING, "i.2", Holder());
+    p.path_add("/a", managed_interfaces);
+
+    Holder removed_interfaces = Holder::create_array();
+    removed_interfaces.array_append(Holder::create_string("i.2"));
+    p.path_remove("/a", removed_interfaces);
+
+    // Because /a has one interface still, it should still be in the children map.
+    ASSERT_EQ(1, p.children().size());
+    {
+        std::shared_ptr<Proxy> p_a = std::dynamic_pointer_cast<Proxy>(p.children().at("/a"));
+        ASSERT_EQ(1, p_a->interfaces_count());
+        ASSERT_EQ(1, p_a->interfaces().count("i.1"));
+    }
+
+    // Remove the second interface
+    removed_interfaces = Holder::create_array();
+    removed_interfaces.array_append(Holder::create_string("i.1"));
+    p.path_remove("/a", removed_interfaces);
+    ASSERT_EQ(0, p.children().size());
+}
diff --git a/third_party/SimpleDBus/test/src/test_proxy_interfaces.cpp b/third_party/SimpleDBus/test/src/test_proxy_interfaces.cpp
new file mode 100644
index 000000000..3d5dc80b0
--- /dev/null
+++ b/third_party/SimpleDBus/test/src/test_proxy_interfaces.cpp
@@ -0,0 +1,74 @@
+#include <gtest/gtest.h>
+
+#include <simpledbus/advanced/Proxy.h>
+
+using namespace SimpleDBus;
+
+TEST(ProxyInterfaces, LoadInterfaces) {
+    Holder managed_interfaces = Holder::create_dict();
+    managed_interfaces.dict_append(Holder::STRING, "i.1", Holder());
+
+    Proxy h = Proxy(nullptr, "", "/");
+    EXPECT_FALSE(h.interfaces_loaded());
+
+    // TODO: Check that all properties of the interface are correctly loaded.
+
+    h.interfaces_load(managed_interfaces);
+
+    EXPECT_TRUE(h.interfaces_loaded());
+    EXPECT_EQ(1, h.interfaces().size());
+    EXPECT_EQ(1, h.interfaces().count("i.1"));
+}
+
+TEST(ProxyInterfaces, UnloadInterfaces) {
+    Holder managed_interfaces = Holder::create_dict();
+    managed_interfaces.dict_append(Holder::STRING, "i.1", Holder());
+    managed_interfaces.dict_append(Holder::STRING, "i.2", Holder());
+    managed_interfaces.dict_append(Holder::STRING, "i.3", Holder());
+
+    Proxy h = Proxy(nullptr, "", "/");
+    h.interfaces_load(managed_interfaces);
+    EXPECT_EQ(3, h.interfaces_count());
+
+    Holder removed_interfaces = Holder::create_array();
+    removed_interfaces.array_append(Holder::create_string("i.3"));
+    h.interfaces_unload(removed_interfaces);
+
+    EXPECT_EQ(2, h.interfaces_count());
+    EXPECT_TRUE(h.interfaces_loaded());
+    EXPECT_FALSE(h.interfaces().at("i.3")->is_loaded());
+
+    removed_interfaces = Holder::create_array();
+    removed_interfaces.array_append(Holder::create_string("i.2"));
+    h.interfaces_unload(removed_interfaces);
+
+    EXPECT_EQ(1, h.interfaces_count());
+    EXPECT_TRUE(h.interfaces_loaded());
+    EXPECT_FALSE(h.interfaces().at("i.2")->is_loaded());
+
+    removed_interfaces = Holder::create_array();
+    removed_interfaces.array_append(Holder::create_string("i.1"));
+    h.interfaces_unload(removed_interfaces);
+
+    EXPECT_EQ(0, h.interfaces_count());
+    EXPECT_FALSE(h.interfaces_loaded());
+    EXPECT_FALSE(h.interfaces().at("i.1")->is_loaded());
+}
+
+TEST(ProxyInterfaces, ReloadInterfaces) {
+    Holder managed_interfaces = Holder::create_dict();
+    managed_interfaces.dict_append(Holder::STRING, "i.1", Holder());
+    managed_interfaces.dict_append(Holder::STRING, "i.2", Holder());
+    managed_interfaces.dict_append(Holder::STRING, "i.3", Holder());
+
+    Proxy h = Proxy(nullptr, "", "/");
+    h.interfaces_load(managed_interfaces);
+    EXPECT_EQ(3, h.interfaces_count());
+
+    managed_interfaces = Holder::create_dict();
+    managed_interfaces.dict_append(Holder::STRING, "i.1", Holder());
+    managed_interfaces.dict_append(Holder::STRING, "i.3", Holder());
+    h.interfaces_reload(managed_interfaces);
+
+    EXPECT_EQ(2, h.interfaces_count());
+}