diff options
author | Sven Gothel <[email protected]> | 2022-02-07 02:09:07 +0100 |
---|---|---|
committer | Sven Gothel <[email protected]> | 2022-02-07 02:09:07 +0100 |
commit | 42d1296cb7b269d164574ed1de35d04109c87e60 (patch) | |
tree | 4a31cf97b1f0542dd04b1cb239d474cc6ad26f09 | |
parent | 24ef91d8c6290dfe0a5cc2caa1e70e7969412f60 (diff) |
Added online unit testing using actual BT adapter, testing client with server functionality
Building with enabled *trial* and *testing* , i.e. live testing with 2 Bluetooth adapter
is provided via the *cmake* build argument `-DBUILD_TRIAL=ON`, see above.
The *trial* tests utilize one or more actual Bluetooth adapter,
hence using the *capsh* launch for the required permissions as described above.
Therefor, *sudo* will be called and a user interaction to enter the *sudo* password may occur.
The *trial* tests cover *Direct-BT*'s Bluetooth functionality,
having its *master/client* and *slave/server peripheral* facilities communicating via actual adapter,
supporting regression testing of the API, its implementation and adapter.
-rw-r--r-- | .classpath | 1 | ||||
-rw-r--r-- | CMakeLists.txt | 12 | ||||
-rw-r--r-- | README.md | 40 | ||||
-rw-r--r-- | scripts/build.sh | 5 | ||||
l--------- | scripts/run-dbt_repeater00.sh | 1 | ||||
-rw-r--r-- | trial/java/CMakeLists.txt | 39 | ||||
-rw-r--r-- | trial/java/manifest.txt.in | 25 | ||||
-rw-r--r-- | trial/java/trial/org/direct_bt/BaseDBTClientServer.java | 161 | ||||
-rw-r--r-- | trial/java/trial/org/direct_bt/DBTClient00.java | 626 | ||||
-rw-r--r-- | trial/java/trial/org/direct_bt/DBTConstants.java | 31 | ||||
-rw-r--r-- | trial/java/trial/org/direct_bt/DBTServer00.java | 742 | ||||
-rw-r--r-- | trial/java/trial/org/direct_bt/DBTUtils.java | 177 | ||||
-rw-r--r-- | trial/java/trial/org/direct_bt/TestDBTClientServer00.java | 217 | ||||
-rw-r--r-- | trial/java/trial/org/direct_bt/TestDBTClientServer10.java | 259 | ||||
-rw-r--r-- | trial/java/trial/org/direct_bt/VersionInfo.java | 14 |
15 files changed, 2346 insertions, 4 deletions
@@ -7,6 +7,7 @@ <classpathentry kind="src" path="jaulib/java"/> <classpathentry kind="src" path="jaulib/java_base"/> <classpathentry kind="src" path="jaulib/java_net"/> + <classpathentry kind="src" path="trial/java"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"> <attributes> <attribute name="module" value="true"/> diff --git a/CMakeLists.txt b/CMakeLists.txt index 0535b6dd..61df04c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,10 @@ find_package (Threads REQUIRED) # set(CMAKE_MESSAGE_LOG_LEVEL DEBUG) +if (BUILD_TRIAL) + set (BUILD_TESTING ON) +endif(BUILD_TRIAL) + include(jaulib/JaulibSetup.cmake) JaulibSetup() @@ -90,4 +94,12 @@ if (BUILD_TESTING) endif(BUILDJAVA) endif(BUILD_TESTING) +if (BUILD_TRIAL) + enable_testing () + if (BUILDJAVA) + configure_file (${CMAKE_CURRENT_SOURCE_DIR}/trial/java/manifest.txt.in ${CMAKE_CURRENT_BINARY_DIR}/trial/java/manifest.txt) + add_subdirectory (trial/java) + endif(BUILDJAVA) +endif(BUILD_TRIAL) + @@ -56,6 +56,11 @@ Some elaboration on the implementation details > exposing BTSecurityLevel and SMPIOCapability setup per connection > and providing *automatic security mode negotiation*. > +> Provoding *dbt_repeater00*, a *BT repeater* forwading between *GATT-Server* and *-Client*, +> allowing protocol analysis between an external client and server. +> +> *Online* unit testing with two BT adapter is provided. +> > BREDR support is planned and prepared for. > @@ -210,7 +215,7 @@ A guide for getting started with *Direct-BT* on C++ and Java may follow up. are available, demonstrating the event driven and multithreading workflow: - [dbt_scanner10.cpp](https://jausoft.com/projects/direct_bt/build/documentation/cpp/html/dbt_scanner10_8cpp-example.html) *Master* with *Gatt-Client* - [dbt_peripheral00.cpp](https://jausoft.com/projects/direct_bt/build/documentation/cpp/html/dbt_peripheral00_8cpp-example.html) *Peripheral* with *GATT-Server* -- [dbt_repeater00.cpp](https://jausoft.com/projects/direct_bt/build/documentation/cpp/html/dbt_repeater00_8cpp-example.html) *Repeater* forwading between *GATT-Server* and *-Client* +- [dbt_repeater00.cpp](https://jausoft.com/projects/direct_bt/build/documentation/cpp/html/dbt_repeater00_8cpp-example.html) *BT Repeater* forwading between *GATT-Server* and *-Client*, allowing protocol analysis between an external client and server. *Direct-BT* [Java examples](https://jausoft.com/projects/direct_bt/build/documentation/java/html/examples.html) @@ -289,6 +294,18 @@ Building debug build: -DDEBUG=ON ~~~~~~~~~~~~~ +Building with enabled *testing*, i.e. offline testing without any potential interaction as user: + +~~~~~~~~~~~~~ +-DBUILD_TESTING=ON +~~~~~~~~~~~~~ + +Building with enabled *trial* and *testing* , i.e. live testing with 2 Bluetooth adapter and potential sudo interaction: + +~~~~~~~~~~~~~ +-DBUILD_TRIAL=ON +~~~~~~~~~~~~~ + Disable stripping native lib even in non debug build: ~~~~~~~~~~~~~ @@ -340,6 +357,23 @@ To build documentation run: make doc ~~~~~~~~~~~~~ +### Unit Testing + +Building with enabled *testing*, i.e. offline testing without any potential interaction as user +is provided via the *cmake* build argument `-DBUILD_TESTING=ON`, see above. + +Building with enabled *trial* and *testing* , i.e. live testing with 2 Bluetooth adapter +is provided via the *cmake* build argument `-DBUILD_TRIAL=ON`, see above. + +The *trial* tests utilize one or more actual Bluetooth adapter, +hence using the *capsh* launch for the required permissions as described above. +Therefor, *sudo* will be called and a user interaction to enter the *sudo* password may occur. + +The *trial* tests cover *Direct-BT*'s Bluetooth functionality, +having its *master/client* and *slave/server peripheral* facilities communicating via actual adapter, +supporting regression testing of the API, its implementation and adapter. + + ### Cross Build Also provided is a [cross-build script](https://jausoft.com/cgit/direct_bt.git/tree/scripts/build-cross.sh) using chroot into a target system using [QEMU User space emulation](https://qemu-project.gitlab.io/qemu/user/main.html) @@ -449,7 +483,9 @@ from the year 2016. * TODO **2.6.0** -* TODO +* Added *online* unit testing using actual BT adapter, testing *client* with *server* functionality. +* BTAdapter/HCIHandler: Fix advertising state: Active until either disabled or connected. +* DBTAdapter: Fix removeAllStatusListener(): Re-add internal listener to maintain functionality. * GATT Server enhancements, incl new DBGattServer::Mode and `dbt_repeater00` implementation. * BTDevice::getGattServices(): MTU and remote GATT Services shall be processed from here at request only, moved from BTDevice::connectGATT(). * jaulib v0.7.11 fixes diff --git a/scripts/build.sh b/scripts/build.sh index 57479f83..78c90df0 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -36,8 +36,9 @@ buildit() { cd build-$archabi # CLANG_ARGS="-DCMAKE_C_COMPILER=/usr/bin/clang -DCMAKE_CXX_COMPILER=/usr/bin/clang++" - cmake $CLANG_ARGS -DCMAKE_INSTALL_PREFIX=$rootdir/dist-$archabi -DBUILDJAVA=ON -DBUILDEXAMPLES=ON -DBUILD_TESTING=ON .. - # cmake $CLANG_ARGS -DCMAKE_INSTALL_PREFIX=$rootdir/dist-$archabi -DBUILDJAVA=ON -DBUILDEXAMPLES=ON -DBUILD_TESTING=ON -DDEBUG=ON .. + cmake $CLANG_ARGS -DCMAKE_INSTALL_PREFIX=$rootdir/dist-$archabi -DBUILDJAVA=ON -DBUILDEXAMPLES=ON -DBUILD_TRIAL=ON .. + # cmake $CLANG_ARGS -DCMAKE_INSTALL_PREFIX=$rootdir/dist-$archabi -DBUILDJAVA=ON -DBUILDEXAMPLES=ON -DBUILD_TESTING=ON .. + # cmake $CLANG_ARGS -DCMAKE_INSTALL_PREFIX=$rootdir/dist-$archabi -DBUILDJAVA=ON -DBUILDEXAMPLES=ON -DBUILD_TRIAL=ON -DDEBUG=ON .. # cmake $CLANG_ARGS -DCMAKE_INSTALL_PREFIX=$rootdir/dist-$archabi -DBUILDJAVA=ON -DBUILDEXAMPLES=ON -DBUILD_TESTING=ON -DUSE_STRIP=OFF .. # cmake $CLANG_ARGS -DCMAKE_INSTALL_PREFIX=$rootdir/dist-$archabi -DBUILDJAVA=ON -DBUILDEXAMPLES=ON -DBUILD_TESTING=ON -DUSE_STRIP=ON -DJAVAC_DEBUG_ARGS="none" .. diff --git a/scripts/run-dbt_repeater00.sh b/scripts/run-dbt_repeater00.sh new file mode 120000 index 00000000..68360a74 --- /dev/null +++ b/scripts/run-dbt_repeater00.sh @@ -0,0 +1 @@ +run-native-example.sh
\ No newline at end of file diff --git a/trial/java/CMakeLists.txt b/trial/java/CMakeLists.txt new file mode 100644 index 00000000..85b1ab4f --- /dev/null +++ b/trial/java/CMakeLists.txt @@ -0,0 +1,39 @@ +# java/CMakeLists.txt + +find_jar(JUNIT_JAR + NAMES junit4 junit + PATHS "/usr/share/java") + +set(direct_bt_trial_jar_file ${CMAKE_CURRENT_BINARY_DIR}/direct_bt-trial.jar CACHE FILEPATH "direct_bt trial jar file" FORCE) + +file(GLOB_RECURSE TEST_JAVA_SOURCES "*.java") + +file(GLOB_RECURSE TEST_JAVA_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "Test*.java") + +add_jar(direct_bt_trial + ${TEST_JAVA_SOURCES} + INCLUDE_JARS ${jaulib_fat_jar_file} ${direct_bt_fat_jar_file} ${jaulib_test_jar_file} ${JUNIT_JAR} + MANIFEST ${CMAKE_CURRENT_BINARY_DIR}/manifest.txt + OUTPUT_NAME direct_bt-trial +) +add_dependencies(direct_bt_trial direct_bt_fat_jar jaulib_test) + +install (FILES ${direct_bt_trial_jar_file} DESTINATION ${CMAKE_INSTALL_LIBDIR}/../lib/java) + +string( REPLACE ".java" "" TEST_JAVA_FILES2 "${TEST_JAVA_FILES}" ) +string( REPLACE "/" "." BASENAMES_IDIOMATIC_EXAMPLES "${TEST_JAVA_FILES2}" ) +set( TARGETS_IDIOMATIC_EXAMPLES ${BASENAMES_IDIOMATIC_EXAMPLES} ) + +#foreach(name ${TARGETS_IDIOMATIC_EXAMPLES}) +# add_test (NAME ${name} COMMAND ${JAVA_RUNTIME} +# -cp ${JUNIT_JAR}:${direct_bt_fat_jar_file}:${jaulib_trial_jar_file}:${direct_bt_trial_jar_file} +# org.junit.runner.JUnitCore ${name}) +#endforeach() + +foreach(name ${TARGETS_IDIOMATIC_EXAMPLES}) + add_test (NAME ${name} COMMAND sudo /sbin/capsh --caps=cap_net_raw,cap_net_admin+eip\ cap_setpcap,cap_setuid,cap_setgid+ep + --keep=1 --user=sven --addamb=cap_net_raw,cap_net_admin+eip + -- -c "ulimit -c unlimited; $EXE_WRAPPER ${JAVA_RUNTIME} -cp ${JUNIT_JAR}:${direct_bt_fat_jar_file}:${jaulib_test_jar_file}:${direct_bt_trial_jar_file} org.junit.runner.JUnitCore ${name}") +endforeach() + + diff --git a/trial/java/manifest.txt.in b/trial/java/manifest.txt.in new file mode 100644 index 00000000..1810a199 --- /dev/null +++ b/trial/java/manifest.txt.in @@ -0,0 +1,25 @@ +Manifest-Version: 1.0 +Bundle-Date: @BUILD_TSTAMP@ +Bundle-ManifestVersion: 2 +Bundle-Name: org.direct_bt.trial +Bundle-SymbolicName: org.direct_bt.trial +Bundle-Version: @VERSION_SHORT@ +Export-Package: org.direct_bt.trial +Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.9))" +Package-Title: org.direct_bt.trial +Package-Vendor: Gothel Software +Package-Version: @VERSION_SHORT@ +Specification-Title: Direct-BT Unit Tests +Specification-Vendor: Gothel Software +Specification-Version: @VERSION_API@ +Implementation-Title: Direct-BT Unit Tests +Implementation-Vendor: Gothel Software +Implementation-Version: @VERSION@ +Implementation-Commit: @VERSION_SHA1@ +Implementation-URL: http://www.jausoft.com/ +Extension-Name: org.direct_bt.trial +Trusted-Library: true +Permissions: all-permissions +Application-Library-Allowable-Codebase: * +Class-Path: direct_bt-fat.jar jaulib-test.jar +Main-Class: trial.org.direct_bt.VersionInfo diff --git a/trial/java/trial/org/direct_bt/BaseDBTClientServer.java b/trial/java/trial/org/direct_bt/BaseDBTClientServer.java new file mode 100644 index 00000000..8739c038 --- /dev/null +++ b/trial/java/trial/org/direct_bt/BaseDBTClientServer.java @@ -0,0 +1,161 @@ +/** + * Author: Sven Gothel <[email protected]> + * Copyright (c) 2022 Gothel Software e.K. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package trial.org.direct_bt; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import org.direct_bt.BTAdapter; +import org.direct_bt.BTDeviceRegistry; +import org.direct_bt.BTException; +import org.direct_bt.BTFactory; +import org.direct_bt.BTManager; +import org.direct_bt.BTSecurityRegistry; +import org.direct_bt.BTUtils; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.runners.MethodSorters; + +import jau.test.junit.util.SingletonJunitCase; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public abstract class BaseDBTClientServer extends SingletonJunitCase { + static boolean DEBUG = false; + + @BeforeClass + public static final void setupAll() { + BTFactory.initDirectBTLibrary(); + + final Class<?> ThisClazz = MethodHandles.lookup().lookupClass(); + BTUtils.println(System.err, "++++ Test "+ThisClazz.getSimpleName()+".setupAll()"); + + if( DEBUG ) { + System.setProperty("direct_bt.debug", "true"); + System.setProperty("org.direct_bt.debug", "true"); + } + Assert.assertTrue( DBTUtils.rmKeyFolder() ); + Assert.assertTrue( DBTUtils.mkdirKeyFolder() ); + } + + /** + * Ensure + * - all adapter are powered off + * - manager being shutdown + */ + @AfterClass + public static final void cleanupAll() { + final Class<?> ThisClazz = MethodHandles.lookup().lookupClass(); + BTUtils.println(System.err, "++++ Test "+ThisClazz.getSimpleName()+".cleanupAll()"); + + BTManager manager = null; + try { + manager = BTFactory.getDirectBTManager(); + } catch (BTException | NoSuchMethodException | SecurityException + | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | ClassNotFoundException e) { + e.printStackTrace(); + BTUtils.println(System.err, "Unable to instantiate DirectBT BluetoothManager: "+e.getMessage()); + e.printStackTrace(); + } + if( null != manager ) { + final List<BTAdapter> adapters = manager.getAdapters(); + for(final BTAdapter a : adapters) { + a.setPowered(false); + } + // All implicit via destructor or shutdown hook! + manager.shutdown(); /* implies: adapter.close(); */ + } + } + + /** + * Ensure + * - all adapter are powered off + */ + @Before + public final void setupTest() { + final Class<?> ThisClazz = MethodHandles.lookup().lookupClass(); + BTUtils.println(System.err, "++++ Test "+ThisClazz.getSimpleName()+".setupTest()"); + + BTManager manager = null; + try { + manager = BTFactory.getDirectBTManager(); + } catch (BTException | NoSuchMethodException | SecurityException + | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | ClassNotFoundException e) { + e.printStackTrace(); + BTUtils.println(System.err, "Unable to instantiate DirectBT BluetoothManager: "+e.getMessage()); + e.printStackTrace(); + } + if( null != manager ) { + final List<BTAdapter> adapters = manager.getAdapters(); + for(final BTAdapter a : adapters) { + Assert.assertTrue( a.setPowered(false) ); + } + } + } + + /** + * Ensure + * - remove all status listener from all adapter + * - all adapter are powered off + * - clear BTDeviceRegistry + * - clear BTSecurityRegistry + */ + @After + public final void cleanupTest() { + final Class<?> ThisClazz = MethodHandles.lookup().lookupClass(); + BTUtils.println(System.err, "++++ Test "+ThisClazz.getSimpleName()+".cleanupTest()"); + + BTManager manager = null; + try { + manager = BTFactory.getDirectBTManager(); + } catch (BTException | NoSuchMethodException | SecurityException + | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | ClassNotFoundException e) { + e.printStackTrace(); + BTUtils.println(System.err, "Unable to instantiate DirectBT BluetoothManager: "+e.getMessage()); + e.printStackTrace(); + } + if( null != manager ) { + final List<BTAdapter> adapters = manager.getAdapters(); + for(final BTAdapter a : adapters) { + { + final int r = a.removeAllStatusListener(); + Assert.assertTrue("Not >= 0 removed listener, but "+r, 0 <= r ); + } + Assert.assertTrue( a.setPowered(false) ); + } + } + BTDeviceRegistry.clearWaitForDevices(); + BTDeviceRegistry.clearProcessedDevices(); + BTDeviceRegistry.clearProcessingDevices(); + BTSecurityRegistry.clear(); + } +} diff --git a/trial/java/trial/org/direct_bt/DBTClient00.java b/trial/java/trial/org/direct_bt/DBTClient00.java new file mode 100644 index 00000000..19721e8b --- /dev/null +++ b/trial/java/trial/org/direct_bt/DBTClient00.java @@ -0,0 +1,626 @@ +/** + * Author: Sven Gothel <[email protected]> + * Copyright (c) 2022 Gothel Software e.K. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package trial.org.direct_bt; + +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.direct_bt.AdapterSettings; +import org.direct_bt.AdapterStatusListener; +import org.direct_bt.BTMode; +import org.direct_bt.BTSecurityLevel; +import org.direct_bt.BTAdapter; +import org.direct_bt.BTDevice; +import org.direct_bt.BTDeviceRegistry; +import org.direct_bt.BTGattChar; +import org.direct_bt.BTGattCmd; +import org.direct_bt.BTGattDesc; +import org.direct_bt.BTGattService; +import org.direct_bt.BTSecurityRegistry; +import org.direct_bt.BTUtils; +import org.direct_bt.DiscoveryPolicy; +import org.direct_bt.EIRDataTypeSet; +import org.direct_bt.EInfoReport; +import org.direct_bt.GattCharPropertySet; +import org.direct_bt.HCIStatusCode; +import org.direct_bt.LE_Features; +import org.direct_bt.LE_PHYs; +import org.direct_bt.PairingMode; +import org.direct_bt.SMPIOCapability; +import org.direct_bt.SMPKeyBin; +import org.direct_bt.SMPPairingState; +import org.direct_bt.ScanType; +import org.jau.net.EUI48; + +/** + * This Java scanner {@link BTRole::Master} test case, working with {@link DBTServer00}. + */ +public class DBTClient00 { + /** + * Disconnect after processing. + * + * Default is `false`. + */ + boolean KEEP_CONNECTED = false; + + /** + * Remove device when disconnecting. + * + * This removes the device from all instances within adapter + * and hence all potential side-effects of the current instance. + * + * Default is `false`, since it is good to test whether such side-effects exists. + */ + boolean REMOVE_DEVICE = false; + + final boolean GATT_VERBOSE = false; + final boolean SHOW_UPDATE_EVENTS = false; + + final long timestamp_t0 = BTUtils.startupTimeMillis(); + + EUI48 useAdapter = EUI48.ALL_DEVICE; + BTMode btMode = BTMode.DUAL; + BTAdapter clientAdapter = null; + + AtomicInteger deviceReadyCount = new AtomicInteger(0); + + AtomicInteger notificationsReceived = new AtomicInteger(0); + AtomicInteger indicationsReceived = new AtomicInteger(0); + AtomicInteger completedGATTCommands = new AtomicInteger(0); + AtomicInteger completedMeasurements = new AtomicInteger(0); + AtomicInteger measurementsLeft = new AtomicInteger(1); + + // Default from DBTServer00.java (or dbt_peripheral00.cpp or DBTPeripheral00.java) + final String cmd_uuid = "d0ca6bf3-3d52-4760-98e5-fc5883e93712"; + final String cmd_rsp_uuid = "d0ca6bf3-3d53-4760-98e5-fc5883e93712"; + final byte cmd_arg = (byte)0x44; + + public DBTClient00(final EUI48 useAdapter, final BTMode btMode) { + this.useAdapter = useAdapter; + this.btMode = btMode; + } + public void setAdapter(final BTAdapter clientAdapter) { + this.clientAdapter = clientAdapter; + } + public BTAdapter getAdapter() { return clientAdapter; } + + static void printf(final String format, final Object... args) { + final Object[] args2 = new Object[args.length+1]; + args2[0] = BTUtils.elapsedTimeMillis(); + System.arraycopy(args, 0, args2, 1, args.length); + System.err.printf("[%,9d] "+format, args2); + // System.err.printf("[%,9d] ", BluetoothUtils.getElapsedMillisecond()); + // System.err.printf(format, args); + } + + static void executeOffThread(final Runnable runobj, final String threadName, final boolean detach) { + final Thread t = new Thread( runobj, threadName ); + if( detach ) { + t.setDaemon(true); // detach thread + } + t.start(); + } + static void execute(final Runnable task, final boolean offThread) { + if( offThread ) { + final Thread t = new Thread(task); + t.setDaemon(true); + t.start(); + } else { + task.run(); + } + } + + class MyAdapterStatusListener extends AdapterStatusListener { + @Override + public void adapterSettingsChanged(final BTAdapter adapter, final AdapterSettings oldmask, + final AdapterSettings newmask, final AdapterSettings changedmask, final long timestamp) { + final boolean initialSetting = oldmask.isEmpty(); + if( initialSetting ) { + BTUtils.println(System.err, "****** Client SETTINGS: "+oldmask+" -> "+newmask+", initial "+changedmask); + } else { + BTUtils.println(System.err, "****** Client SETTINGS: "+oldmask+" -> "+newmask+", changed "+changedmask); + } + BTUtils.println(System.err, "Status Adapter:"); + BTUtils.println(System.err, adapter.toString()); + } + + @Override + public void discoveringChanged(final BTAdapter adapter, final ScanType currentMeta, final ScanType changedType, final boolean changedEnabled, final DiscoveryPolicy policy, final long timestamp) { + BTUtils.println(System.err, "****** Client DISCOVERING: meta "+currentMeta+", changed["+changedType+", enabled "+changedEnabled+", policy "+policy+"] on "+adapter); + } + + @Override + public boolean deviceFound(final BTDevice device, final long timestamp) { + if( !BTDeviceRegistry.isDeviceProcessing( device.getAddressAndType() ) && + ( !BTDeviceRegistry.isWaitingForAnyDevice() || + ( BTDeviceRegistry.isWaitingForDevice(device.getAddressAndType().address, device.getName()) && + ( 0 < measurementsLeft.get() || !BTDeviceRegistry.isDeviceProcessed(device.getAddressAndType()) ) + ) + ) + ) + { + BTUtils.println(System.err, "****** Client FOUND__-0: Connecting "+device.toString()); + { + final long td = BTUtils.currentTimeMillis() - timestamp_t0; // adapter-init -> now + BTUtils.println(System.err, "PERF: adapter-init -> FOUND__-0 " + td + " ms"); + } + executeOffThread( () -> { connectDiscoveredDevice(device); }, + "DBT-Connect-"+device.getAddressAndType(), true /* detach */); + return true; + } else { + BTUtils.println(System.err, "****** Client FOUND__-1: NOP "+device.toString()); + return false; + } + } + + @Override + public void deviceUpdated(final BTDevice device, final EIRDataTypeSet updateMask, final long timestamp) { + } + + @Override + public void deviceConnected(final BTDevice device, final short handle, final long timestamp) { + BTUtils.println(System.err, "****** Client CONNECTED: "+device.toString()); + } + + @Override + public void devicePairingState(final BTDevice device, final SMPPairingState state, final PairingMode mode, final long timestamp) { + BTUtils.println(System.err, "****** Client PAIRING_STATE: state "+state+", mode "+mode+": "+device); + switch( state ) { + case NONE: + // next: deviceReady(..) + break; + case FAILED: { + final boolean res = SMPKeyBin.remove(DBTConstants.CLIENT_KEY_PATH, device); + BTUtils.println(System.err, "****** Client PAIRING_STATE: state "+state+"; Remove key file "+SMPKeyBin.getFilename(DBTConstants.CLIENT_KEY_PATH, device)+", res "+res); + // next: deviceReady() or deviceDisconnected(..) + } break; + case REQUESTED_BY_RESPONDER: + // next: FEATURE_EXCHANGE_STARTED + break; + case FEATURE_EXCHANGE_STARTED: + // next: FEATURE_EXCHANGE_COMPLETED + break; + case FEATURE_EXCHANGE_COMPLETED: + // next: PASSKEY_EXPECTED... or KEY_DISTRIBUTION + break; + case PASSKEY_EXPECTED: { + final BTSecurityRegistry.Entry sec = BTSecurityRegistry.getStartOf(device.getAddressAndType().address, ""); + if( null != sec && sec.getPairingPasskey() != BTSecurityRegistry.NO_PASSKEY ) { + executeOffThread( () -> { device.setPairingPasskey( sec.getPairingPasskey() ); }, "DBT-SetPasskey-"+device.getAddressAndType(), true /* detach */); + } else { + executeOffThread( () -> { device.setPairingPasskey( 0 ); }, "DBT-SetPasskey-"+device.getAddressAndType(), true /* detach */); + // 3s disconnect: executeOffThread( () -> { device.setPairingPasskeyNegative(); }, "DBT-SetPasskeyNegative-"+device.getAddressAndType(), true /* detach */); + } + // next: KEY_DISTRIBUTION or FAILED + } break; + case NUMERIC_COMPARE_EXPECTED: { + final BTSecurityRegistry.Entry sec = BTSecurityRegistry.getStartOf(device.getAddressAndType().address, ""); + if( null != sec ) { + executeOffThread( () -> { device.setPairingNumericComparison( sec.getPairingNumericComparison() ); }, "DBT-SetNumericComp-"+device.getAddressAndType(), true /* detach */); + } else { + executeOffThread( () -> { device.setPairingNumericComparison( false ); }, "DBT-SetNumericCompFalse-"+device.getAddressAndType(), true /* detach */); + } + // next: KEY_DISTRIBUTION or FAILED + } break; + case OOB_EXPECTED: + // FIXME: ABORT + break; + case KEY_DISTRIBUTION: + // next: COMPLETED or FAILED + break; + case COMPLETED: + // next: deviceReady(..) + break; + default: // nop + break; + } + } + + @Override + public void deviceReady(final BTDevice device, final long timestamp) { + if( !BTDeviceRegistry.isDeviceProcessing( device.getAddressAndType() ) && + ( !BTDeviceRegistry.isWaitingForAnyDevice() || + ( BTDeviceRegistry.isWaitingForDevice(device.getAddressAndType().address, device.getName()) && + ( 0 < measurementsLeft.get() || !BTDeviceRegistry.isDeviceProcessed(device.getAddressAndType()) ) + ) + ) + ) + { + deviceReadyCount.incrementAndGet(); + BTUtils.println(System.err, "****** Client READY-0: Processing["+deviceReadyCount.get()+"] "+device.toString()); + { + final long td = BTUtils.currentTimeMillis() - timestamp_t0; // adapter-init -> now + BTUtils.println(System.err, "PERF: adapter-init -> READY-0 " + td + " ms"); + } + BTDeviceRegistry.addToProcessingDevices(device.getAddressAndType(), device.getName()); + + // Be nice to Test* case, allowing to reach its own listener.deviceReady() added later + executeOffThread( () -> { processReadyDevice(device); }, + "DBT-Process-"+device.getAddressAndType(), true /* detach */); + // processReadyDevice(device); // AdapterStatusListener::deviceReady() explicitly allows prolonged and complex code execution! + } else { + BTUtils.println(System.err, "****** Client READY-1: NOP " + device.toString()); + } + } + + @Override + public void deviceDisconnected(final BTDevice device, final HCIStatusCode reason, final short handle, final long timestamp) { + BTUtils.println(System.err, "****** Client DISCONNECTED: Reason "+reason+", old handle 0x"+Integer.toHexString(handle)+": "+device+" on "+device.getAdapter()); + + executeOffThread( () -> { removeDevice(device); }, "DBT-Remove-"+device.getAddressAndType(), true /* detach */); + } + + @Override + public String toString() { + return "AdapterStatusListener[user, per-adapter]"; + } + }; + + class MyGATTEventListener implements BTGattChar.Listener { + private final int i, j; + + public MyGATTEventListener(final int i_, final int j_) { i=i_; j=j_; } + + @Override + public void notificationReceived(final BTGattChar charDecl, + final byte[] value, final long timestamp) { + if( GATT_VERBOSE ) { + final long tR = BTUtils.currentTimeMillis(); + printf("**[%02d.%02d] Characteristic-Notify: UUID %s, td %d ******\n", + i, j, charDecl.getUUID(), (tR-timestamp)); + printf("**[%02d.%02d] Characteristic: %s ******\n", i, j, charDecl.toString()); + printf("**[%02d.%02d] Value R: size %d, ro: %s ******\n", i, j, value.length, BTUtils.bytesHexString(value, 0, -1, true)); + printf("**[%02d.%02d] Value S: %s ******\n", i, j, BTUtils.decodeUTF8String(value, 0, value.length)); + } + notificationsReceived.incrementAndGet(); + } + + @Override + public void indicationReceived(final BTGattChar charDecl, + final byte[] value, final long timestamp, final boolean confirmationSent) { + if( GATT_VERBOSE ) { + final long tR = BTUtils.currentTimeMillis(); + printf("**[%02d.%02d] Characteristic-Indication: UUID %s, td %d, confirmed %b ******\n", + i, j, charDecl.getUUID(), (tR-timestamp), confirmationSent); + printf("**[%02d.%02d] Characteristic: %s ******\n", i, j, charDecl.toString()); + printf("**[%02d.%02d] Value R: size %d, ro: %s ******\n", i, j, value.length, BTUtils.bytesHexString(value, 0, -1, true)); + printf("**[%02d.%02d] Value S: %s ******\n", i, j, BTUtils.decodeUTF8String(value, 0, value.length)); + } + indicationsReceived.incrementAndGet(); + } + } + + private void resetLastProcessingStats() { + completedGATTCommands.set(0); + notificationsReceived.set(0); + indicationsReceived.set(0); + } + + private void connectDiscoveredDevice(final BTDevice device) { + BTUtils.println(System.err, "****** Connecting Device: Start " + device.toString()); + + resetLastProcessingStats(); + + final BTSecurityRegistry.Entry sec = BTSecurityRegistry.getStartOf(device.getAddressAndType().address, device.getName()); + if( null != sec ) { + BTUtils.println(System.err, "****** Connecting Device: Found SecurityDetail "+sec.toString()+" for "+device.toString()); + } else { + BTUtils.println(System.err, "****** Connecting Device: No SecurityDetail for "+device.toString()); + } + final BTSecurityLevel req_sec_level = null != sec ? sec.getSecLevel() : BTSecurityLevel.UNSET; + HCIStatusCode res = device.uploadKeys(DBTConstants.CLIENT_KEY_PATH, req_sec_level, true /* verbose_ */); + BTUtils.fprintf_td(System.err, "****** Connecting Device: BTDevice::uploadKeys(...) result %s\n", res.toString()); + if( HCIStatusCode.SUCCESS != res ) { + if( null != sec ) { + if( sec.isSecurityAutoEnabled() ) { + final boolean r = device.setConnSecurityAuto( sec.getSecurityAutoIOCap() ); + BTUtils.println(System.err, "****** Connecting Device: Using SecurityDetail.SEC AUTO "+sec+" -> set OK "+r); + } else if( sec.isSecLevelOrIOCapSet() ) { + final boolean r = device.setConnSecurity(sec.getSecLevel(), sec.getIOCap()); + BTUtils.println(System.err, "****** Connecting Device: Using SecurityDetail.Level+IOCap "+sec+" -> set OK "+r); + } else { + final boolean r = device.setConnSecurityAuto( SMPIOCapability.KEYBOARD_ONLY ); + BTUtils.println(System.err, "****** Connecting Device: Setting SEC AUTO security detail w/ KEYBOARD_ONLY ("+sec+") -> set OK "+r); + } + } else { + final boolean r = device.setConnSecurityAuto( SMPIOCapability.KEYBOARD_ONLY ); + BTUtils.println(System.err, "****** Connecting Device: Setting SEC AUTO security detail w/ KEYBOARD_ONLY -> set OK "+r); + } + } + final EInfoReport eir = device.getEIR(); + BTUtils.println(System.err, "Using EIR "+eir.toString()); + + short conn_interval_min = (short)12; + short conn_interval_max = (short)12; + final short conn_latency = (short)0; + if( eir.isSet(EIRDataTypeSet.DataType.CONN_IVAL) ) { + final short[] minmax = new short[2]; + eir.getConnInterval(minmax); + conn_interval_min = minmax[0]; + conn_interval_max = minmax[1]; + } + final short supervision_timeout = BTUtils.getHCIConnSupervisorTimeout(conn_latency, (int) ( conn_interval_max * 1.25 ) /* ms */); + res = device.connectLE(le_scan_interval, le_scan_window, conn_interval_min, conn_interval_max, conn_latency, supervision_timeout); + // res = device.connectDefault(); + BTUtils.println(System.err, "****** Connecting Device Command, res "+res+": End result "+res+" of " + device.toString()); + } + + private void processReadyDevice(final BTDevice device) { + BTUtils.println(System.err, "****** Processing Ready Device: Start " + device.toString()); + final long t1 = BTUtils.currentTimeMillis(); + + SMPKeyBin.createAndWrite(device, DBTConstants.CLIENT_KEY_PATH, true /* verbose */); + final long t2 = BTUtils.currentTimeMillis(); + + boolean success = false; + + { + final LE_PHYs resTx[] = { new LE_PHYs() }; + final LE_PHYs resRx[] = { new LE_PHYs() }; + final HCIStatusCode res = device.getConnectedLE_PHY(resTx, resRx); + BTUtils.fprintf_td(System.err, "****** Got Connected LE PHY: status %s: Tx %s, Rx %s\n", + res.toString(), resTx[0].toString(), resRx[0].toString()); + } + final long t3 = BTUtils.currentTimeMillis(); + + // + // GATT Service Processing + // + try { + final List<BTGattService> primServices = device.getGattServices(); + if( null == primServices || 0 == primServices.size() ) { + // Cheating the flow, but avoiding: goto, do-while-false and lastly unreadable intendations + // And it is an error case nonetheless ;-) + throw new RuntimeException("Processing Ready Device: getServices() failed " + device.toString()); + } + final long t5 = BTUtils.currentTimeMillis(); + { + final long td00 = device.getLastDiscoveryTimestamp() - timestamp_t0; // adapter-init to discovered + final long td01 = t1 - timestamp_t0; // adapter-init to processing-start + final long td05 = t5 - timestamp_t0; // adapter-init -> gatt-complete + final long tdc1 = t1 - device.getLastDiscoveryTimestamp(); // discovered to processing-start + final long tdc5 = t5 - device.getLastDiscoveryTimestamp(); // discovered to gatt-complete + final long td12 = t2 - t1; // SMPKeyBin + final long td23 = t3 - t2; // LE_PHY + final long td13 = t3 - t1; // SMPKeyBin + LE_PHY + final long td35 = t5 - t3; // get-gatt-services + BTUtils.println(System.err, System.lineSeparator()+System.lineSeparator()); + BTUtils.println(System.err, "PERF: GATT primary-services completed"+System.lineSeparator()+ + "PERF: adapter-init to discovered " + td00 + " ms,"+System.lineSeparator()+ + "PERF: adapter-init to processing-start " + td01 + " ms,"+System.lineSeparator()+ + "PERF: adapter-init to gatt-complete " + td05 + " ms,"+System.lineSeparator()+ + "PERF: discovered to processing-start " + tdc1 + " ms,"+System.lineSeparator()+ + "PERF: discovered to gatt-complete " + tdc5 + " ms,"+System.lineSeparator()+ + "PERF: SMPKeyBin + LE_PHY " + td13 + " ms (SMPKeyBin "+td12+"ms, LE_PHY "+td23+"ms), "+System.lineSeparator()+ + "PERF: get-gatt-services " + td35 + " ms,"+System.lineSeparator()); + } + + { + final BTGattCmd cmd = new BTGattCmd(device, "TestCmd", null /* service_uuid */, cmd_uuid, cmd_rsp_uuid); + cmd.setVerbose(true); + final boolean cmd_resolved = cmd.isResolved(); + BTUtils.println(System.err, "Command test: "+cmd.toString()+", resolved "+cmd_resolved); + final byte[] cmd_data = { cmd_arg }; + final HCIStatusCode cmd_res = cmd.send(true /* prefNoAck */, cmd_data, 3000 /* timeoutMS */); + if( HCIStatusCode.SUCCESS == cmd_res ) { + if( cmd.hasResponseSet() ) { + final byte[] resp = cmd.getResponse(); + if( 1 == resp.length && resp[0] == cmd_arg ) { + BTUtils.fprintf_td(System.err, "Success: %s -> %s (echo response)\n", cmd.toString(), BTUtils.bytesHexString(resp, 0, resp.length, true /* lsb */)); + completedGATTCommands.incrementAndGet(); + } else { + BTUtils.fprintf_td(System.err, "Failure: %s -> %s (different response)\n", cmd.toString(), BTUtils.bytesHexString(resp, 0, resp.length, true /* lsb */)); + } + } else { + BTUtils.fprintf_td(System.err, "Failure: %s -> no response\n", cmd.toString()); + } + } else { + BTUtils.fprintf_td(System.err, "Failure: %s -> %s\n", cmd.toString(), cmd_res.toString()); + } + cmd.close(); + } + + try { + int i=0; + for(final Iterator<BTGattService> srvIter = primServices.iterator(); srvIter.hasNext(); i++) { + final BTGattService primService = srvIter.next(); + if( GATT_VERBOSE ) { + printf(" [%02d] Service UUID %s\n", i, primService.getUUID()); + printf(" [%02d] %s\n", i, primService.toString()); + } + int j=0; + final List<BTGattChar> serviceCharacteristics = primService.getChars(); + for(final Iterator<BTGattChar> charIter = serviceCharacteristics.iterator(); charIter.hasNext(); j++) { + final BTGattChar serviceChar = charIter.next(); + if( GATT_VERBOSE ) { + printf(" [%02d.%02d] Characteristic: UUID %s\n", i, j, serviceChar.getUUID()); + printf(" [%02d.%02d] %s\n", i, j, serviceChar.toString()); + } + final GattCharPropertySet properties = serviceChar.getProperties(); + if( properties.isSet(GattCharPropertySet.Type.Read) ) { + final byte[] value = serviceChar.readValue(); + final String svalue = BTUtils.decodeUTF8String(value, 0, value.length); + if( GATT_VERBOSE ) { + printf(" [%02d.%02d] value: %s ('%s')\n", i, j, BTUtils.bytesHexString(value, 0, -1, true), svalue); + } + } + int k=0; + final List<BTGattDesc> charDescList = serviceChar.getDescriptors(); + for(final Iterator<BTGattDesc> descIter = charDescList.iterator(); descIter.hasNext(); k++) { + final BTGattDesc charDesc = descIter.next(); + if( GATT_VERBOSE ) { + printf(" [%02d.%02d.%02d] Descriptor: UUID %s\n", i, j, k, charDesc.getUUID()); + printf(" [%02d.%02d.%02d] %s\n", i, j, k, charDesc.toString()); + } + } + final boolean cccdEnableResult[] = { false, false }; + if( serviceChar.enableNotificationOrIndication( cccdEnableResult ) ) { + // ClientCharConfigDescriptor (CCD) is available + final boolean clAdded = null != serviceChar.addCharListener( new MyGATTEventListener(i, j) ); + if( GATT_VERBOSE ) { + printf(" [%02d.%02d] Characteristic-Listener: Notification(%b), Indication(%b): Added %b\n", + i, j, cccdEnableResult[0], cccdEnableResult[1], clAdded); + printf("\n"); + } + } + } + if( GATT_VERBOSE ) { + printf("\n"); + } + } + } catch( final Exception ex) { + BTUtils.println(System.err, "Caught "+ex.getMessage()); + ex.printStackTrace(); + } + // FIXME sleep 1s for notifications and/or indications ... + try { + Thread.sleep(1000); + } catch (final InterruptedException e) { + e.printStackTrace(); + } + success = completedGATTCommands.get() > 0 && ( notificationsReceived.get() > 0 || indicationsReceived.get() > 0 ); + } catch (final Throwable t ) { + BTUtils.println(System.err, "****** Processing Ready Device: Exception caught for " + device.toString() + ": "+t.getMessage()); + t.printStackTrace(); + } + + BTUtils.println(System.err, "****** Processing Ready Device: End-1: Success " + success + + " on " + device.toString() + "; devInProc "+BTDeviceRegistry.getProcessingDeviceCount()); + + BTDeviceRegistry.removeFromProcessingDevices( device.getAddressAndType() ); + + if( DiscoveryPolicy.PAUSE_CONNECTED_UNTIL_DISCONNECTED == discoveryPolicy ) { + device.getAdapter().removeDevicePausingDiscovery(device); + } + + BTUtils.println(System.err, "****** Processing Ready Device: End-2: Success " + success + + " on " + device.toString() + "; devInProc "+BTDeviceRegistry.getProcessingDeviceCount()); + if( success ) { + BTDeviceRegistry.addToProcessedDevices(device.getAddressAndType(), device.getName()); + } + device.removeAllCharListener(); + + if( !KEEP_CONNECTED ) { + if( REMOVE_DEVICE ) { + device.remove(); + } else { + device.disconnect(); + } + } + + if( success ) { + completedMeasurements.addAndGet(1); + } + if( 0 < measurementsLeft.get() ) { + measurementsLeft.decrementAndGet(); + } + BTUtils.println(System.err, "****** Processing Ready Device: Success "+success+ + "; Measurements completed "+completedMeasurements.get()+ + ", done "+completedMeasurements.get()+ + ", left "+measurementsLeft.get()+ + "; Received notitifications "+notificationsReceived.get()+", indications "+indicationsReceived.get()+ + "; Completed GATT commands "+completedGATTCommands.get()+ + ": "+device.getAddressAndType()); + } + + private void removeDevice(final BTDevice device) { + BTUtils.println(System.err, "****** Remove Device: removing: "+device.getAddressAndType()); + + BTDeviceRegistry.removeFromProcessingDevices(device.getAddressAndType()); + + if( REMOVE_DEVICE ) { + device.remove(); + } + } + + DiscoveryPolicy discoveryPolicy = DiscoveryPolicy.PAUSE_CONNECTED_UNTIL_READY; // default value + boolean le_scan_active = true; // default value + static final short le_scan_interval = (short)24; // default value + static final short le_scan_window = (short)24; // default value + static final byte filter_policy = (byte)0; // default value + static final boolean filter_dup = true; // default value + + public HCIStatusCode startDiscovery(final BTAdapter adapter, final String msg) { + if( !useAdapter.equals(EUI48.ALL_DEVICE) && !useAdapter.equals(adapter.getAddressAndType().address) ) { + BTUtils.fprintf_td(System.err, "****** Start discovery (%s): Adapter not selected: %s\n", msg, adapter.toString()); + return HCIStatusCode.FAILED; + } + + resetLastProcessingStats(); + + final HCIStatusCode status = adapter.startDiscovery( discoveryPolicy, le_scan_active, le_scan_interval, le_scan_window, filter_policy, filter_dup ); + BTUtils.println(System.err, "****** Start discovery ("+msg+") result: "+status); + return status; + } + + public HCIStatusCode stopDiscovery(final BTAdapter adapter, final String msg) { + if( !useAdapter.equals(EUI48.ALL_DEVICE) && !useAdapter.equals(adapter.getAddressAndType().address) ) { + BTUtils.fprintf_td(System.err, "****** Stop discovery (%s): Adapter not selected: %s\n", msg, adapter.toString()); + return HCIStatusCode.FAILED; + } + + final HCIStatusCode status = adapter.stopDiscovery(); + BTUtils.println(System.err, "****** Stop discovery ("+msg+") result: "+status); + return status; + } + + public boolean initAdapter(final BTAdapter adapter) { + if( !useAdapter.equals(EUI48.ALL_DEVICE) && !useAdapter.equals(adapter.getAddressAndType().address) ) { + BTUtils.fprintf_td(System.err, "initClientAdapter: Adapter not selected: %s\n", adapter.toString()); + return false; + } + // Initialize with defaults and power-on + if( !adapter.isInitialized() ) { + final HCIStatusCode status = adapter.initialize( btMode ); + if( HCIStatusCode.SUCCESS != status ) { + BTUtils.fprintf_td(System.err, "initClientAdapter: Adapter initialization failed: %s: %s\n", + status.toString(), adapter.toString()); + return false; + } + } else if( !adapter.setPowered( true ) ) { + BTUtils.fprintf_td(System.err, "initClientAdapter: Already initialized adapter power-on failed:: %s\n", adapter.toString()); + return false; + } + // adapter is powered-on + BTUtils.fprintf_td(System.err, "initClientAdapter: %s\n", adapter.toString()); + { + final LE_Features le_feats = adapter.getLEFeatures(); + BTUtils.fprintf_td(System.err, "initClientAdapter: LE_Features %s\n", le_feats.toString()); + } + { + final LE_PHYs Tx = new LE_PHYs(LE_PHYs.PHY.LE_2M); + final LE_PHYs Rx = new LE_PHYs(LE_PHYs.PHY.LE_2M); + + final HCIStatusCode res = adapter.setDefaultLE_PHY(Tx, Rx); + BTUtils.fprintf_td(System.err, "initClientAdapter: Set Default LE PHY: status %s: Tx %s, Rx %s\n", + res.toString(), Tx.toString(), Rx.toString()); + } + final AdapterStatusListener asl = new MyAdapterStatusListener(); + adapter.addStatusListener( asl ); + + return true; + } +} diff --git a/trial/java/trial/org/direct_bt/DBTConstants.java b/trial/java/trial/org/direct_bt/DBTConstants.java new file mode 100644 index 00000000..5cf890d5 --- /dev/null +++ b/trial/java/trial/org/direct_bt/DBTConstants.java @@ -0,0 +1,31 @@ +/** + * Author: Sven Gothel <[email protected]> + * Copyright (c) 2022 Gothel Software e.K. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package trial.org.direct_bt; + +public class DBTConstants { + public static final String CLIENT_KEY_PATH = "client_keys"; + + public static final String SERVER_KEY_PATH = "server_keys"; +} diff --git a/trial/java/trial/org/direct_bt/DBTServer00.java b/trial/java/trial/org/direct_bt/DBTServer00.java new file mode 100644 index 00000000..571387a6 --- /dev/null +++ b/trial/java/trial/org/direct_bt/DBTServer00.java @@ -0,0 +1,742 @@ +/** + * Author: Sven Gothel <[email protected]> + * Copyright (c) 2022 Gothel Software e.K. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package trial.org.direct_bt; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.direct_bt.AdapterSettings; +import org.direct_bt.AdapterStatusListener; +import org.direct_bt.BDAddressAndType; +import org.direct_bt.BTMode; +import org.direct_bt.BTSecurityLevel; +import org.direct_bt.BTAdapter; +import org.direct_bt.BTDevice; +import org.direct_bt.BTDeviceRegistry; +import org.direct_bt.BTSecurityRegistry; +import org.direct_bt.BTUtils; +import org.direct_bt.DBGattChar; +import org.direct_bt.DBGattDesc; +import org.direct_bt.DBGattServer; +import org.direct_bt.DBGattService; +import org.direct_bt.DBGattValue; +import org.direct_bt.DiscoveryPolicy; +import org.direct_bt.EIRDataTypeSet; +import org.direct_bt.EInfoReport; +import org.direct_bt.GAPFlags; +import org.direct_bt.GattCharPropertySet; +import org.direct_bt.HCIStatusCode; +import org.direct_bt.LE_Features; +import org.direct_bt.LE_PHYs; +import org.direct_bt.PairingMode; +import org.direct_bt.SMPIOCapability; +import org.direct_bt.SMPPairingState; +import org.direct_bt.ScanType; +import org.jau.net.EUI48; + +/** + * This Java peripheral {@link BTRole::Slave} test case working with {@link DBTClient00}. + */ +public class DBTServer00 { + final boolean GATT_VERBOSE = false; + boolean SHOW_UPDATE_EVENTS = false; + + EUI48 useAdapter = EUI48.ALL_DEVICE; + BTMode btMode = BTMode.DUAL; + boolean use_SC = true; + String adapterName = "TestDev001_J"; + final String adapterShortName = "TDev001J"; + BTSecurityLevel adapterSecurityLevel = BTSecurityLevel.UNSET; + BTAdapter serverAdapter = null; + + public AtomicInteger servedConnections = new AtomicInteger(0); + + public DBTServer00(final EUI48 useAdapter, final BTMode btMode, final boolean use_SC, final String adapterName, final BTSecurityLevel adapterSecurityLevel) { + this.useAdapter = useAdapter; + this.btMode = btMode; + this.use_SC = use_SC; + this.adapterName = adapterName; + this.adapterSecurityLevel = adapterSecurityLevel; + + final MyGATTServerListener listener = new MyGATTServerListener(); + dbGattServer.addListener( listener ); + } + public DBTServer00(final EUI48 useAdapter, final String adapterName, final BTSecurityLevel adapterSecurityLevel) { + this(useAdapter, BTMode.DUAL, true /* SC */, adapterName, adapterSecurityLevel); + } + public DBTServer00(final String adapterName, final BTSecurityLevel adapterSecurityLevel) { + this(EUI48.ALL_DEVICE, BTMode.DUAL, true /* SC */, adapterName, adapterSecurityLevel); + } + public void setAdapter(final BTAdapter serverAdapter) { + this.serverAdapter = serverAdapter; + } + public BTAdapter getAdapter() { return serverAdapter; } + + boolean matches(final List<BDAddressAndType> cont, final BDAddressAndType mac) { + for(final Iterator<BDAddressAndType> it = cont.iterator(); it.hasNext(); ) { + if( it.next().matches(mac) ) { + return true; + } + } + return false; + } + + static void printf(final String format, final Object... args) { + final Object[] args2 = new Object[args.length+1]; + args2[0] = BTUtils.elapsedTimeMillis(); + System.arraycopy(args, 0, args2, 1, args.length); + System.err.printf("[%,9d] "+format, args2); + // System.err.printf("[%,9d] ", BluetoothUtils.getElapsedMillisecond()); + // System.err.printf(format, args); + } + + static Thread executeOffThread(final Runnable runobj, final String threadName, final boolean detach) { + final Thread t = new Thread( runobj, threadName ); + if( detach ) { + t.setDaemon(true); // detach thread + } + t.start(); + return t; + } + static Thread executeOffThread(final Runnable runobj, final boolean detach) { + final Thread t = new Thread( runobj ); + if( detach ) { + t.setDaemon(true); // detach thread + } + t.start(); + return t; + } + + static DBGattValue make_gvalue(final String name) { + final byte[] p = name.getBytes(StandardCharsets.UTF_8); + return new DBGattValue(p, p.length); + } + static DBGattValue make_gvalue(final String name, final int capacity) { + final byte[] p = name.getBytes(StandardCharsets.UTF_8); + return new DBGattValue(p, Math.max(capacity, p.length), capacity > p.length/* variable_length */); + } + static DBGattValue make_gvalue(final short v) { + final byte[] p = { (byte)0, (byte)0 }; + p[0] = (byte)(v); + p[1] = (byte)(v >> 8); + return new DBGattValue(p, p.length); + } + static DBGattValue make_gvalue(final int capacity, final int size) { + final byte[] p = new byte[size]; + return new DBGattValue(p, capacity, true /* variable_length */); + } + + static String DataServiceUUID = "d0ca6bf3-3d50-4760-98e5-fc5883e93712"; + static String StaticDataUUID = "d0ca6bf3-3d51-4760-98e5-fc5883e93712"; + static String CommandUUID = "d0ca6bf3-3d52-4760-98e5-fc5883e93712"; + static String ResponseUUID = "d0ca6bf3-3d53-4760-98e5-fc5883e93712"; + static String PulseDataUUID = "d0ca6bf3-3d54-4760-98e5-fc5883e93712"; + + // DBGattServerRef dbGattServer = std::make_shared<DBGattServer>( + private final DBGattServer dbGattServer = new DBGattServer( + /* services: */ + Arrays.asList( // DBGattService + new DBGattService ( true /* primary */, + DBGattService.UUID16.GENERIC_ACCESS /* type_ */, + Arrays.asList( // DBGattChar + new DBGattChar( DBGattChar.UUID16.DEVICE_NAME /* value_type_ */, + new GattCharPropertySet(GattCharPropertySet.Type.Read), + new ArrayList<DBGattDesc>(/* intentionally w/o Desc */ ), + make_gvalue(adapterName, 128) /* value */ ), + new DBGattChar( DBGattChar.UUID16.APPEARANCE /* value_type_ */, + new GattCharPropertySet(GattCharPropertySet.Type.Read), + new ArrayList<DBGattDesc>(/* intentionally w/o Desc */ ), + make_gvalue((short)0) /* value */ ) + ) ), + new DBGattService ( true /* primary */, + DBGattService.UUID16.DEVICE_INFORMATION /* type_ */, + Arrays.asList( // DBGattChar + new DBGattChar( DBGattChar.UUID16.MANUFACTURER_NAME_STRING /* value_type_ */, + new GattCharPropertySet(GattCharPropertySet.Type.Read), + new ArrayList<DBGattDesc>(/* intentionally w/o Desc */ ), + make_gvalue("Gothel Software") /* value */ ), + new DBGattChar( DBGattChar.UUID16.MODEL_NUMBER_STRING /* value_type_ */, + new GattCharPropertySet(GattCharPropertySet.Type.Read), + new ArrayList<DBGattDesc>(/* intentionally w/o Desc */ ), + make_gvalue("2.4.0-pre") /* value */ ), + new DBGattChar( DBGattChar.UUID16.SERIAL_NUMBER_STRING /* value_type_ */, + new GattCharPropertySet(GattCharPropertySet.Type.Read), + new ArrayList<DBGattDesc>(/* intentionally w/o Desc */ ), + make_gvalue("sn:0123456789") /* value */ ), + new DBGattChar( DBGattChar.UUID16.HARDWARE_REVISION_STRING /* value_type_ */, + new GattCharPropertySet(GattCharPropertySet.Type.Read), + new ArrayList<DBGattDesc>(/* intentionally w/o Desc */ ), + make_gvalue("hw:0123456789") /* value */ ), + new DBGattChar( DBGattChar.UUID16.FIRMWARE_REVISION_STRING /* value_type_ */, + new GattCharPropertySet(GattCharPropertySet.Type.Read), + new ArrayList<DBGattDesc>(/* intentionally w/o Desc */ ), + make_gvalue("fw:0123456789") /* value */ ), + new DBGattChar( DBGattChar.UUID16.SOFTWARE_REVISION_STRING /* value_type_ */, + new GattCharPropertySet(GattCharPropertySet.Type.Read), + new ArrayList<DBGattDesc>(/* intentionally w/o Desc */ ), + make_gvalue("sw:0123456789") /* value */ ) + ) ), + new DBGattService ( true /* primary */, + DataServiceUUID /* type_ */, + Arrays.asList( // DBGattChar + new DBGattChar( StaticDataUUID /* value_type_ */, + new GattCharPropertySet(GattCharPropertySet.Type.Read), + Arrays.asList( // DBGattDesc + new DBGattDesc( DBGattDesc.UUID16.USER_DESC, make_gvalue("DATA_STATIC") ) + ), + make_gvalue("Proprietary Static Data 0x00010203") /* value */ ), + new DBGattChar( CommandUUID /* value_type_ */, + new GattCharPropertySet(GattCharPropertySet.Type.WriteNoAck).set(GattCharPropertySet.Type.WriteWithAck), + Arrays.asList( // DBGattDesc + new DBGattDesc( DBGattDesc.UUID16.USER_DESC, make_gvalue("COMMAND") ) + ), + make_gvalue(128, 64) /* value */ ), + new DBGattChar( ResponseUUID /* value_type_ */, + new GattCharPropertySet(GattCharPropertySet.Type.Notify).set(GattCharPropertySet.Type.Indicate), + Arrays.asList( // DBGattDesc + new DBGattDesc( DBGattDesc.UUID16.USER_DESC, make_gvalue("RESPONSE") ), + DBGattDesc.createClientCharConfig() + ), + make_gvalue((short)0) /* value */ ), + new DBGattChar( PulseDataUUID /* value_type_ */, + new GattCharPropertySet(GattCharPropertySet.Type.Notify).set(GattCharPropertySet.Type.Indicate), + Arrays.asList( // DBGattDesc + new DBGattDesc( DBGattDesc.UUID16.USER_DESC, make_gvalue("DATA_PULSE") ), + DBGattDesc.createClientCharConfig() + ), + make_gvalue("Synthethic Sensor 01") /* value */ ) + ) ) + ) ); + + + class MyAdapterStatusListener extends AdapterStatusListener { + @Override + public void adapterSettingsChanged(final BTAdapter adapter, final AdapterSettings oldmask, + final AdapterSettings newmask, final AdapterSettings changedmask, final long timestamp) { + final boolean initialSetting = oldmask.isEmpty(); + if( initialSetting ) { + BTUtils.println(System.err, "****** Server SETTINGS: "+oldmask+" -> "+newmask+", initial "+changedmask); + } else { + BTUtils.println(System.err, "****** Server SETTINGS: "+oldmask+" -> "+newmask+", changed "+changedmask); + } + BTUtils.println(System.err, "Status Adapter:"); + BTUtils.println(System.err, adapter.toString()); + } + + @Override + public void discoveringChanged(final BTAdapter adapter, final ScanType currentMeta, final ScanType changedType, final boolean changedEnabled, final DiscoveryPolicy policy, final long timestamp) { + BTUtils.println(System.err, "****** Server DISCOVERING: meta "+currentMeta+", changed["+changedType+", enabled "+changedEnabled+", policy "+policy+"] on "+adapter); + } + + @Override + public boolean deviceFound(final BTDevice device, final long timestamp) { + BTUtils.println(System.err, "****** Server FOUND__-1: NOP "+device.toString()); + return false; + } + + @Override + public void deviceUpdated(final BTDevice device, final EIRDataTypeSet updateMask, final long timestamp) { + if( SHOW_UPDATE_EVENTS ) { + BTUtils.println(System.err, "****** Server UPDATED: "+updateMask+" of "+device); + } + } + + @Override + public void deviceConnected(final BTDevice device, final short handle, final long timestamp) { + BTUtils.println(System.err, "****** Server CONNECTED: "+device.toString()); + } + + @Override + public void devicePairingState(final BTDevice device, final SMPPairingState state, final PairingMode mode, final long timestamp) { + BTUtils.println(System.err, "****** Server PAIRING_STATE: state "+state+", mode "+mode+": "+device); + switch( state ) { + case NONE: + // next: deviceReady(..) + break; + case FAILED: { + // next: deviceReady() or deviceDisconnected(..) + } break; + case REQUESTED_BY_RESPONDER: + // next: FEATURE_EXCHANGE_STARTED + break; + case FEATURE_EXCHANGE_STARTED: + // next: FEATURE_EXCHANGE_COMPLETED + break; + case FEATURE_EXCHANGE_COMPLETED: + // next: PASSKEY_EXPECTED... or KEY_DISTRIBUTION + break; + case PASSKEY_EXPECTED: { + final BTSecurityRegistry.Entry sec = BTSecurityRegistry.getStartOf(device.getAddressAndType().address, ""); + if( null != sec && sec.getPairingPasskey() != BTSecurityRegistry.NO_PASSKEY ) { + executeOffThread( () -> { device.setPairingPasskey( sec.getPairingPasskey() ); }, "DBT-SetPasskey-"+device.getAddressAndType(), true /* detach */); + } else { + executeOffThread( () -> { device.setPairingPasskey( 0 ); }, "DBT-SetPasskey-"+device.getAddressAndType(), true /* detach */); + // 3s disconnect: executeOffThread( () -> { device.setPairingPasskeyNegative(); }, "DBT-SetPasskeyNegative-"+device.getAddressAndType(), true /* detach */); + } + // next: KEY_DISTRIBUTION or FAILED + } break; + case NUMERIC_COMPARE_EXPECTED: { + final BTSecurityRegistry.Entry sec = BTSecurityRegistry.getStartOf(device.getAddressAndType().address, ""); + if( null != sec ) { + executeOffThread( () -> { device.setPairingNumericComparison( sec.getPairingNumericComparison() ); }, "DBT-SetNumericComp-"+device.getAddressAndType(), true /* detach */); + } else { + executeOffThread( () -> { device.setPairingNumericComparison( false ); }, "DBT-SetNumericCompFalse-"+device.getAddressAndType(), true /* detach */); + } + // next: KEY_DISTRIBUTION or FAILED + } break; + case OOB_EXPECTED: + // FIXME: ABORT + break; + case KEY_DISTRIBUTION: + // next: COMPLETED or FAILED + break; + case COMPLETED: + // next: deviceReady(..) + break; + default: // nop + break; + } + } + + @Override + public void deviceReady(final BTDevice device, final long timestamp) { + BTUtils.println(System.err, "****** Server READY-1: NOP " + device.toString()); + } + + @Override + public void deviceDisconnected(final BTDevice device, final HCIStatusCode reason, final short handle, final long timestamp) { + BTUtils.println(System.err, "****** Server DISCONNECTED (count "+(servedConnections.get()+1)+"): Reason "+reason+", old handle 0x"+Integer.toHexString(handle)+": "+device+" on "+device.getAdapter()); + + executeOffThread( () -> { processDisconnectedDevice(device); }, + "DBT-Disconnected-"+device.getAdapter().getAddressAndType(), true /* detach */); + } + + @Override + public String toString() { + return "AdapterStatusListener[user, per-adapter]"; + } + }; + + class MyGATTServerListener extends DBGattServer.Listener implements AutoCloseable { + private volatile boolean sync_data; + private final Thread pulseSenderThread; + private volatile boolean stopPulseSender = false; + + private volatile short handlePulseDataNotify = 0; + private volatile short handlePulseDataIndicate = 0; + private volatile short handleResponseDataNotify = 0; + private volatile short handleResponseDataIndicate = 0; + + private BTDevice connectedDevice; + private int usedMTU = 23; // BTGattHandler::number(BTGattHandler::Defaults::MIN_ATT_MTU); + + private boolean matches(final BTDevice device) { + final boolean local = sync_data; // SC-DRF acquire via sc_atomic_bool::load() + final boolean res = null != connectedDevice ? connectedDevice.equals(device) : false; + sync_data = local; // SC-DRF release via sc_atomic_bool::store() + return res; + } + + private void clear() { + final boolean local = sync_data; // SC-DRF acquire via sc_atomic_bool::load() + + handlePulseDataNotify = 0; + handlePulseDataIndicate = 0; + handleResponseDataNotify = 0; + handleResponseDataIndicate = 0; + connectedDevice = null; + + dbGattServer.resetGattClientCharConfig(DataServiceUUID, PulseDataUUID); + dbGattServer.resetGattClientCharConfig(DataServiceUUID, ResponseUUID); + + sync_data = local; // SC-DRF release via sc_atomic_bool::store() + } + + private void pulseSender() { + while( !stopPulseSender ) { + { + final boolean local = sync_data; // SC-DRF acquire via sc_atomic_bool::load() + if( null != connectedDevice && connectedDevice.getConnected() ) { + if( 0 != handlePulseDataNotify || 0 != handlePulseDataIndicate ) { + final String data = String.format("Dynamic Data Example. Elapsed Milliseconds: %,9d", BTUtils.elapsedTimeMillis()); + final byte[] v = data.getBytes(StandardCharsets.UTF_8); + if( 0 != handlePulseDataNotify ) { + if( GATT_VERBOSE ) { + BTUtils.fprintf_td(System.err, "****** GATT::sendNotification: PULSE to %s\n", connectedDevice.toString()); + } + connectedDevice.sendNotification(handlePulseDataNotify, v); + } + if( 0 != handlePulseDataIndicate ) { + if( GATT_VERBOSE ) { + BTUtils.fprintf_td(System.err, "****** GATT::sendIndication: PULSE to %s\n", connectedDevice.toString()); + } + connectedDevice.sendIndication(handlePulseDataIndicate, v); + } + } + } + sync_data = local; // SC-DRF release via sc_atomic_bool::store() + } + try { + Thread.sleep(500); // 500ms + } catch (final InterruptedException e) { } + } + } + + void sendResponse(final byte[] data) { + if( null != connectedDevice && connectedDevice.getConnected() ) { + if( 0 != handleResponseDataNotify || 0 != handleResponseDataIndicate ) { + if( 0 != handleResponseDataNotify ) { + if( GATT_VERBOSE ) { + BTUtils.fprintf_td(System.err, "****** GATT::sendNotification: %s to %s\n", + BTUtils.bytesHexString(data, 0, data.length, true /* lsb */), connectedDevice.toString()); + } + connectedDevice.sendNotification(handleResponseDataNotify, data); + } + if( 0 != handleResponseDataIndicate ) { + if( GATT_VERBOSE ) { + BTUtils.fprintf_td(System.err, "****** GATT::sendIndication: %s to %s\n", + BTUtils.bytesHexString(data, 0, data.length, true /* lsb */), connectedDevice.toString()); + } + connectedDevice.sendIndication(handleResponseDataIndicate, data); + } + } + } + } + + public MyGATTServerListener() { + pulseSenderThread = executeOffThread( () -> { pulseSender(); }, "GattServer-PulseSender", false /* detach */); + } + + @Override + public void close() { + { + final boolean local = sync_data; // SC-DRF acquire via sc_atomic_bool::load() + stopPulseSender = true; + connectedDevice = null; + sync_data = local; // SC-DRF release via sc_atomic_bool::store() + } + if( !pulseSenderThread.isDaemon() ) { + try { + pulseSenderThread.join(1000); + } catch (final InterruptedException e) { } + } + super.close(); + } + + @Override + public void connected(final BTDevice device, final int initialMTU) { + final boolean local = sync_data; // SC-DRF acquire via sc_atomic_bool::load() + final boolean available = null == connectedDevice; + BTUtils.fprintf_td(System.err, "****** GATT::connected(available %b): initMTU %d, %s\n", + available, initialMTU, device.toString()); + if( available ) { + connectedDevice = device; + usedMTU = initialMTU; + } + sync_data = local; // SC-DRF release via sc_atomic_bool::store() + } + + @Override + public void disconnected(final BTDevice device) { + final boolean local = sync_data; // SC-DRF acquire via sc_atomic_bool::load() + final boolean match = null != connectedDevice ? connectedDevice.equals(device) : false; + BTUtils.fprintf_td(System.err, "****** GATT::disconnected(match %b): %s\n", match, device.toString()); + if( match ) { + clear(); + } + sync_data = local; // SC-DRF release via sc_atomic_bool::store() + } + + @Override + public void mtuChanged(final BTDevice device, final int mtu) { + final boolean match = matches(device); + if( GATT_VERBOSE ) { + BTUtils.fprintf_td(System.err, "****** GATT::mtuChanged(match %b): %d -> %d, %s\n", + match, match ? (int)usedMTU : 0, mtu, device.toString()); + } + if( match ) { + usedMTU = mtu; + } + } + + @Override + public boolean readCharValue(final BTDevice device, final DBGattService s, final DBGattChar c) { + final boolean match = matches(device); + if( GATT_VERBOSE ) { + BTUtils.fprintf_td(System.err, "****** GATT::readCharValue(match %b): to %s, from\n %s\n %s\n", + match, device.toString(), s.toString(), c.toString()); + } + return match; + } + + @Override + public boolean readDescValue(final BTDevice device, final DBGattService s, final DBGattChar c, final DBGattDesc d) { + final boolean match = matches(device); + if( GATT_VERBOSE ) { + BTUtils.fprintf_td(System.err, "****** GATT::readDescValue(match %b): to %s, from\n %s\n %s\n %s\n", + match, device.toString(), s.toString(), c.toString(), d.toString()); + } + return match; + } + + @Override + public boolean writeCharValue(final BTDevice device, final DBGattService s, final DBGattChar c, final byte[] value, final int value_offset) { + final boolean match = matches(device); + if( GATT_VERBOSE ) { + final String value_s = BTUtils.bytesHexString(value, 0, value.length, true /* lsbFirst */)+ + " '"+BTUtils.decodeUTF8String(value, 0, value.length)+"'"; + BTUtils.fprintf_td(System.err, "****** GATT::writeCharValue(match %b): %s @ %d from %s, to\n %s\n %s\n", + match, value_s, value_offset, + device.toString(), s.toString(), c.toString()); + } + return match; + } + + @Override + public void writeCharValueDone(final BTDevice device, final DBGattService s, final DBGattChar c) { + final boolean match = matches(device); + final DBGattValue value = c.getValue(); + if( GATT_VERBOSE ) { + BTUtils.fprintf_td(System.err, "****** GATT::writeCharValueDone(match %b): From %s, to\n %s\n %s\n Char-Value: %s\n", + match, device.toString(), s.toString(), c.toString(), value.toString()); + } + if( match && + c.getValueType().equals( CommandUUID ) && + ( 0 != handleResponseDataNotify || 0 != handleResponseDataIndicate ) ) + { + executeOffThread( () -> { sendResponse( value.data() ); }, true /* detach */); + } + } + + @Override + public boolean writeDescValue(final BTDevice device, final DBGattService s, final DBGattChar c, final DBGattDesc d, + final byte[] value, final int value_offset) + { + final boolean match = matches(device); + if( GATT_VERBOSE ) { + final String value_s = BTUtils.bytesHexString(value, 0, value.length, true /* lsbFirst */)+ + " '"+BTUtils.decodeUTF8String(value, 0, value.length)+"'"; + BTUtils.fprintf_td(System.err, "****** GATT::writeDescValue(match %b): %s @ %d from %s\n %s\n %s\n %s\n", + match, value_s, value_offset, + device.toString(), s.toString(), c.toString(), d.toString()); + } + return match; + } + + @Override + public void writeDescValueDone(final BTDevice device, final DBGattService s, final DBGattChar c, final DBGattDesc d) { + if( GATT_VERBOSE ) { + final boolean match = matches(device); + final DBGattValue value = d.getValue(); + BTUtils.fprintf_td(System.err, "****** GATT::writeDescValueDone(match %b): From %s\n %s\n %s\n %s\n Desc-Value: %s\n", + match, device.toString(), s.toString(), c.toString(), d.toString(), value.toString()); + } + } + + @Override + public void clientCharConfigChanged(final BTDevice device, final DBGattService s, final DBGattChar c, final DBGattDesc d, + final boolean notificationEnabled, final boolean indicationEnabled) + { + final boolean match = matches(device); + if( GATT_VERBOSE ) { + final DBGattValue value = d.getValue(); + BTUtils.fprintf_td(System.err, "****** GATT::clientCharConfigChanged(match %b): notify %b, indicate %b from %s\n %s\n %s\n %s\n Desc-Value: %s\n", + match, notificationEnabled, indicationEnabled, + device.toString(), s.toString(), c.toString(), d.toString(), value.toString()); + } + if( match ) { + final boolean local = sync_data; // SC-DRF acquire via sc_atomic_bool::load() + final String value_type = c.getValueType(); + final short value_handle = c.getValueHandle(); + if( value_type.equals( PulseDataUUID ) ) { + handlePulseDataNotify = notificationEnabled ? value_handle : 0; + handlePulseDataIndicate = indicationEnabled ? value_handle : 0; + } else if( value_type.equals( ResponseUUID ) ) { + handleResponseDataNotify = notificationEnabled ? value_handle : 0; + handleResponseDataIndicate = indicationEnabled ? value_handle : 0; + } + sync_data = local; // SC-DRF release via sc_atomic_bool::store() + } + } + } + static final short adv_interval_min=(short)640; + static final short adv_interval_max=(short)640; + static final byte adv_type=(byte)0; // AD_PDU_Type::ADV_IND; + static final byte adv_chan_map=(byte)0x07; + static final byte filter_policy=(byte)0x00; + + public HCIStatusCode stopAdvertising(final BTAdapter adapter, final String msg) { + if( !useAdapter.equals(EUI48.ALL_DEVICE) && !useAdapter.equals(adapter.getAddressAndType().address) ) { + BTUtils.fprintf_td(System.err, "****** Stop advertising (%s): Adapter not selected: %s\n", msg, adapter.toString()); + return HCIStatusCode.FAILED; + } + final HCIStatusCode status = adapter.stopAdvertising(); + BTUtils.println(System.err, "****** Stop advertising ("+msg+") result: "+status+": "+adapter.toString()); + return status; + } + + public HCIStatusCode startAdvertising(final BTAdapter adapter, final String msg) { + if( !useAdapter.equals(EUI48.ALL_DEVICE) && !useAdapter.equals(adapter.getAddressAndType().address) ) { + BTUtils.fprintf_td(System.err, "****** Start advertising (%s): Adapter not selected: %s\n", msg, adapter.toString()); + return HCIStatusCode.FAILED; + } + final EInfoReport eir = new EInfoReport(); + final EIRDataTypeSet adv_mask = new EIRDataTypeSet(); + final EIRDataTypeSet scanrsp_mask = new EIRDataTypeSet(); + + adv_mask.set(EIRDataTypeSet.DataType.FLAGS); + adv_mask.set(EIRDataTypeSet.DataType.SERVICE_UUID); + + scanrsp_mask.set(EIRDataTypeSet.DataType.NAME); + scanrsp_mask.set(EIRDataTypeSet.DataType.CONN_IVAL); + + eir.addFlag(GAPFlags.Bit.LE_Gen_Disc); + eir.addFlag(GAPFlags.Bit.BREDR_UNSUP); + + eir.addService(DataServiceUUID); + eir.setServicesComplete(false); + + eir.setName(adapter.getName()); + eir.setConnInterval((short)10, (short)24); + + final DBGattChar gattDevNameChar = dbGattServer.findGattChar(DBGattService.UUID16.GENERIC_ACCESS, DBGattChar.UUID16.DEVICE_NAME); + if( null != gattDevNameChar ) { + final byte[] aname_bytes = adapter.getName().getBytes(StandardCharsets.UTF_8); + gattDevNameChar.setValue(aname_bytes, 0, aname_bytes.length, 0); + } + + BTUtils.println(System.err, "****** Start advertising ("+msg+"): EIR "+eir.toString()); + BTUtils.println(System.err, "****** Start advertising ("+msg+"): adv "+adv_mask.toString()+", scanrsp "+scanrsp_mask.toString()); + + final HCIStatusCode status = adapter.startAdvertising(dbGattServer, eir, adv_mask, scanrsp_mask, + adv_interval_min, adv_interval_max, + adv_type, adv_chan_map, filter_policy); + BTUtils.println(System.err, "****** Start advertising ("+msg+") result: "+status+": "+adapter.toString()); + if( GATT_VERBOSE ) { + BTUtils.println(System.err, dbGattServer.toFullString()); + } + return status; + } + + private void processDisconnectedDevice(final BTDevice device) { + BTUtils.println(System.err, "****** Disconnected Device (count "+(servedConnections.get()+1)+"): Start "+device.toString()); + + // already unpaired + BTDeviceRegistry.removeFromProcessingDevices(device.getAddressAndType()); + try { + Thread.sleep(100); // wait a little (FIXME: Fast restart of advertising error) + } catch (final InterruptedException e) { } + + BTUtils.println(System.err, "****** Disonnected Device: End "+device.toString()); + servedConnections.addAndGet(1); + } + + public boolean initAdapter(final BTAdapter adapter) { + if( !useAdapter.equals(EUI48.ALL_DEVICE) && !useAdapter.equals(adapter.getAddressAndType().address) ) { + BTUtils.fprintf_td(System.err, "initServerAdapter: Adapter not selected: %s\n", adapter.toString()); + return false; + } + if( !adapter.isInitialized() ) { + // Initialize with defaults and power-on + final HCIStatusCode status = adapter.initialize( btMode ); + if( HCIStatusCode.SUCCESS != status ) { + BTUtils.fprintf_td(System.err, "initServerAdapter: initialization failed: %s: %s\n", + status.toString(), adapter.toString()); + return false; + } + } else if( !adapter.setPowered( true ) ) { + BTUtils.fprintf_td(System.err, "initServerAdapter: setPower.1 on failed: %s\n", adapter.toString()); + return false; + } + // adapter is powered-on + BTUtils.println(System.err, "initServerAdapter.1: "+adapter.toString()); + + if( adapter.setPowered(false) ) { + HCIStatusCode status = adapter.setName(adapterName, adapterShortName); + if( HCIStatusCode.SUCCESS == status ) { + BTUtils.fprintf_td(System.err, "initServerAdapter: setLocalName OK: %s\n", adapter.toString()); + } else { + BTUtils.fprintf_td(System.err, "initServerAdapter: setLocalName failed: %s\n", adapter.toString()); + return false; + } + + status = adapter.setSecureConnections( use_SC ); + if( HCIStatusCode.SUCCESS == status ) { + BTUtils.fprintf_td(System.err, "initServerAdapter: setSecureConnections OK: %s\n", adapter.toString()); + } else { + BTUtils.fprintf_td(System.err, "initServerAdapter: setSecureConnections failed: %s\n", adapter.toString()); + return false; + } + + final short conn_min_interval = 8; // 10ms + final short conn_max_interval = 40; // 50ms + final short conn_latency = 0; + final short supervision_timeout = 50; // 500ms + status = adapter.setDefaultConnParam(conn_min_interval, conn_max_interval, conn_latency, supervision_timeout); + if( HCIStatusCode.SUCCESS == status ) { + BTUtils.fprintf_td(System.err, "initServerAdapter: setDefaultConnParam OK: %s\n", adapter.toString()); + } else { + BTUtils.fprintf_td(System.err, "initServerAdapter: setDefaultConnParam failed: %s\n", adapter.toString()); + return false; + } + + if( !adapter.setPowered( true ) ) { + BTUtils.fprintf_td(System.err, "initServerAdapter: setPower.2 on failed: %s\n", adapter.toString()); + return false; + } + } else { + BTUtils.fprintf_td(System.err, "initServerAdapter: setPowered.2 off failed: %s\n", adapter.toString()); + } + BTUtils.println(System.err, "initServerAdapter.2: "+adapter.toString()); + + { + final LE_Features le_feats = adapter.getLEFeatures(); + BTUtils.fprintf_td(System.err, "initServerAdapter: LE_Features %s\n", le_feats.toString()); + } + { + final LE_PHYs Tx = new LE_PHYs( LE_PHYs.PHY.LE_2M ); + final LE_PHYs Rx = new LE_PHYs( LE_PHYs.PHY.LE_2M ); + final HCIStatusCode res = adapter.setDefaultLE_PHY(Tx, Rx); + BTUtils.fprintf_td(System.err, "initServerAdapter: Set Default LE PHY: status %s: Tx %s, Rx %s\n", + res.toString(), Tx.toString(), Rx.toString()); + } + adapter.setSMPKeyPath(DBTConstants.SERVER_KEY_PATH); + + // adapter is powered-on + final AdapterStatusListener asl = new MyAdapterStatusListener(); + adapter.addStatusListener( asl ); + // Flush discovered devices after registering our status listener. + // This avoids discovered devices before we have registered! + adapter.removeDiscoveredDevices(); + + adapter.setServerConnSecurity(adapterSecurityLevel, SMPIOCapability.UNSET); + + return true; + } +} diff --git a/trial/java/trial/org/direct_bt/DBTUtils.java b/trial/java/trial/org/direct_bt/DBTUtils.java new file mode 100644 index 00000000..586bc2ea --- /dev/null +++ b/trial/java/trial/org/direct_bt/DBTUtils.java @@ -0,0 +1,177 @@ +/** + * Author: Sven Gothel <[email protected]> + * Copyright (c) 2022 Gothel Software e.K. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package trial.org.direct_bt; + +import java.io.File; +import java.nio.file.Files; +import java.util.Iterator; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.direct_bt.BTFactory; +import org.direct_bt.BTUtils; +import org.direct_bt.DirectBTVersion; +import org.jau.util.VersionUtil; + +public class DBTUtils { + public static final void printVersionInfo() { + BTFactory.initDirectBTLibrary(); + + BTUtils.println(System.err, "BTFactory: Jaulib: Available "+BTFactory.JAULIB_AVAILABLE+", JarCache in use "+BTFactory.JAULIB_JARCACHE_USED); + if( BTFactory.JAULIB_AVAILABLE ) { + System.err.println(VersionUtil.getPlatformInfo()); + BTUtils.println(System.err, "Version Info:"); + final DirectBTVersion v = DirectBTVersion.getInstance(); + System.err.println(v.toString()); + BTUtils.println(System.err, ""); + BTUtils.println(System.err, "Full Manifest:"); + System.err.println(v.getFullManifestInfo(null).toString()); + } else { + BTUtils.println(System.err, "Full Manifest:"); + final Manifest manifest = BTFactory.getManifest(BTFactory.class.getClassLoader(), new String[] { "org.direct_bt" } ); + final Attributes attr = manifest.getMainAttributes(); + final Set<Object> keys = attr.keySet(); + final StringBuilder sb = new StringBuilder(); + for(final Iterator<Object> iter=keys.iterator(); iter.hasNext(); ) { + final Attributes.Name key = (Attributes.Name) iter.next(); + final String val = attr.getValue(key); + sb.append(" "); + sb.append(key); + sb.append(" = "); + sb.append(val); + sb.append(System.lineSeparator()); + } + System.err.println(sb.toString()); + } + + BTUtils.println(System.err, "DirectBT Native Version "+BTFactory.getNativeVersion()+" (API "+BTFactory.getNativeAPIVersion()+")"); + BTUtils.println(System.err, "DirectBT Java Version "+BTFactory.getImplVersion()+" (API "+BTFactory.getAPIVersion()+")"); + } + + public static final boolean mkdirKeyFolder() { + BTFactory.initDirectBTLibrary(); + boolean res = true; + { + final File file = new File(DBTConstants.CLIENT_KEY_PATH); + try { + if( !file.isDirectory() ) { + final boolean res2 = file.mkdirs(); + BTUtils.println(System.err, "****** KEY_PATH '"+file.toString()+"': mkdir: "+res2); + res = res && res2; + } else { + BTUtils.println(System.err, "****** KEY_PATH '"+file.toString()+"': already exists"); + } + } catch(final Throwable t) { + BTUtils.println(System.err, "****** KEY_PATH '"+file.toString()+"': Caught "+t.getMessage()); + res = false; + } + } + if( res ) { + final File file = new File(DBTConstants.SERVER_KEY_PATH); + try { + if( !file.isDirectory() ) { + final boolean res2 = file.mkdirs(); + BTUtils.println(System.err, "****** KEY_PATH '"+file.toString()+"': mkdir: "+res2); + res = res && res2; + } else { + BTUtils.println(System.err, "****** KEY_PATH '"+file.toString()+"': already exists"); + } + } catch(final Throwable t) { + BTUtils.println(System.err, "****** KEY_PATH '"+file.toString()+"': Caught "+t.getMessage()); + res = false; + } + } + return res; + } + + /** + * + * @param file + * @param recursive + * @return true only if the file or the directory with content has been deleted, otherwise false + */ + private static boolean delete(final File file, final boolean recursive) { + boolean rm_parent = true; + final File[] contents = file.listFiles(); + if (contents != null) { + for (final File f : contents) { + if( f.isDirectory() && !Files.isSymbolicLink( f.toPath() ) ) { + if( recursive ) { + rm_parent = delete(f, true) && rm_parent; + } else { + // can't empty contents -> can't rm 'file' + rm_parent = false; + } + } else { + try { + rm_parent = f.delete() && rm_parent; + } catch( final Exception e ) { + e.printStackTrace(); + rm_parent = false; + } + } + } + } + if( rm_parent ) { + try { + return file.delete(); + } catch( final Exception e ) { e.printStackTrace(); } + } + return false; + } + + public static final boolean rmKeyFolder() { + BTFactory.initDirectBTLibrary(); + boolean res = true; + { + final File file = new File(DBTConstants.CLIENT_KEY_PATH); + try { + if( file.isDirectory() ) { + final boolean res2 = delete(file, false /* recursive */); + res = res2 && res; + BTUtils.println(System.err, "****** KEY_PATH '"+file.toString()+"': delete: "+res2); + } + } catch(final Throwable t) { + BTUtils.println(System.err, "****** KEY_PATH '"+file.toString()+"': Caught "+t.getMessage()); + res = false; + } + } + if( res ) { + final File file = new File(DBTConstants.SERVER_KEY_PATH); + try { + if( file.isDirectory() ) { + final boolean res2 = delete(file, false /* recursive */); + res = res2 && res; + BTUtils.println(System.err, "****** KEY_PATH '"+file.toString()+"': delete: "+res2); + } + } catch(final Throwable t) { + BTUtils.println(System.err, "****** KEY_PATH '"+file.toString()+"': Caught "+t.getMessage()); + res = false; + } + } + return res; + } +} diff --git a/trial/java/trial/org/direct_bt/TestDBTClientServer00.java b/trial/java/trial/org/direct_bt/TestDBTClientServer00.java new file mode 100644 index 00000000..161a65a5 --- /dev/null +++ b/trial/java/trial/org/direct_bt/TestDBTClientServer00.java @@ -0,0 +1,217 @@ +/** + * Author: Sven Gothel <[email protected]> + * Copyright (c) 2022 Gothel Software e.K. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package trial.org.direct_bt; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import org.direct_bt.BTMode; +import org.direct_bt.BTRole; +import org.direct_bt.BTSecurityLevel; +import org.direct_bt.BTAdapter; +import org.direct_bt.BTException; +import org.direct_bt.BTFactory; +import org.direct_bt.BTManager; +import org.direct_bt.BTUtils; +import org.direct_bt.HCIStatusCode; +import org.jau.net.EUI48; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +/** + * Basic client and server Bluetooth tests, requiring one BT adapter. + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class TestDBTClientServer00 extends BaseDBTClientServer { + static final boolean DEBUG = false; + + @BeforeClass + public static final void setupAllLocal() { + BTFactory.initDirectBTLibrary(); + + final Class<?> ThisClazz = MethodHandles.lookup().lookupClass(); + BTUtils.println(System.err, "++++ Test "+ThisClazz.getSimpleName()+".setupAllLocal()"); + + DBTUtils.printVersionInfo(); + } + + /** + * Testing BTManager bring up and + * - test that at least two adapter are present + * - validating basic default adapter status + */ + @Test(timeout = 5000) + public final void test01_ManagerBringup() { + BTManager manager = null; + try { + manager = BTFactory.getDirectBTManager(); + } catch (BTException | NoSuchMethodException | SecurityException + | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | ClassNotFoundException e) { + e.printStackTrace(); + Assert.assertNull("Unable to instantiate DirectBT BluetoothManager: "+e.getMessage(), e); + } + if( null == manager ) { + return; + } + final List<BTAdapter> adapters = manager.getAdapters(); + BTUtils.println(System.err, "Adapter: Count "+adapters.size()+": "+adapters.toString()); + Assert.assertTrue("Adapter count not >= 1 but "+adapters.size(), adapters.size() >= 1); + + for(final BTAdapter a : adapters) { + Assert.assertFalse( a.isInitialized() ); + Assert.assertFalse( a.isPowered() ); + Assert.assertEquals( BTRole.Master, a.getRole() ); // default role + Assert.assertTrue( 4 <= a.getBTMajorVersion() ); + } + } + + /** + * Testing start and stop advertising (server mode) using a full DBGattServer, + * having the adapter in BTRole::Slave. + * + * Thereafter start and stop discovery (client mode), + * having the adapter in BTRole::Client. + */ + @Test(timeout = 5000) + public final void test10_ServerStartStop_and_ToggleDiscovery() { + BTManager manager = null; + try { + manager = BTFactory.getDirectBTManager(); + } catch (BTException | NoSuchMethodException | SecurityException + | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | ClassNotFoundException e) { + e.printStackTrace(); + Assert.assertNull("Unable to instantiate DirectBT BluetoothManager: "+e.getMessage(), e); + } + if( null == manager ) { + return; + } + + final String serverName = "TestDBTCS00-T10"; + final DBTServer00 server = new DBTServer00(EUI48.ALL_DEVICE, BTMode.DUAL, true /* SC */, serverName, BTSecurityLevel.NONE); + + final BTManager.ChangedAdapterSetListener myChangedAdapterSetListener = + new BTManager.ChangedAdapterSetListener() { + @Override + public void adapterAdded(final BTAdapter adapter) { + if( null == server.getAdapter() ) { + if( server.initAdapter( adapter ) ) { + server.setAdapter(adapter); + BTUtils.println(System.err, "****** Adapter-Server ADDED__: InitOK: " + adapter); + return; + } + } + BTUtils.println(System.err, "****** Adapter ADDED__: Ignored: " + adapter); + } + + @Override + public void adapterRemoved(final BTAdapter adapter) { + if( null != server.getAdapter() && adapter == server.getAdapter() ) { + server.setAdapter(null); + BTUtils.println(System.err, "****** Adapter-Server REMOVED: " + adapter); + return; + } + BTUtils.println(System.err, "****** Adapter REMOVED: Ignored " + adapter); + } + }; + + manager.addChangedAdapterSetListener(myChangedAdapterSetListener); + Assert.assertNotNull("No server adapter found", server.getAdapter()); + + // + // Server start + // + Assert.assertTrue( server.getAdapter().isInitialized() ); + Assert.assertTrue( server.getAdapter().isPowered() ); + Assert.assertEquals( BTRole.Master, server.getAdapter().getRole() ); + Assert.assertTrue( 4 <= server.getAdapter().getBTMajorVersion() ); + { + Assert.assertFalse(server.getAdapter().isAdvertising()); + Assert.assertFalse(server.getAdapter().isDiscovering()); + + Assert.assertEquals( HCIStatusCode.SUCCESS, server.startAdvertising(server.getAdapter(), "test10_startAdvertising") ); + Assert.assertTrue(server.getAdapter().isAdvertising()); + Assert.assertFalse(server.getAdapter().isDiscovering()); + Assert.assertEquals( BTRole.Slave, server.getAdapter().getRole() ); + Assert.assertEquals( serverName, server.getAdapter().getName() ); + } + + // + // Server stop + // + { + Assert.assertTrue(server.getAdapter().isAdvertising()); + Assert.assertFalse(server.getAdapter().isDiscovering()); + + Assert.assertEquals( HCIStatusCode.SUCCESS, server.stopAdvertising(server.getAdapter(), "test10_stopAdvertising") ); + Assert.assertFalse(server.getAdapter().isAdvertising()); + Assert.assertFalse(server.getAdapter().isDiscovering()); + Assert.assertEquals( BTRole.Slave, server.getAdapter().getRole() ); // keeps role + } + + // + // Now reuse adapter for client mode -> Start discovery + Stop Discovery + // + { + Assert.assertFalse(server.getAdapter().isAdvertising()); + Assert.assertFalse(server.getAdapter().isDiscovering()); + + final BTAdapter adapter = server.getAdapter(); + { + final int r = adapter.removeAllStatusListener(); + Assert.assertTrue("Not > 0 removed listener, but "+r, 0 < r ); + } + + Assert.assertEquals( HCIStatusCode.SUCCESS, adapter.startDiscovery() ); // pending action + while( !adapter.isDiscovering() ) { + try { Thread.sleep(100); } catch (final InterruptedException e) { e.printStackTrace(); } + } + Assert.assertFalse(server.getAdapter().isAdvertising()); + Assert.assertTrue(server.getAdapter().isDiscovering()); + Assert.assertEquals( BTRole.Master, server.getAdapter().getRole() ); // changed role + + Assert.assertEquals( HCIStatusCode.SUCCESS, adapter.stopDiscovery() ); // pending action + while( adapter.isDiscovering() ) { + try { Thread.sleep(100); } catch (final InterruptedException e) { e.printStackTrace(); } + } + Assert.assertFalse(server.getAdapter().isAdvertising()); + Assert.assertFalse(server.getAdapter().isDiscovering()); + Assert.assertEquals( BTRole.Master, server.getAdapter().getRole() ); // keeps role + + } + + final int count = manager.removeChangedAdapterSetListener(myChangedAdapterSetListener); + BTUtils.println(System.err, "****** EOL Removed ChangedAdapterSetCallback " + count); + } + + public static void main(final String args[]) { + org.junit.runner.JUnitCore.main(TestDBTClientServer00.class.getName()); + } +} diff --git a/trial/java/trial/org/direct_bt/TestDBTClientServer10.java b/trial/java/trial/org/direct_bt/TestDBTClientServer10.java new file mode 100644 index 00000000..295c918a --- /dev/null +++ b/trial/java/trial/org/direct_bt/TestDBTClientServer10.java @@ -0,0 +1,259 @@ +/** + * Author: Sven Gothel <[email protected]> + * Copyright (c) 2022 Gothel Software e.K. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package trial.org.direct_bt; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import org.direct_bt.BTMode; +import org.direct_bt.BTRole; +import org.direct_bt.BTSecurityLevel; +import org.direct_bt.AdapterStatusListener; +import org.direct_bt.BTAdapter; +import org.direct_bt.BTDevice; +import org.direct_bt.BTDeviceRegistry; +import org.direct_bt.BTException; +import org.direct_bt.BTFactory; +import org.direct_bt.BTManager; +import org.direct_bt.BTSecurityRegistry; +import org.direct_bt.BTUtils; +import org.direct_bt.DiscoveryPolicy; +import org.direct_bt.HCIStatusCode; +import org.direct_bt.PairingMode; +import org.direct_bt.SMPKeyBin; +import org.jau.net.EUI48; +import org.junit.Assert; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +/** + * Testing a full Bluetooth server and client lifecycle of operations, requiring two BT adapter: + * - start server advertising + * - start client discovery and connect to server when discovered + * - client/server processing of connection when ready + * - client disconnect + * - server stop advertising + * - security-level: NONE, ENC_ONLY freshly-paired and ENC_ONLY pre-paired + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class TestDBTClientServer10 extends BaseDBTClientServer { + static final boolean DEBUG = false; + + @Test(timeout = 20000) + public final void test00_FullCycle_EncNone() { + test8x_fullCycle("00", BTSecurityLevel.NONE, false /* serverShallHaveKeys */, BTSecurityLevel.NONE, false /* clientShallHaveKeys */); + } + + @Test(timeout = 20000) + public final void test01_FullCycle_EncOnlyNo1() { + test8x_fullCycle("01", BTSecurityLevel.ENC_ONLY, false /* serverShallHaveKeys */, BTSecurityLevel.ENC_ONLY, false /* clientShallHaveKeys */); + } + + @Test(timeout = 30000) + public final void test02_FullCycle_EncOnlyNo2() { + test8x_fullCycle("02", BTSecurityLevel.ENC_ONLY, true /* serverShallHaveKeys */, BTSecurityLevel.ENC_ONLY, true /* clientShallHaveKeys */); + } + + volatile BTDevice lastCompletedDevice = null; + volatile PairingMode lastCompletedDevicePairingMode = PairingMode.NONE; + volatile BTSecurityLevel lastCompletedDeviceSecurityLevel = BTSecurityLevel.NONE; + + final void test8x_fullCycle(final String suffix, + final BTSecurityLevel secLevelServer, final boolean serverShallHaveKeys, + final BTSecurityLevel secLevelClient, final boolean clientShallHaveKeys) + { + BTManager manager = null; + try { + manager = BTFactory.getDirectBTManager(); + } catch (BTException | NoSuchMethodException | SecurityException + | IllegalAccessException | IllegalArgumentException + | InvocationTargetException | ClassNotFoundException e) { + e.printStackTrace(); + Assert.assertNull("Unable to instantiate DirectBT BluetoothManager: "+e.getMessage(), e); + } + if( null == manager ) { + return; + } + + final List<BTAdapter> adapters = manager.getAdapters(); + BTUtils.println(System.err, "Adapter: Count "+adapters.size()+": "+adapters.toString()); + Assert.assertTrue("Adapter count not >= 2 but "+adapters.size(), adapters.size() >= 2); + + final String serverName = "TestDBTCS00-T"+suffix; + final DBTServer00 server = new DBTServer00(EUI48.ALL_DEVICE, BTMode.DUAL, true /* SC */, serverName, secLevelServer); + + final DBTClient00 client = new DBTClient00(EUI48.ALL_DEVICE, BTMode.DUAL); + BTDeviceRegistry.addToWaitForDevices( serverName ); + { + final BTSecurityRegistry.Entry sec = BTSecurityRegistry.getOrCreate(serverName); + sec.sec_level = secLevelClient; + } + client.KEEP_CONNECTED = false; // default + client.REMOVE_DEVICE = false; // default and test side-effects + client.measurementsLeft.set(1); + client.discoveryPolicy = DiscoveryPolicy.PAUSE_CONNECTED_UNTIL_DISCONNECTED; + + final BTManager.ChangedAdapterSetListener myChangedAdapterSetListener = + new BTManager.ChangedAdapterSetListener() { + @Override + public void adapterAdded(final BTAdapter adapter) { + if( null == server.getAdapter() ) { + if( server.initAdapter( adapter ) ) { + server.setAdapter(adapter); + BTUtils.println(System.err, "****** Adapter-Server ADDED__: InitOK: " + adapter); + return; + } + } + if( null == client.getAdapter() ) { + if( client.initAdapter( adapter ) ) { + client.setAdapter(adapter); + BTUtils.println(System.err, "****** Adapter-Client ADDED__: InitOK: " + adapter); + return; + } + } + BTUtils.println(System.err, "****** Adapter ADDED__: Ignored: " + adapter); + } + + @Override + public void adapterRemoved(final BTAdapter adapter) { + if( null != server.getAdapter() && adapter == server.getAdapter() ) { + server.setAdapter(null); + BTUtils.println(System.err, "****** Adapter-Server REMOVED: " + adapter); + return; + } + if( null != client.getAdapter() && adapter == client.getAdapter() ) { + client.setAdapter(null); + BTUtils.println(System.err, "****** Adapter-Client REMOVED: " + adapter); + return; + } + BTUtils.println(System.err, "****** Adapter REMOVED: Ignored " + adapter); + } + }; + + manager.addChangedAdapterSetListener(myChangedAdapterSetListener); + Assert.assertNotNull("No server adapter found", server.getAdapter()); + Assert.assertNotNull("No client adapter found", client.getAdapter()); + + lastCompletedDevice = null; + lastCompletedDevicePairingMode = PairingMode.NONE; + lastCompletedDeviceSecurityLevel = BTSecurityLevel.NONE; + final AdapterStatusListener clientAdapterStatusListener = new AdapterStatusListener() { + @Override + public void deviceReady(final BTDevice device, final long timestamp) { + lastCompletedDevice = device; + lastCompletedDevicePairingMode = device.getPairingMode(); + lastCompletedDeviceSecurityLevel = device.getConnSecurityLevel(); + BTUtils.println(System.err, "XXXXXX Client Ready: "+device); + } + }; + Assert.assertTrue( client.getAdapter().addStatusListener(clientAdapterStatusListener) ); + + // + // Server start + // + Assert.assertTrue( server.getAdapter().isInitialized() ); + Assert.assertTrue( server.getAdapter().isPowered() ); + Assert.assertEquals( BTRole.Master, server.getAdapter().getRole() ); + Assert.assertTrue( 4 <= server.getAdapter().getBTMajorVersion() ); + { + Assert.assertFalse(server.getAdapter().isAdvertising()); + Assert.assertFalse(server.getAdapter().isDiscovering()); + + Assert.assertEquals( HCIStatusCode.SUCCESS, server.startAdvertising(server.getAdapter(), "test"+suffix+"_startAdvertising") ); + Assert.assertTrue(server.getAdapter().isAdvertising()); + Assert.assertFalse(server.getAdapter().isDiscovering()); + Assert.assertEquals( BTRole.Slave, server.getAdapter().getRole() ); + Assert.assertEquals( serverName, server.getAdapter().getName() ); + } + + // + // Client start + // + Assert.assertTrue( client.getAdapter().isInitialized() ); + Assert.assertTrue( client.getAdapter().isPowered() ); + Assert.assertEquals( BTRole.Master, client.getAdapter().getRole() ); + Assert.assertTrue( 4 <= client.getAdapter().getBTMajorVersion() ); + { + Assert.assertFalse(client.getAdapter().isAdvertising()); + Assert.assertFalse(client.getAdapter().isDiscovering()); + + Assert.assertEquals( HCIStatusCode.SUCCESS, client.startDiscovery(client.getAdapter(), "test"+suffix+"_startDiscovery") ); + Assert.assertFalse(client.getAdapter().isAdvertising()); + Assert.assertTrue(client.getAdapter().isDiscovering()); + Assert.assertEquals( BTRole.Master, client.getAdapter().getRole() ); + } + + while( 1 > server.servedConnections.get() || + 1 > client.completedMeasurements.get() || + null == lastCompletedDevice ) + { + try { Thread.sleep(500); } catch (final InterruptedException e) { e.printStackTrace(); } + } + Assert.assertEquals(1, server.servedConnections.get()); + Assert.assertEquals(1, client.completedMeasurements.get()); + Assert.assertNotNull(lastCompletedDevice); + Assert.assertEquals( HCIStatusCode.SUCCESS, client.stopDiscovery(client.getAdapter(), "test"+suffix+"_stopDiscovery") ); + + { + Assert.assertFalse(server.getAdapter().isAdvertising()); // stopped by connection + Assert.assertFalse(server.getAdapter().isDiscovering()); + + // Stopping advertising wven if stopped must be OK! + Assert.assertEquals( HCIStatusCode.SUCCESS, server.stopAdvertising(server.getAdapter(), "test"+suffix+"_stopAdvertising") ); + Assert.assertFalse(server.getAdapter().isAdvertising()); + Assert.assertFalse(server.getAdapter().isDiscovering()); + Assert.assertEquals( BTRole.Slave, server.getAdapter().getRole() ); // kept + } + + final SMPKeyBin clientKeys = SMPKeyBin.read(DBTConstants.CLIENT_KEY_PATH, lastCompletedDevice, true /* verbose */); + Assert.assertTrue(clientKeys.isValid()); + final BTSecurityLevel clientKeysSecLevel = clientKeys.getSecLevel(); + Assert.assertEquals(secLevelClient, clientKeysSecLevel); + { + if( clientShallHaveKeys ) { + // Using encryption: pre-paired + Assert.assertEquals(PairingMode.PRE_PAIRED, lastCompletedDevicePairingMode); + Assert.assertEquals(BTSecurityLevel.ENC_ONLY, lastCompletedDeviceSecurityLevel); // pre-paired fixed level, no auth + } else if( BTSecurityLevel.NONE.value < secLevelClient.value ) { + // Using encryption: Newly paired + Assert.assertNotEquals(PairingMode.PRE_PAIRED, lastCompletedDevicePairingMode); + Assert.assertTrue("PairingMode client "+lastCompletedDevicePairingMode+" not > NONE", PairingMode.NONE.value < lastCompletedDevicePairingMode.value); + Assert.assertTrue("SecurityLevel client "+lastCompletedDeviceSecurityLevel+" not >= "+secLevelClient, secLevelClient.value <= lastCompletedDeviceSecurityLevel.value); + } else { + // No encryption: No pairing + Assert.assertEquals(PairingMode.NONE, lastCompletedDevicePairingMode); + Assert.assertEquals(BTSecurityLevel.NONE, lastCompletedDeviceSecurityLevel); + } + } + + final int count = manager.removeChangedAdapterSetListener(myChangedAdapterSetListener); + BTUtils.println(System.err, "****** EOL Removed ChangedAdapterSetCallback " + count); + } + + public static void main(final String args[]) { + org.junit.runner.JUnitCore.main(TestDBTClientServer10.class.getName()); + } +} diff --git a/trial/java/trial/org/direct_bt/VersionInfo.java b/trial/java/trial/org/direct_bt/VersionInfo.java new file mode 100644 index 00000000..c5b0c69e --- /dev/null +++ b/trial/java/trial/org/direct_bt/VersionInfo.java @@ -0,0 +1,14 @@ +package trial.org.direct_bt; + +import java.io.IOException; + +import org.direct_bt.BTFactory; +import org.direct_bt.DirectBTVersion; +import org.jau.util.VersionUtil; + +public class VersionInfo { + public static void main(final String args[]) throws IOException { + BTFactory.main(args); + } + +} |