1739
1740
1741
1742
@@ -1884,7 +2088,9 @@ address[0] is the LSB of the address, address[5] is the MSB.
1875
1876
1877
-1878 class Address :
+1878
+1879
+1880 class Address :
'''
Bluetooth Address (see Bluetooth spec Vol 6, Part B - 1.3 DEVICE ADDRESS)
NOTE: the address bytes are stored in little-endian byte order here, so
@@ -2063,9 +2269,7 @@ the type is set to PUBLIC_DEVICE_ADDRESS.
Source code in bumble/hci.py
- 1793
-1794
-1795
+ 1795
1796
1797
1798
@@ -2089,7 +2293,9 @@ the type is set to PUBLIC_DEVICE_ADDRESS.
1816
1817
1818
-1819 def __init__ (
+1819
+1820
+1821 def __init__ (
self , address : Union [ bytes , str ], address_type : int = RANDOM_DEVICE_ADDRESS
):
'''
@@ -2141,15 +2347,15 @@ qualifier.
Source code in bumble/hci.py
- 1855
-1856
-1857
+ 1857
1858
1859
1860
1861
1862
-1863 def to_string ( self , with_type_qualifier = True ):
+1863
+1864
+1865 def to_string ( self , with_type_qualifier = True ):
'''
String representation of the address, MSB first, with an optional type
qualifier.
@@ -2185,9 +2391,7 @@ qualifier.
Source code in bumble/hci.py
- 1909
-1910
-1911
+ 1911
1912
1913
1914
@@ -2214,7 +2418,9 @@ qualifier.
1935
1936
1937
-1938 class HCI_Packet :
+1938
+1939
+1940 class HCI_Packet :
'''
Abstract Base class for HCI packets
'''
@@ -2283,9 +2489,7 @@ qualifier.
Source code in bumble/hci.py
- 1953
-1954
-1955
+ 1955
1956
1957
1958
@@ -2402,7 +2606,9 @@ qualifier.
2069
2070
2071
-2072 class HCI_Command ( HCI_Packet ):
+2072
+2073
+2074 class HCI_Command ( HCI_Packet ):
'''
See Bluetooth spec @ Vol 2, Part E - 5.4.1 HCI Command Packet
'''
@@ -2559,9 +2765,7 @@ qualifier.
Source code in bumble/hci.py
- 1962
-1963
-1964
+ 1964
1965
1966
1967
@@ -2586,7 +2790,9 @@ qualifier.
1986
1987
1988
-1989 @staticmethod
+1989
+1990
+1991 @staticmethod
def command ( fields = (), return_parameters_fields = ()):
'''
Decorator used to declare and register subclasses
@@ -2643,16 +2849,16 @@ qualifier.
Source code in bumble/hci.py
- 2118
-2119
-2120
+ 2120
2121
2122
2123
2124
2125
2126
-2127 @HCI_Command . command (
+2127
+2128
+2129
diff --git a/apps_and_tools/console.html b/apps_and_tools/console.html
index 8af09b0e..4457639f 100644
--- a/apps_and_tools/console.html
+++ b/apps_and_tools/console.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/apps_and_tools/gatt_dump.html b/apps_and_tools/gatt_dump.html
index 80856298..eeeb6774 100644
--- a/apps_and_tools/gatt_dump.html
+++ b/apps_and_tools/gatt_dump.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/apps_and_tools/gg_bridge.html b/apps_and_tools/gg_bridge.html
index 325536b5..fdf7da49 100644
--- a/apps_and_tools/gg_bridge.html
+++ b/apps_and_tools/gg_bridge.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/apps_and_tools/hci_bridge.html b/apps_and_tools/hci_bridge.html
index 029f5d6e..cd37caf8 100644
--- a/apps_and_tools/hci_bridge.html
+++ b/apps_and_tools/hci_bridge.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/apps_and_tools/index.html b/apps_and_tools/index.html
index dd0d6833..566758d6 100644
--- a/apps_and_tools/index.html
+++ b/apps_and_tools/index.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/apps_and_tools/link_relay.html b/apps_and_tools/link_relay.html
index 3944a567..1ddd8e39 100644
--- a/apps_and_tools/link_relay.html
+++ b/apps_and_tools/link_relay.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/apps_and_tools/pair.html b/apps_and_tools/pair.html
index fd1681b2..ad683cc4 100644
--- a/apps_and_tools/pair.html
+++ b/apps_and_tools/pair.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/apps_and_tools/show.html b/apps_and_tools/show.html
index df2d766e..62689f9b 100644
--- a/apps_and_tools/show.html
+++ b/apps_and_tools/show.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/apps_and_tools/speaker.html b/apps_and_tools/speaker.html
index 3670b7e8..ae24b6ca 100644
--- a/apps_and_tools/speaker.html
+++ b/apps_and_tools/speaker.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/apps_and_tools/unbond.html b/apps_and_tools/unbond.html
index ac422e00..3ff076cd 100644
--- a/apps_and_tools/unbond.html
+++ b/apps_and_tools/unbond.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/apps_and_tools/usb_probe.html b/apps_and_tools/usb_probe.html
index ccdae560..33e98bf0 100644
--- a/apps_and_tools/usb_probe.html
+++ b/apps_and_tools/usb_probe.html
@@ -1728,6 +1728,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/components/controller.html b/components/controller.html
index 58947c99..15eb2449 100644
--- a/components/controller.html
+++ b/components/controller.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/components/gatt.html b/components/gatt.html
index 0db992ec..67f664a3 100644
--- a/components/gatt.html
+++ b/components/gatt.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/components/host.html b/components/host.html
index 61456f28..ff80af33 100644
--- a/components/host.html
+++ b/components/host.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/components/security_manager.html b/components/security_manager.html
index 04439b58..64e9c8c2 100644
--- a/components/security_manager.html
+++ b/components/security_manager.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/development/code_style.html b/development/code_style.html
index 3075be34..e7c967d8 100644
--- a/development/code_style.html
+++ b/development/code_style.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/development/contributing.html b/development/contributing.html
index 6db5933b..3285e23b 100644
--- a/development/contributing.html
+++ b/development/contributing.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/development/python_environments.html b/development/python_environments.html
index f9123011..dc13802c 100644
--- a/development/python_environments.html
+++ b/development/python_environments.html
@@ -1762,6 +1762,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/drivers/index.html b/drivers/index.html
index 621fee9c..44e052dd 100644
--- a/drivers/index.html
+++ b/drivers/index.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/drivers/realtek.html b/drivers/realtek.html
index 242904f4..7f353099 100644
--- a/drivers/realtek.html
+++ b/drivers/realtek.html
@@ -1735,6 +1735,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/examples/index.html b/examples/index.html
index 842a7e2c..512fca56 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -12,6 +12,8 @@
+
+
@@ -1873,6 +1875,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
@@ -2151,6 +2359,20 @@ for those characteristics at regular intervals.
+
+
diff --git a/extras/android_remote_hci.html b/extras/android_remote_hci.html
new file mode 100644
index 00000000..0a4f939e
--- /dev/null
+++ b/extras/android_remote_hci.html
@@ -0,0 +1,2341 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Android Remote HCI - Bumble
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ANDROID REMOTE HCI APP
+This application allows using an android phone's built-in Bluetooth controller with
+a Bumble host stack running outside the phone (typically a development laptop or desktop).
+The app runs an HCI proxy between a TCP socket on the "outside" and the Bluetooth HCI HAL
+on the "inside". (See this page for a high level
+description of the Android Bluetooth HCI HAL).
+The HCI packets received on the TCP socket are forwarded to the phone's controller, and the
+packets coming from the controller are forwarded to the TCP socket.
+Building
+You can build the app by running ./gradlew build (use gradlew.bat on Windows) from the RemoteHCI top level directory.
+You can also build with Android Studio: open the RemoteHCI project. You can build and/or debug from there.
+If the build succeeds, you can find the app APKs (debug and release) at:
+
+[Release] app/build/outputs/apk/release/app-release-unsigned.apk
+[Debug] app/build/outputs/apk/debug/app-debug.apk
+
+Running
+Preconditions
+When the proxy starts (tapping the "Start" button in the app's main activity), it will try to
+bind to the Bluetooth HAL. This requires disabling SELinux temporarily, and being the only HAL client.
+Disabling SELinux
+Binding to the Bluetooth HCI HAL requires certain SELinux permissions that can't simply be changed
+on a device without rebuilding its system image. To bypass these restrictions, you will need
+to disable SELinux on your phone (please be aware that this is global, not just for the proxy app,
+so proceed with caution).
+In order to disable SELinux, you need to root the phone (it may be advisable to do this on a
+development phone).
+
+
Disabling SELinux Temporarily
+
Restart adb as root:
+
+
Then disable SELinux
+
$ adb shell setenforce 0
+
+
Once you're done using the proxy, you can restore SELinux, if you need to, with
+
$ adb shell setenforce 1
+
+
This state will also reset to the normal SELinux enforcement when you reboot.
+
+Stopping the bluetooth process
+Since the Bluetooth HAL service can only accept one client, and that in normal conditions
+that client is the Android's bluetooth stack, it is required to first shut down the
+Android bluetooth stack process.
+
+
Checking if the Bluetooth process is running
+
$ adb shell "ps -A | grep com.google.android.bluetooth"
+
+If the process is running, you will get a line like:
+
bluetooth 10759 876 17455796 136620 do_epoll_wait 0 S com.google.android.bluetooth
+
+If you don't, it means that the process is not running and you are clear to proceed.
+
+Simply turning Bluetooth off from the phone's settings does not ensure that the bluetooth process will exit.
+If the bluetooth process is still running after toggling Bluetooth off from the settings, you may try enabling
+Airplane Mode, then rebooting. The bluetooth process should, in theory, not restart after the reboot.
+
+
Stopping the bluetooth process with adb
+
$ adb shell cmd bluetooth_manager disable
+
+
+Starting the app
+You can start the app from the Android launcher, from Android Studio, or with adb
+Launching from the launcher
+Just tap the app icon on the launcher, check the TCP port that is configured, and tap
+the "Start" button.
+Launching with adb
+Using the am command, you can start the activity, and pass it arguments so that you can
+automatically start the proxy, and/or set the port number.
+
+
Launching from adb with auto-start
+
$ adb shell am start -n com.github.google.bumble.remotehci/.MainActivity --ez autostart true
+
+
+
+
Launching from adb with auto-start and a port
+
In this example, we auto-start the proxy upon launch, with the port set to 9995
+
$ adb shell am start -n com.github.google.bumble.remotehci/.MainActivity --ez autostart true --ei port 9995
+
+
+Selecting a TCP port
+The RemoteHCI app's main activity has a "TCP Port" setting where you can change the port on
+which the proxy is accepting connections. If the default value isn't suitable, you can
+change it there (you can also use the special value 0 to let the OS assign a port number for you).
+Connecting to the proxy
+To connect the Bumble stack to the proxy, you need to be able to reach the phone's network
+stack. This can be done over the phone's WiFi connection, or, alternatively, using an adb
+TCP forward (which should be faster than over WiFi).
+
+
Forwarding TCP with adb
+
To connect to the proxy via an adb TCP forward, use:
+
$ adb forward tcp:<outside-port> tcp:<inside-port>
+
+Where
<outside-port> is the port number for a listening socket on your laptop or
+desktop machine, and
is the TCP port selected in the app's user interface.
+Those two ports may be the same, of course.
+For example, with the default TCP port 9993:
+$ adb forward tcp:9993 tcp:9993
+
+
+Once you've ensured that you can reach the proxy's TCP port on the phone, either directly or
+via an adb forward, you can then use it as a Bumble transport, using the transport name:
+tcp-client:<host>:<port> syntax.
+
+
Connecting a Bumble client
+
Connecting the bumble-controller-info app to the phone's controller.
+Assuming you have set up an adb forward on port 9993:
+
$ bumble-controller-info tcp-client:localhost:9993
+
+
Or over WiFi with, in this example, the IP address of the phone being 192.168.86.27
+
$ bumble-controller-info tcp-client:192.168.86.27:9993
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extras/index.html b/extras/index.html
new file mode 100644
index 00000000..18390dd6
--- /dev/null
+++ b/extras/index.html
@@ -0,0 +1,2072 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Overview - Bumble
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+A collection of add-ons, apps and tools, to the Bumble project.
+Android Remote HCI
+Allows using an Android phone's built-in Bluetooth controller with a Bumble
+stack running on a development machine.
+See Android Remote HCI for details.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/getting_started.html b/getting_started.html
index 94036242..7d74dc22 100644
--- a/getting_started.html
+++ b/getting_started.html
@@ -1689,6 +1689,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/hardware/index.html b/hardware/index.html
index 1f085a71..92759c45 100644
--- a/hardware/index.html
+++ b/hardware/index.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
@@ -1727,7 +1933,7 @@
HARDWARE
The Bumble Host connects to a controller over an HCI Transport .
To use a hardware controller attached to the host on which the host application is running, the transport is typically either HCI over UART or HCI over USB .
-On Linux, the VHCI Transport can be used to communicate with any controller hardware managed by the operating system. Alternatively, a remote controller (a phyiscal controller attached to a remote host) can be used by connecting one of the networked transports (such as the TCP Client transport , the TCP Server transport or the UDP Transport ) to an HCI Bridge bridging the network transport to a physical controller on a remote host.
+On Linux, the VHCI Transport can be used to communicate with any controller hardware managed by the operating system. Alternatively, a remote controller (a phyiscal controller attached to a remote host) can be used by connecting one of the networked transports (such as the TCP Client transport , the TCP Server transport or the UDP Transport ) to an HCI Bridge bridging the network transport to a physical controller on a remote host.
In theory, any controller that is compliant with the HCI over UART or HCI over USB protocols can be used.
HCI over USB is very common, implemented by a number of commercial Bluetooth dongles.
It is also possible to use an embedded development board, running a specialized application, such as the HCI UART and HCI USB demo applications from the Zephyr project , or the blehci application from mynewt/nimble
diff --git a/hive/index.html b/hive/index.html
new file mode 100644
index 00000000..0a1128cc
--- /dev/null
+++ b/hive/index.html
@@ -0,0 +1,2102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Overview - Bumble
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+HIVE
+Welcome to the Bumble Hive.
+This is a collection of apps and virtual devices that can run entirely in a browser page.
+The code for the apps and devices, as well as the Bumble runtime code, runs via Pyodide .
+Pyodide is a Python distribution for the browser and Node.js based on WebAssembly.
+The Bumble stack uses a WebSocket to exchange HCI packets with a virtual or physical
+Bluetooth controller.
+The apps and devices in the hive can be accessed by following the links below. Each
+page has a settings button that may be used to configure the WebSocket URL to use for
+the virutal HCI connection. This will typically be the WebSocket URL for a netsim
+daemon.
+There is also a TOML index that can be used by tools to know at which URL to access
+each of the apps and devices, as well as their names and short desciptions.
+Applications
+
+Scanner - Scans for BLE devices.
+
+Virtual Devices
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hive/index.toml b/hive/index.toml
new file mode 100644
index 00000000..5b187e39
--- /dev/null
+++ b/hive/index.toml
@@ -0,0 +1,21 @@
+version = "1.0.0"
+base_url = "https://google.github.io/bumble/hive/web"
+default_hci_query_param = "hci"
+
+[[index]]
+name = "speaker"
+description = "Bumble Virtual Speaker"
+type = "Device"
+url = "speaker/speaker.html"
+
+[[index]]
+name = "scanner"
+description = "Simple Scanner Application"
+type = "Application"
+url = "scanner/scanner.html"
+
+[[index]]
+name = "heart-rate-monitor"
+description = "Virtual Heart Rate Monitor"
+type = "Device"
+url = "heart_rate_monitor/heart_rate_monitor.html"
diff --git a/hive/web/bumble.js b/hive/web/bumble.js
new file mode 100644
index 00000000..cb807eb7
--- /dev/null
+++ b/hive/web/bumble.js
@@ -0,0 +1,188 @@
+function bufferToHex(buffer) {
+ return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join('');
+}
+
+class PacketSource {
+ constructor(pyodide) {
+ this.parser = pyodide.runPython(`
+ from bumble.transport.common import PacketParser
+ class ProxiedPacketParser(PacketParser):
+ def feed_data(self, js_data):
+ super().feed_data(bytes(js_data.to_py()))
+ ProxiedPacketParser()
+ `);
+ }
+
+ set_packet_sink(sink) {
+ this.parser.set_packet_sink(sink);
+ }
+
+ data_received(data) {
+ //console.log(`HCI[controller->host]: ${bufferToHex(data)}`);
+ this.parser.feed_data(data);
+ }
+}
+
+class PacketSink {
+ on_packet(packet) {
+ if (!this.writer) {
+ return;
+ }
+ const buffer = packet.toJs({create_proxies : false});
+ packet.destroy();
+ //console.log(`HCI[host->controller]: ${bufferToHex(buffer)}`);
+ // TODO: create an async queue here instead of blindly calling write without awaiting
+ this.writer(buffer);
+ }
+}
+
+class LogEvent extends Event {
+ constructor(message) {
+ super('log');
+ this.message = message;
+ }
+}
+
+export class Bumble extends EventTarget {
+ constructor(pyodide) {
+ super();
+ this.pyodide = pyodide;
+ }
+
+ async loadRuntime(bumblePackage) {
+ // Load pyodide if it isn't provided.
+ if (this.pyodide === undefined) {
+ this.log('Loading Pyodide');
+ this.pyodide = await loadPyodide();
+ }
+
+ // Load the Bumble module
+ bumblePackage ||= 'bumble';
+ console.log('Installing micropip');
+ this.log(`Installing ${bumblePackage}`)
+ await this.pyodide.loadPackage('micropip');
+ await this.pyodide.runPythonAsync(`
+ import micropip
+ await micropip.install('${bumblePackage}')
+ package_list = micropip.list()
+ print(package_list)
+ `)
+
+ // Mount a filesystem so that we can persist data like the Key Store
+ let mountDir = '/bumble';
+ this.pyodide.FS.mkdir(mountDir);
+ this.pyodide.FS.mount(this.pyodide.FS.filesystems.IDBFS, { root: '.' }, mountDir);
+
+ // Sync previously persisted filesystem data into memory
+ await new Promise(resolve => {
+ this.pyodide.FS.syncfs(true, () => {
+ console.log('FS synced in');
+ resolve();
+ });
+ })
+
+ // Setup the HCI source and sink
+ this.packetSource = new PacketSource(this.pyodide);
+ this.packetSink = new PacketSink();
+ }
+
+ log(message) {
+ this.dispatchEvent(new LogEvent(message));
+ }
+
+ async connectWebSocketTransport(hciWsUrl) {
+ return new Promise((resolve, reject) => {
+ let resolved = false;
+
+ let ws = new WebSocket(hciWsUrl);
+ ws.binaryType = 'arraybuffer';
+
+ ws.onopen = () => {
+ this.log('WebSocket open');
+ resolve();
+ resolved = true;
+ }
+
+ ws.onclose = () => {
+ this.log('WebSocket close');
+ if (!resolved) {
+ reject(`Failed to connect to ${hciWsUrl}`);
+ }
+ }
+
+ ws.onmessage = (event) => {
+ this.packetSource.data_received(event.data);
+ }
+
+ this.packetSink.writer = (packet) => {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(packet);
+ }
+ }
+ this.closeTransport = async () => {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.close();
+ }
+ }
+ })
+ }
+
+ async loadApp(appUrl) {
+ this.log('Loading app');
+ const script = await (await fetch(appUrl)).text();
+ await this.pyodide.runPythonAsync(script);
+ const pythonMain = this.pyodide.globals.get('main');
+ const app = await pythonMain(this.packetSource, this.packetSink);
+ if (app.on) {
+ app.on('key_store_update', this.onKeystoreUpdate.bind(this));
+ }
+ this.log('App is ready!');
+ return app;
+ }
+
+ onKeystoreUpdate() {
+ // Sync the FS
+ this.pyodide.FS.syncfs(() => {
+ console.log('FS synced out');
+ });
+ }
+}
+
+export async function setupSimpleApp(appUrl, bumbleControls, log) {
+ // Load Bumble
+ log('Loading Bumble');
+ const bumble = new Bumble();
+ bumble.addEventListener('log', (event) => {
+ log(event.message);
+ })
+ const params = (new URL(document.location)).searchParams;
+ await bumble.loadRuntime(params.get('package'));
+
+ log('Bumble is ready!')
+ const app = await bumble.loadApp(appUrl);
+
+ bumbleControls.connector = async (hciWsUrl) => {
+ try {
+ // Connect the WebSocket HCI transport
+ await bumble.connectWebSocketTransport(hciWsUrl);
+
+ // Start the app
+ await app.start();
+
+ return true;
+ } catch (err) {
+ log(err);
+ return false;
+ }
+ }
+ bumbleControls.stopper = async () => {
+ // Stop the app
+ await app.stop();
+
+ // Close the HCI transport
+ await bumble.closeTransport();
+ }
+ bumbleControls.onBumbleLoaded();
+
+ return app;
+}
\ No newline at end of file
diff --git a/hive/web/heart_rate_monitor/heart_rate_monitor.html b/hive/web/heart_rate_monitor/heart_rate_monitor.html
new file mode 100644
index 00000000..f44470fd
--- /dev/null
+++ b/hive/web/heart_rate_monitor/heart_rate_monitor.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ cardiology
+
+ 60
+
+
arrow_upward
+
arrow_downward
+
+
+
+
diff --git a/hive/web/heart_rate_monitor/heart_rate_monitor.js b/hive/web/heart_rate_monitor/heart_rate_monitor.js
new file mode 100644
index 00000000..468e728c
--- /dev/null
+++ b/hive/web/heart_rate_monitor/heart_rate_monitor.js
@@ -0,0 +1,30 @@
+import {setupSimpleApp} from '../bumble.js';
+
+const logOutput = document.querySelector('#log-output');
+function logToOutput(message) {
+ console.log(message);
+ logOutput.value += message + '\n';
+}
+
+let heartRate = 60;
+const heartRateText = document.querySelector('#hr-value')
+
+function setHeartRate(newHeartRate) {
+ heartRate = newHeartRate;
+ heartRateText.innerHTML = heartRate;
+ app.set_heart_rate(heartRate);
+}
+
+// Setup the UI
+const bumbleControls = document.querySelector('#bumble-controls');
+document.querySelector('#hr-up-button').addEventListener('click', () => {
+ setHeartRate(heartRate + 1);
+})
+document.querySelector('#hr-down-button').addEventListener('click', () => {
+ setHeartRate(heartRate - 1);
+})
+
+// Setup the app
+const app = await setupSimpleApp('heart_rate_monitor.py', bumbleControls, logToOutput);
+logToOutput('Click the Bluetooth button to start');
+
diff --git a/hive/web/heart_rate_monitor/heart_rate_monitor.py b/hive/web/heart_rate_monitor/heart_rate_monitor.py
new file mode 100644
index 00000000..4a843b46
--- /dev/null
+++ b/hive/web/heart_rate_monitor/heart_rate_monitor.py
@@ -0,0 +1,119 @@
+# Copyright 2021-2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# -----------------------------------------------------------------------------
+# Imports
+# -----------------------------------------------------------------------------
+import struct
+
+from bumble.core import AdvertisingData
+from bumble.device import Device
+from bumble.hci import HCI_Reset_Command
+from bumble.profiles.device_information_service import DeviceInformationService
+from bumble.profiles.heart_rate_service import HeartRateService
+from bumble.utils import AsyncRunner
+
+
+# -----------------------------------------------------------------------------
+class HeartRateMonitor:
+ def __init__(self, hci_source, hci_sink):
+ self.heart_rate = 60
+
+ self.device = Device.with_hci(
+ 'Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink
+ )
+
+ device_information_service = DeviceInformationService(
+ manufacturer_name='ACME',
+ model_number='HR-102',
+ serial_number='7654321',
+ hardware_revision='1.1.3',
+ software_revision='2.5.6',
+ system_id=(0x123456, 0x8877665544),
+ )
+
+ self.heart_rate_service = HeartRateService(
+ read_heart_rate_measurement=lambda _: HeartRateService.HeartRateMeasurement(
+ heart_rate=self.heart_rate,
+ sensor_contact_detected=True,
+ ),
+ body_sensor_location=HeartRateService.BodySensorLocation.WRIST,
+ reset_energy_expended=self.reset_energy_expended,
+ )
+
+ # Notify subscribers of the current value as soon as they subscribe
+ @self.heart_rate_service.heart_rate_measurement_characteristic.on(
+ 'subscription'
+ )
+ def on_subscription(_, notify_enabled, indicate_enabled):
+ if notify_enabled or indicate_enabled:
+ self.notify_heart_rate()
+
+ self.device.add_services([device_information_service, self.heart_rate_service])
+
+ self.device.advertising_data = bytes(
+ AdvertisingData(
+ [
+ (
+ AdvertisingData.FLAGS,
+ bytes(
+ [
+ AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
+ | AdvertisingData.BR_EDR_NOT_SUPPORTED_FLAG
+ ]
+ ),
+ ),
+ (
+ AdvertisingData.COMPLETE_LOCAL_NAME,
+ bytes('Bumble Heart', 'utf-8'),
+ ),
+ (
+ AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
+ bytes(self.heart_rate_service.uuid),
+ ),
+ (AdvertisingData.APPEARANCE, struct.pack('
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hive/web/scanner/scanner.py b/hive/web/scanner/scanner.py
new file mode 100644
index 00000000..e498f7a1
--- /dev/null
+++ b/hive/web/scanner/scanner.py
@@ -0,0 +1,72 @@
+# Copyright 2021-2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# -----------------------------------------------------------------------------
+# Imports
+# -----------------------------------------------------------------------------
+from bumble.device import Device
+from bumble.hci import HCI_Reset_Command
+
+
+# -----------------------------------------------------------------------------
+class Scanner:
+ class ScanEntry:
+ def __init__(self, advertisement):
+ self.address = advertisement.address.to_string(False)
+ self.address_type = (
+ 'Public',
+ 'Random',
+ 'Public Identity',
+ 'Random Identity',
+ )[advertisement.address.address_type]
+ self.rssi = advertisement.rssi
+ self.data = advertisement.data.to_string('\n')
+
+ def __init__(self, hci_source, hci_sink):
+ super().__init__()
+ self.device = Device.with_hci(
+ 'Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink
+ )
+ self.scan_entries = {}
+ self.listeners = {}
+ self.device.on('advertisement', self.on_advertisement)
+
+ async def start(self):
+ print('### Starting Scanner')
+ self.scan_entries = {}
+ self.emit_update()
+ await self.device.power_on()
+ await self.device.start_scanning()
+ print('### Scanner started')
+
+ async def stop(self):
+ # TODO: replace this once a proper reset is implemented in the lib.
+ await self.device.host.send_command(HCI_Reset_Command())
+ await self.device.power_off()
+ print('### Scanner stopped')
+
+ def emit_update(self):
+ if listener := self.listeners.get('update'):
+ listener(list(self.scan_entries.values()))
+
+ def on(self, event_name, listener):
+ self.listeners[event_name] = listener
+
+ def on_advertisement(self, advertisement):
+ self.scan_entries[advertisement.address] = self.ScanEntry(advertisement)
+ self.emit_update()
+
+# -----------------------------------------------------------------------------
+def main(hci_source, hci_sink):
+ return Scanner(hci_source, hci_sink)
diff --git a/hive/web/speaker/logo.svg b/hive/web/speaker/logo.svg
new file mode 100644
index 00000000..70ef7a90
--- /dev/null
+++ b/hive/web/speaker/logo.svg
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hive/web/speaker/speaker.css b/hive/web/speaker/speaker.css
new file mode 100644
index 00000000..95860548
--- /dev/null
+++ b/hive/web/speaker/speaker.css
@@ -0,0 +1,85 @@
+body, h1, h2, h3, h4, h5, h6 {
+ font-family: sans-serif;
+}
+
+#controlsDiv {
+ margin: 6px;
+}
+
+#errorText {
+ background-color: rgb(239, 89, 75);
+ border: none;
+ border-radius: 4px;
+ padding: 8px;
+ display: none;
+ margin: 4px;
+}
+
+#progressText {
+ background-color: rgb(179, 208, 146);
+ border: none;
+ border-radius: 4px;
+ padding: 8px;
+ display: none;
+ margin: 4px;
+}
+
+#startButton {
+ padding: 4px;
+ margin: 6px;
+}
+
+#fftCanvas {
+ border-radius: 16px;
+ margin: 6px;
+}
+
+#bandwidthCanvas {
+ border: grey;
+ border-style: solid;
+ border-radius: 8px;
+ margin: 6px;
+}
+
+#streamStateText {
+ background-color: rgb(93, 165, 93);
+ border: none;
+ border-radius: 8px;
+ padding: 10px 20px;
+ display: inline-block;
+ margin: 6px;
+}
+
+#connectionStateText {
+ background-color: rgb(112, 146, 206);
+ border: none;
+ border-radius: 8px;
+ padding: 10px 20px;
+ display: inline-block;
+ margin: 6px;
+}
+
+#propertiesTable {
+ border: grey;
+ border-style: solid;
+ border-radius: 4px;
+ padding: 4px;
+ margin: 6px;
+ margin-left: 0px;
+}
+
+th, td {
+ padding-left: 6px;
+ padding-right: 6px;
+}
+
+.properties td:nth-child(even) {
+ background-color: #D6EEEE;
+ font-family: monospace;
+}
+
+.properties td:nth-child(odd) {
+ font-weight: bold;
+}
+
+.properties tr td:nth-child(2) { width: 150px; }
\ No newline at end of file
diff --git a/hive/web/speaker/speaker.html b/hive/web/speaker/speaker.html
new file mode 100644
index 00000000..750df9d0
--- /dev/null
+++ b/hive/web/speaker/speaker.html
@@ -0,0 +1,35 @@
+
+
+
+ Bumble Speaker
+
+
+
+
+
+
+ Bumble Virtual Speaker
+
+
+
+
+ Codec
+ Packets
+ Bytes
+
+
+
+ Bandwidth Graph
+
+
+
IDLE
+
NOT CONNECTED
+
+
Audio Frequencies Animation
+
+
+
+
\ No newline at end of file
diff --git a/hive/web/speaker/speaker.js b/hive/web/speaker/speaker.js
new file mode 100644
index 00000000..12189a47
--- /dev/null
+++ b/hive/web/speaker/speaker.js
@@ -0,0 +1,221 @@
+import {setupSimpleApp} from '../bumble.js';
+
+(function () {
+ 'use strict';
+
+ let codecText;
+ let packetsReceivedText;
+ let bytesReceivedText;
+ let streamStateText;
+ let connectionStateText;
+ let audioOnButton;
+ let mediaSource;
+ let sourceBuffer;
+ let audioElement;
+ let audioContext;
+ let audioAnalyzer;
+ let audioFrequencyBinCount;
+ let audioFrequencyData;
+ let packetsReceived = 0;
+ let bytesReceived = 0;
+ let audioState = 'stopped';
+ let streamState = 'IDLE';
+ let fftCanvas;
+ let fftCanvasContext;
+ let bandwidthCanvas;
+ let bandwidthCanvasContext;
+ let bandwidthBinCount;
+ let bandwidthBins = [];
+
+ const FFT_WIDTH = 800;
+ const FFT_HEIGHT = 256;
+ const BANDWIDTH_WIDTH = 500;
+ const BANDWIDTH_HEIGHT = 100;
+
+
+ function init() {
+ initUI();
+ initMediaSource();
+ initAudioElement();
+ initAnalyzer();
+ initBumble();
+ }
+
+ function initUI() {
+ audioOnButton = document.getElementById('audioOnButton');
+ codecText = document.getElementById('codecText');
+ packetsReceivedText = document.getElementById('packetsReceivedText');
+ bytesReceivedText = document.getElementById('bytesReceivedText');
+ streamStateText = document.getElementById('streamStateText');
+ connectionStateText = document.getElementById('connectionStateText');
+
+ audioOnButton.onclick = startAudio;
+
+ codecText.innerText = 'AAC';
+
+ requestAnimationFrame(onAnimationFrame);
+ }
+
+ function initMediaSource() {
+ mediaSource = new MediaSource();
+ mediaSource.onsourceopen = onMediaSourceOpen;
+ mediaSource.onsourceclose = onMediaSourceClose;
+ mediaSource.onsourceended = onMediaSourceEnd;
+ }
+
+ function initAudioElement() {
+ audioElement = document.getElementById('audio');
+ audioElement.src = URL.createObjectURL(mediaSource);
+ // audioElement.controls = true;
+ }
+
+ function initAnalyzer() {
+ fftCanvas = document.getElementById('fftCanvas');
+ fftCanvas.width = FFT_WIDTH
+ fftCanvas.height = FFT_HEIGHT
+ fftCanvasContext = fftCanvas.getContext('2d');
+ fftCanvasContext.fillStyle = 'rgb(0, 0, 0)';
+ fftCanvasContext.fillRect(0, 0, FFT_WIDTH, FFT_HEIGHT);
+
+ bandwidthCanvas = document.getElementById('bandwidthCanvas');
+ bandwidthCanvas.width = BANDWIDTH_WIDTH
+ bandwidthCanvas.height = BANDWIDTH_HEIGHT
+ bandwidthCanvasContext = bandwidthCanvas.getContext('2d');
+ bandwidthCanvasContext.fillStyle = 'rgb(255, 255, 255)';
+ bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
+ }
+
+ async function initBumble() {
+ const bumbleControls = document.querySelector('#bumble-controls');
+ const app = await setupSimpleApp('speaker.py', bumbleControls, console.log);
+ app.on('start', onStart);
+ app.on('stop', onStop);
+ app.on('suspend', onSuspend);
+ app.on('connection', onConnection);
+ app.on('disconnection', onDisconnection);
+ app.on('audio', onAudio);
+ }
+
+ function startAnalyzer() {
+ // FFT
+ if (audioElement.captureStream !== undefined) {
+ audioContext = new AudioContext();
+ audioAnalyzer = audioContext.createAnalyser();
+ audioAnalyzer.fftSize = 128;
+ audioFrequencyBinCount = audioAnalyzer.frequencyBinCount;
+ audioFrequencyData = new Uint8Array(audioFrequencyBinCount);
+ const stream = audioElement.captureStream();
+ const source = audioContext.createMediaStreamSource(stream);
+ source.connect(audioAnalyzer);
+ }
+
+ // Bandwidth
+ bandwidthBinCount = BANDWIDTH_WIDTH / 2;
+ bandwidthBins = [];
+ }
+
+ function setStreamState(state) {
+ streamState = state;
+ streamStateText.innerText = streamState;
+ }
+
+ function onAnimationFrame() {
+ // FFT
+ if (audioAnalyzer !== undefined) {
+ audioAnalyzer.getByteFrequencyData(audioFrequencyData);
+ fftCanvasContext.fillStyle = 'rgb(0, 0, 0)';
+ fftCanvasContext.fillRect(0, 0, FFT_WIDTH, FFT_HEIGHT);
+ const barCount = audioFrequencyBinCount;
+ const barWidth = (FFT_WIDTH / audioFrequencyBinCount) - 1;
+ for (let bar = 0; bar < barCount; bar++) {
+ const barHeight = audioFrequencyData[bar];
+ fftCanvasContext.fillStyle = `rgb(${barHeight / 256 * 200 + 50}, 50, ${50 + 2 * bar})`;
+ fftCanvasContext.fillRect(bar * (barWidth + 1), FFT_HEIGHT - barHeight, barWidth, barHeight);
+ }
+ }
+
+ // Bandwidth
+ bandwidthCanvasContext.fillStyle = 'rgb(255, 255, 255)';
+ bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
+ bandwidthCanvasContext.fillStyle = `rgb(100, 100, 100)`;
+ for (let t = 0; t < bandwidthBins.length; t++) {
+ const lineHeight = (bandwidthBins[t] / 1000) * BANDWIDTH_HEIGHT;
+ bandwidthCanvasContext.fillRect(t * 2, BANDWIDTH_HEIGHT - lineHeight, 2, lineHeight);
+ }
+
+ // Display again at the next frame
+ requestAnimationFrame(onAnimationFrame);
+ }
+
+ function onMediaSourceOpen() {
+ console.log(this.readyState);
+ sourceBuffer = mediaSource.addSourceBuffer('audio/aac');
+ }
+
+ function onMediaSourceClose() {
+ console.log(this.readyState);
+ }
+
+ function onMediaSourceEnd() {
+ console.log(this.readyState);
+ }
+
+ async function startAudio() {
+ try {
+ console.log('starting audio...');
+ audioOnButton.disabled = true;
+ audioState = 'starting';
+ await audioElement.play();
+ console.log('audio started');
+ audioState = 'playing';
+ startAnalyzer();
+ } catch (error) {
+ console.error(`play failed: ${error}`);
+ audioState = 'stopped';
+ audioOnButton.disabled = false;
+ }
+ }
+
+ function onStart() {
+ setStreamState('STARTED');
+ }
+
+ function onStop() {
+ setStreamState('STOPPED');
+ }
+
+ function onSuspend() {
+ setStreamState('SUSPENDED');
+ }
+
+ function onConnection(params) {
+ connectionStateText.innerText = `CONNECTED: ${params.get('peer_name')} (${params.get('peer_address')})`;
+ }
+
+ function onDisconnection(params) {
+ connectionStateText.innerText = 'DISCONNECTED';
+ }
+
+ function onAudio(python_packet) {
+ const packet = python_packet.toJs({create_proxies : false});
+ python_packet.destroy();
+ if (audioState != 'stopped') {
+ // Queue the audio packet.
+ sourceBuffer.appendBuffer(packet);
+ }
+
+ packetsReceived += 1;
+ packetsReceivedText.innerText = packetsReceived;
+ bytesReceived += packet.byteLength;
+ bytesReceivedText.innerText = bytesReceived;
+
+ bandwidthBins[bandwidthBins.length] = packet.byteLength;
+ if (bandwidthBins.length > bandwidthBinCount) {
+ bandwidthBins.shift();
+ }
+ }
+
+ window.onload = (event) => {
+ init();
+ }
+}());
\ No newline at end of file
diff --git a/hive/web/speaker/speaker.py b/hive/web/speaker/speaker.py
new file mode 100644
index 00000000..4157981f
--- /dev/null
+++ b/hive/web/speaker/speaker.py
@@ -0,0 +1,325 @@
+# Copyright 2021-2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# -----------------------------------------------------------------------------
+# Imports
+# -----------------------------------------------------------------------------
+from __future__ import annotations
+import enum
+import logging
+from typing import Dict, List
+
+from bumble.core import BT_BR_EDR_TRANSPORT, CommandTimeoutError
+from bumble.device import Device, DeviceConfiguration
+from bumble.pairing import PairingConfig
+from bumble.sdp import ServiceAttribute
+from bumble.avdtp import (
+ AVDTP_AUDIO_MEDIA_TYPE,
+ Listener,
+ MediaCodecCapabilities,
+ MediaPacket,
+ Protocol,
+)
+from bumble.a2dp import (
+ make_audio_sink_service_sdp_records,
+ MPEG_2_AAC_LC_OBJECT_TYPE,
+ A2DP_SBC_CODEC_TYPE,
+ A2DP_MPEG_2_4_AAC_CODEC_TYPE,
+ SBC_MONO_CHANNEL_MODE,
+ SBC_DUAL_CHANNEL_MODE,
+ SBC_SNR_ALLOCATION_METHOD,
+ SBC_LOUDNESS_ALLOCATION_METHOD,
+ SBC_STEREO_CHANNEL_MODE,
+ SBC_JOINT_STEREO_CHANNEL_MODE,
+ SbcMediaCodecInformation,
+ AacMediaCodecInformation,
+)
+from bumble.utils import AsyncRunner
+from bumble.codecs import AacAudioRtpPacket
+from bumble.hci import HCI_Reset_Command
+
+
+# -----------------------------------------------------------------------------
+# Logging
+# -----------------------------------------------------------------------------
+logger = logging.getLogger(__name__)
+
+
+# -----------------------------------------------------------------------------
+class AudioExtractor:
+ @staticmethod
+ def create(codec: str):
+ if codec == 'aac':
+ return AacAudioExtractor()
+ if codec == 'sbc':
+ return SbcAudioExtractor()
+
+ def extract_audio(self, packet: MediaPacket) -> bytes:
+ raise NotImplementedError()
+
+
+# -----------------------------------------------------------------------------
+class AacAudioExtractor:
+ def extract_audio(self, packet: MediaPacket) -> bytes:
+ return AacAudioRtpPacket(packet.payload).to_adts()
+
+
+# -----------------------------------------------------------------------------
+class SbcAudioExtractor:
+ def extract_audio(self, packet: MediaPacket) -> bytes:
+ # header = packet.payload[0]
+ # fragmented = header >> 7
+ # start = (header >> 6) & 0x01
+ # last = (header >> 5) & 0x01
+ # number_of_frames = header & 0x0F
+
+ # TODO: support fragmented payloads
+ return packet.payload[1:]
+
+
+# -----------------------------------------------------------------------------
+class Speaker:
+ class StreamState(enum.Enum):
+ IDLE = 0
+ STOPPED = 1
+ STARTED = 2
+ SUSPENDED = 3
+
+ def __init__(self, hci_source, hci_sink, codec):
+ self.hci_source = hci_source
+ self.hci_sink = hci_sink
+ self.js_listeners = {}
+ self.codec = codec
+ self.device = None
+ self.connection = None
+ self.avdtp_listener = None
+ self.packets_received = 0
+ self.bytes_received = 0
+ self.stream_state = Speaker.StreamState.IDLE
+ self.audio_extractor = AudioExtractor.create(codec)
+
+ def sdp_records(self) -> Dict[int, List[ServiceAttribute]]:
+ service_record_handle = 0x00010001
+ return {
+ service_record_handle: make_audio_sink_service_sdp_records(
+ service_record_handle
+ )
+ }
+
+ def codec_capabilities(self) -> MediaCodecCapabilities:
+ if self.codec == 'aac':
+ return self.aac_codec_capabilities()
+
+ if self.codec == 'sbc':
+ return self.sbc_codec_capabilities()
+
+ raise RuntimeError('unsupported codec')
+
+ def aac_codec_capabilities(self) -> MediaCodecCapabilities:
+ return MediaCodecCapabilities(
+ media_type=AVDTP_AUDIO_MEDIA_TYPE,
+ media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
+ media_codec_information=AacMediaCodecInformation.from_lists(
+ object_types=[MPEG_2_AAC_LC_OBJECT_TYPE],
+ sampling_frequencies=[48000, 44100],
+ channels=[1, 2],
+ vbr=1,
+ bitrate=256000,
+ ),
+ )
+
+ def sbc_codec_capabilities(self) -> MediaCodecCapabilities:
+ return MediaCodecCapabilities(
+ media_type=AVDTP_AUDIO_MEDIA_TYPE,
+ media_codec_type=A2DP_SBC_CODEC_TYPE,
+ media_codec_information=SbcMediaCodecInformation.from_lists(
+ sampling_frequencies=[48000, 44100, 32000, 16000],
+ channel_modes=[
+ SBC_MONO_CHANNEL_MODE,
+ SBC_DUAL_CHANNEL_MODE,
+ SBC_STEREO_CHANNEL_MODE,
+ SBC_JOINT_STEREO_CHANNEL_MODE,
+ ],
+ block_lengths=[4, 8, 12, 16],
+ subbands=[4, 8],
+ allocation_methods=[
+ SBC_LOUDNESS_ALLOCATION_METHOD,
+ SBC_SNR_ALLOCATION_METHOD,
+ ],
+ minimum_bitpool_value=2,
+ maximum_bitpool_value=53,
+ ),
+ )
+
+ def on_key_store_update(self):
+ print("Key Store updated")
+ self.emit('key_store_update')
+
+ def on_bluetooth_connection(self, connection):
+ print(f'Connection: {connection}')
+ self.connection = connection
+ connection.on('disconnection', self.on_bluetooth_disconnection)
+ peer_name = '' if connection.peer_name is None else connection.peer_name
+ peer_address = connection.peer_address.to_string(False)
+ self.emit(
+ 'connection', {'peer_name': peer_name, 'peer_address': peer_address}
+ )
+
+ def on_bluetooth_disconnection(self, reason):
+ print(f'Disconnection ({reason})')
+ self.connection = None
+ self.emit('disconnection', None)
+
+ def on_avdtp_connection(self, protocol):
+ print('Audio Stream Open')
+
+ # Add a sink endpoint to the server
+ sink = protocol.add_sink(self.codec_capabilities())
+ sink.on('start', self.on_sink_start)
+ sink.on('stop', self.on_sink_stop)
+ sink.on('suspend', self.on_sink_suspend)
+ sink.on('configuration', lambda: self.on_sink_configuration(sink.configuration))
+ sink.on('rtp_packet', self.on_rtp_packet)
+ sink.on('rtp_channel_open', self.on_rtp_channel_open)
+ sink.on('rtp_channel_close', self.on_rtp_channel_close)
+
+ # Listen for close events
+ protocol.on('close', self.on_avdtp_close)
+
+ def on_avdtp_close(self):
+ print("Audio Stream Closed")
+
+ def on_sink_start(self):
+ print("Sink Started")
+ self.stream_state = self.StreamState.STARTED
+ self.emit('start', None)
+
+ def on_sink_stop(self):
+ print("Sink Stopped")
+ self.stream_state = self.StreamState.STOPPED
+ self.emit('stop', None)
+
+ def on_sink_suspend(self):
+ print("Sink Suspended")
+ self.stream_state = self.StreamState.SUSPENDED
+ self.emit('suspend', None)
+
+ def on_sink_configuration(self, config):
+ print("Sink Configuration:")
+ print('\n'.join([" " + str(capability) for capability in config]))
+
+ def on_rtp_channel_open(self):
+ print("RTP Channel Open")
+
+ def on_rtp_channel_close(self):
+ print("RTP Channel Closed")
+ self.stream_state = self.StreamState.IDLE
+
+ def on_rtp_packet(self, packet):
+ self.packets_received += 1
+ self.bytes_received += len(packet.payload)
+ self.emit("audio", self.audio_extractor.extract_audio(packet))
+
+ async def connect(self, address):
+ # Connect to the source
+ print(f'=== Connecting to {address}...')
+ connection = await self.device.connect(address, transport=BT_BR_EDR_TRANSPORT)
+ print(f'=== Connected to {connection.peer_address}')
+
+ # Request authentication
+ print('*** Authenticating...')
+ await connection.authenticate()
+ print('*** Authenticated')
+
+ # Enable encryption
+ print('*** Enabling encryption...')
+ await connection.encrypt()
+ print('*** Encryption on')
+
+ protocol = await Protocol.connect(connection)
+ self.avdtp_listener.set_server(connection, protocol)
+ self.on_avdtp_connection(protocol)
+
+ async def discover_remote_endpoints(self, protocol):
+ endpoints = await protocol.discover_remote_endpoints()
+ print(f'@@@ Found {len(endpoints)} endpoints')
+ for endpoint in endpoints:
+ print('@@@', endpoint)
+
+ def on(self, event_name, listener):
+ self.js_listeners[event_name] = listener
+
+ def emit(self, event_name, event=None):
+ if listener := self.js_listeners.get(event_name):
+ listener(event)
+
+ async def run(self, connect_address):
+ # Create a device
+ device_config = DeviceConfiguration()
+ device_config.name = "Bumble Speaker"
+ device_config.class_of_device = 0x240414
+ device_config.keystore = "JsonKeyStore:/bumble/keystore.json"
+ device_config.classic_enabled = True
+ device_config.le_enabled = False
+ self.device = Device.from_config_with_hci(
+ device_config, self.hci_source, self.hci_sink
+ )
+
+ # Setup the SDP to expose the sink service
+ self.device.sdp_service_records = self.sdp_records()
+
+ # Don't require MITM when pairing.
+ self.device.pairing_config_factory = lambda connection: PairingConfig(
+ mitm=False
+ )
+
+ # Start the controller
+ await self.device.power_on()
+
+ # Listen for Bluetooth connections
+ self.device.on('connection', self.on_bluetooth_connection)
+
+ # Listen for changes to the key store
+ self.device.on('key_store_update', self.on_key_store_update)
+
+ # Create a listener to wait for AVDTP connections
+ self.avdtp_listener = Listener.for_device(self.device)
+ self.avdtp_listener.on('connection', self.on_avdtp_connection)
+
+ print(f'Speaker ready to play, codec={self.codec}')
+
+ if connect_address:
+ # Connect to the source
+ try:
+ await self.connect(connect_address)
+ except CommandTimeoutError:
+ print("Connection timed out")
+ return
+ else:
+ # We'll wait for a connection
+ print("Waiting for connection...")
+
+ async def start(self):
+ await self.run(None)
+
+ async def stop(self):
+ # TODO: replace this once a proper reset is implemented in the lib.
+ await self.device.host.send_command(HCI_Reset_Command())
+ await self.device.power_off()
+ print('Speaker stopped')
+
+
+# -----------------------------------------------------------------------------
+def main(hci_source, hci_sink):
+ return Speaker(hci_source, hci_sink, "aac")
diff --git a/hive/web/ui.js b/hive/web/ui.js
new file mode 100644
index 00000000..a72ab67c
--- /dev/null
+++ b/hive/web/ui.js
@@ -0,0 +1,102 @@
+import {LitElement, html} from 'https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js';
+
+class BumbleControls extends LitElement {
+ constructor() {
+ super();
+ this.bumbleLoaded = false;
+ this.connected = false;
+ }
+
+ render() {
+ return html`
+
+
+ WebSocket URL for HCI transport
+
+
+
settings
+
bluetooth
+
stop
+ `
+ }
+
+ get settingsHciUrlInput() {
+ return this.renderRoot.querySelector('#settings-hci-url-input');
+ }
+
+ get settingsDialog() {
+ return this.renderRoot.querySelector('#settings-dialog');
+ }
+
+ canConnect() {
+ return this.bumbleLoaded && !this.connected && this.getHciUrl();
+ }
+
+ getHciUrl() {
+ // Look for a URL parameter setting first.
+ const params = (new URL(document.location)).searchParams;
+ let hciWsUrl = params.get("hci");
+ if (hciWsUrl) {
+ return hciWsUrl;
+ }
+
+ // Try to load the setting from storage.
+ hciWsUrl = localStorage.getItem("hciWsUrl");
+ if (hciWsUrl) {
+ return hciWsUrl;
+ }
+
+ // Finally, default to nothing.
+ return null;
+ }
+
+ openSettingsDialog() {
+ const hciUrl = this.getHciUrl();
+ if (hciUrl) {
+ this.settingsHciUrlInput.value = hciUrl;
+ } else {
+ // Start with a template.
+ this.settingsHciUrlInput.value = "ws://localhost:XYZW/v1/websocket/bt"
+ }
+ this.settingsDialog.showModal();
+ }
+
+ onSettingsDialogClose() {
+ if (this.settingsDialog.returnValue === "cancel") {
+ return;
+ }
+ if (this.settingsHciUrlInput.value) {
+ localStorage.setItem("hciWsUrl", this.settingsHciUrlInput.value);
+ } else {
+ localStorage.removeItem("hciWsUrl");
+ }
+
+ this.requestUpdate();
+ }
+
+ saveSettings(event) {
+ event.preventDefault();
+ this.settingsDialog.close(this.settingsHciUrlInput.value);
+ }
+
+ async connectBluetooth() {
+ this.connected = await this.connector(this.getHciUrl());
+ this.requestUpdate();
+ }
+
+ async stop() {
+ await this.stopper();
+ this.connected = false;
+ this.requestUpdate();
+ }
+
+ onBumbleLoaded() {
+ this.bumbleLoaded = true;
+ this.requestUpdate();
+ }
+}
+customElements.define('bumble-controls', BumbleControls);
diff --git a/index.html b/index.html
index 75a63691..96a1285b 100644
--- a/index.html
+++ b/index.html
@@ -1903,6 +1903,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/platforms/android.html b/platforms/android.html
index e582e9f4..8d64dfe5 100644
--- a/platforms/android.html
+++ b/platforms/android.html
@@ -1762,6 +1762,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/platforms/index.html b/platforms/index.html
index 68d5bba1..418db3a0 100644
--- a/platforms/index.html
+++ b/platforms/index.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/platforms/linux.html b/platforms/linux.html
index e7cf98b6..33ed9d94 100644
--- a/platforms/linux.html
+++ b/platforms/linux.html
@@ -1829,6 +1829,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/platforms/macos.html b/platforms/macos.html
index 88e20778..ff543044 100644
--- a/platforms/macos.html
+++ b/platforms/macos.html
@@ -1728,6 +1728,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/platforms/windows.html b/platforms/windows.html
index 5ec700da..39dd0b0b 100644
--- a/platforms/windows.html
+++ b/platforms/windows.html
@@ -1728,6 +1728,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/platforms/zephyr.html b/platforms/zephyr.html
index c05668f1..fdfe0830 100644
--- a/platforms/zephyr.html
+++ b/platforms/zephyr.html
@@ -1728,6 +1728,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/sitemap.xml.gz b/sitemap.xml.gz
index c404fb1c1a6a49739b2ac0f6a16f2d11ed855dee..45848c0f0c14dfae49cd15e7bf9d3370719e33b9 100644
GIT binary patch
delta 13
Ucmb=gXP58h;OK6$o5)@P031&Q8vp
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
@@ -1789,9 +1995,9 @@ Use the android-netsim transport name instead.
connections.
Moniker
The moniker syntax for an Android Emulator "netsim" transport is: android-netsim:[<host>:<port>][<options>],
-where <options> is a ','-separated list of <name>=<value> pairs.
-Themodeparameter name can specify running as a host or a controller, and:can specify a host name (or IP address) and TCP port number on which to reach the gRPC server for the emulator (in "host" mode), or to accept gRPC connections (in "controller" mode).
-Both themode=and:parameters are optional (so the monikerandroid-netsimby itself is a valid moniker, which will create a transport inhostmode, connected tolocalhost` on the default gRPC port for the Netsim background process).
+where <options> is a comma-separated list of <name>=<value> pairs.
+The mode parameter name can specify running as a host or a controller, and <hostname>:<port> can specify a host name (or IP address) and TCP port number on which to reach the gRPC server for the emulator (in "host" mode), or to accept gRPC connections (in "controller" mode).
+Both the mode=<host|controller> and <hostname>:<port> parameters are optional (so the moniker android-netsim by itself is a valid moniker, which will create a transport in host mode, connected to localhost on the default gRPC port for the Netsim background process).
diff --git a/transports/hci_socket.html b/transports/hci_socket.html
index d9bebabe..97b763a4 100644
--- a/transports/hci_socket.html
+++ b/transports/hci_socket.html
@@ -1728,6 +1728,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/transports/index.html b/transports/index.html
index bf1fc5b2..f1ce089e 100644
--- a/transports/index.html
+++ b/transports/index.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/transports/pty.html b/transports/pty.html
index e0f8810d..25e55fbc 100644
--- a/transports/pty.html
+++ b/transports/pty.html
@@ -1728,6 +1728,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/transports/serial.html b/transports/serial.html
index 375aafa9..baca3afd 100644
--- a/transports/serial.html
+++ b/transports/serial.html
@@ -1728,6 +1728,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/transports/tcp_client.html b/transports/tcp_client.html
index dda5c2f0..51a1770a 100644
--- a/transports/tcp_client.html
+++ b/transports/tcp_client.html
@@ -1728,6 +1728,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/transports/tcp_server.html b/transports/tcp_server.html
index 6b040025..7abea239 100644
--- a/transports/tcp_server.html
+++ b/transports/tcp_server.html
@@ -1728,6 +1728,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/transports/udp.html b/transports/udp.html
index a310d182..6e4e8440 100644
--- a/transports/udp.html
+++ b/transports/udp.html
@@ -1728,6 +1728,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/transports/usb.html b/transports/usb.html
index e85fec4b..e362194a 100644
--- a/transports/usb.html
+++ b/transports/usb.html
@@ -1789,6 +1789,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/transports/vhci.html b/transports/vhci.html
index 2f1bea5c..e5bbd355 100644
--- a/transports/vhci.html
+++ b/transports/vhci.html
@@ -1728,6 +1728,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/transports/ws_client.html b/transports/ws_client.html
index 3a693e4c..a644af1c 100644
--- a/transports/ws_client.html
+++ b/transports/ws_client.html
@@ -1728,6 +1728,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/transports/ws_server.html b/transports/ws_server.html
index 6ab1a45c..13c7193b 100644
--- a/transports/ws_server.html
+++ b/transports/ws_server.html
@@ -1728,6 +1728,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/use_cases/index.html b/use_cases/index.html
index a3dfbd97..3313aca1 100644
--- a/use_cases/index.html
+++ b/use_cases/index.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/use_cases/use_case_1.html b/use_cases/use_case_1.html
index 86629ec6..fc7defb1 100644
--- a/use_cases/use_case_1.html
+++ b/use_cases/use_case_1.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/use_cases/use_case_2.html b/use_cases/use_case_2.html
index af804af8..770dd850 100644
--- a/use_cases/use_case_2.html
+++ b/use_cases/use_case_2.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/use_cases/use_case_3.html b/use_cases/use_case_3.html
index 7192ef47..e23f0174 100644
--- a/use_cases/use_case_3.html
+++ b/use_cases/use_case_3.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/use_cases/use_case_4.html b/use_cases/use_case_4.html
index bb2cea75..1e86be95 100644
--- a/use_cases/use_case_4.html
+++ b/use_cases/use_case_4.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/use_cases/use_case_5.html b/use_cases/use_case_5.html
index 2ed5ab4a..6e795b72 100644
--- a/use_cases/use_case_5.html
+++ b/use_cases/use_case_5.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
diff --git a/use_cases/use_case_6.html b/use_cases/use_case_6.html
index 6de595e1..b182be3b 100644
--- a/use_cases/use_case_6.html
+++ b/use_cases/use_case_6.html
@@ -1691,6 +1691,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+ Extras
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+
+
+ Hive
+
+
+
+
+
+
+
+