├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── nl
│ │ │ └── rwslinkman
│ │ │ └── simdeviceble
│ │ │ ├── AppModel.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── bluetooth
│ │ │ ├── AdvertiseCommand.kt
│ │ │ ├── AdvertisementManager.kt
│ │ │ ├── BluetoothBytesParser.java
│ │ │ ├── BluetoothDelegate.kt
│ │ │ └── BluetoothUUID.kt
│ │ │ ├── device
│ │ │ ├── DigitalClock.kt
│ │ │ ├── EarThermometer.kt
│ │ │ ├── HeartRatePeripheral.kt
│ │ │ └── model
│ │ │ │ ├── Characteristic.kt
│ │ │ │ ├── Device.kt
│ │ │ │ └── Service.kt
│ │ │ ├── grpc
│ │ │ ├── EventListAdapter.kt
│ │ │ ├── GrpcDataModel.kt
│ │ │ ├── GrpcServerActivity.kt
│ │ │ ├── HostData.kt
│ │ │ └── server
│ │ │ │ ├── GrpcActions.kt
│ │ │ │ ├── GrpcEvents.kt
│ │ │ │ ├── GrpcServer.kt
│ │ │ │ └── SimDeviceGrpcService.kt
│ │ │ ├── service
│ │ │ ├── battery
│ │ │ │ ├── BatteryLevelCharacteristic.kt
│ │ │ │ └── BatteryService.kt
│ │ │ ├── currenttime
│ │ │ │ ├── CurrentTimeCharacteristic.kt
│ │ │ │ └── CurrentTimeService.kt
│ │ │ ├── deviceinformation
│ │ │ │ ├── DeviceInformationService.kt
│ │ │ │ ├── FirmwareRevisionCharacteristic.kt
│ │ │ │ ├── HardwareRevisionCharacteristic.kt
│ │ │ │ ├── ManufacturerNameCharacteristic.kt
│ │ │ │ ├── ModelNumberCharacteristic.kt
│ │ │ │ ├── PnpIdentifierCharacteristic.kt
│ │ │ │ ├── RegulatoryCertificationCharacteristic.kt
│ │ │ │ ├── SerialNumberCharacteristic.kt
│ │ │ │ ├── SoftwareRevisionCharacteristic.kt
│ │ │ │ └── SystemIdentifierCharacteristic.kt
│ │ │ ├── healththermometer
│ │ │ │ ├── HealthThermometerService.kt
│ │ │ │ ├── MeasurementIntervalCharacteristic.kt
│ │ │ │ └── TemperatureMeasurementCharacteristic.kt
│ │ │ └── heartrate
│ │ │ │ ├── BodySensorLocationCharacteristic.kt
│ │ │ │ ├── HeartRateControlPointCharacteristic.kt
│ │ │ │ ├── HeartRateMeasurementCharacteristic.kt
│ │ │ │ └── HeartRateService.kt
│ │ │ └── ui
│ │ │ ├── data
│ │ │ ├── CharacteristicDataViewHolder.kt
│ │ │ ├── ServiceDataAdapter.kt
│ │ │ ├── ServiceDataFragment.kt
│ │ │ ├── ServiceDataViewHolder.kt
│ │ │ └── controls
│ │ │ │ ├── CharacteristicControls.kt
│ │ │ │ ├── DecimalCharacteristicControls.kt
│ │ │ │ ├── NumberCharacteristicControls.kt
│ │ │ │ └── TextCharacteristicControls.kt
│ │ │ ├── devices
│ │ │ ├── SupportedDevicesAdapter.kt
│ │ │ └── SupportedDevicesFragment.kt
│ │ │ └── home
│ │ │ └── HomeFragment.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── ic_bluetooth_black_24dp.xml
│ │ ├── ic_devices_supported_black_24dp.xml
│ │ ├── ic_home_black_24dp.xml
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ ├── activity_grpc_server.xml
│ │ ├── activity_main.xml
│ │ ├── card_advertising.xml
│ │ ├── card_bluetooth_status.xml
│ │ ├── card_connections.xml
│ │ ├── characteristic_update_control_decimal.xml
│ │ ├── characteristic_update_control_number.xml
│ │ ├── characteristic_update_control_text.xml
│ │ ├── fragment_home.xml
│ │ ├── fragment_servicedata.xml
│ │ ├── fragment_supported_devices.xml
│ │ ├── list_item_characteristic.xml
│ │ ├── list_item_grpc_event.xml
│ │ ├── list_item_servicedata.xml
│ │ ├── list_item_supported_characteristic.xml
│ │ ├── list_item_supported_device.xml
│ │ └── spinner_item_device.xml
│ │ ├── menu
│ │ ├── bottom_nav_menu.xml
│ │ └── header_menu.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── navigation
│ │ └── mobile_navigation.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── nl
│ └── rwslinkman
│ └── simdeviceble
│ └── ExampleUnitTest.kt
├── build.gradle
├── cucumbertest
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ ├── assets
│ │ └── features
│ │ │ ├── AdvertisementTest.feature
│ │ │ └── GrpcConnectionTest.feature
│ └── java
│ │ └── nl
│ │ └── rwslinkman
│ │ └── simdeviceble
│ │ └── cucumbertest
│ │ └── test
│ │ ├── ActivityScenarioHolder.kt
│ │ ├── GrpcServerCucumberTests.kt
│ │ ├── grpc
│ │ ├── AdvertisedCharacteristic.kt
│ │ ├── SimDevice.kt
│ │ └── SimDeviceGrpcClient.kt
│ │ └── steps
│ │ ├── BluetoothSteps.kt
│ │ ├── GeneralSteps.kt
│ │ └── GrpcControlSteps.kt
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── nl
│ │ └── rwslinkman
│ │ └── simdeviceble
│ │ └── cucumbertest
│ │ ├── AndroidExt.kt
│ │ ├── AppPermissionChecker.kt
│ │ └── CucumberTestActivity.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ ├── ic_bluetooth_black_24dp.xml
│ ├── ic_gear_black_24dp.xml
│ ├── ic_launcher_background.xml
│ ├── ic_location_black_24dp.xml
│ ├── ic_success_black_24dp.xml
│ └── ic_warning_black_24dp.xml
│ ├── layout
│ └── activity_cucumber_test.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
├── docs
├── image_data_fragment.jpg
└── image_home_fragment.jpg
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── grpc
├── SimDeviceBLE.proto
├── compiler
│ ├── protoc-gen-grpc-java-1460
│ └── protoc-gen-grpc-java-1530
└── generate_java.sh
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | .idea/workspace.xml
42 | .idea/tasks.xml
43 | .idea/gradle.xml
44 | .idea/assetWizardSettings.xml
45 | .idea/dictionaries
46 | .idea/libraries
47 | # Android Studio 3 in .gitignore file.
48 | .idea/caches
49 | .idea/modules.xml
50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
51 | .idea/navEditor.xml
52 |
53 | # Keystore files
54 | # Uncomment the following lines if you do not want to check your keystore files in.
55 | #*.jks
56 | #*.keystore
57 |
58 | # External native build folder generated in Android Studio 2.2 and later
59 | .externalNativeBuild
60 | .cxx/
61 |
62 | # Google Services (e.g. APIs or Firebase)
63 | # google-services.json
64 |
65 | # Freeline
66 | freeline.py
67 | freeline/
68 | freeline_project_description.json
69 |
70 | # fastlane
71 | fastlane/report.xml
72 | fastlane/Preview.html
73 | fastlane/screenshots
74 | fastlane/test_output
75 | fastlane/readme.md
76 |
77 | # Version control
78 | vcs.xml
79 |
80 | # lint
81 | lint/intermediates/
82 | lint/generated/
83 | lint/outputs/
84 | lint/tmp/
85 | # lint/reports/
86 | .gradle
87 | /local.properties
88 | /.idea/caches
89 | /.idea/libraries
90 | /.idea/modules.xml
91 | /.idea/workspace.xml
92 | /.idea/navEditor.xml
93 | /.idea/assetWizardSettings.xml
94 | .DS_Store
95 | /build
96 | /captures
97 | .cxx
98 | .idea
99 | grpc/generated_java
100 | grpc/generated_kt
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SimDeviceBLE
2 | Simulate a BLE device using this Android app
3 |
4 | SimDeviceBLE allows developers to simulate Bluetooth devices with multiple GATT Services.
5 | This **app** allows full configuration of the BLE advertisement data and shows the amount of connected devices.
6 | The simulated device and the advertised data can be manipulated using the app UI or the **gRPC** interface.
7 |
8 | Feel free to import the `app` module into your Android Studio project to simulate your own BLE peripherals.
9 | Contributing the proprietary devices, services and characteristics is appreciated but not required.
10 |
11 | ## App
12 | Checkout the project and open it with Android Studio.
13 | Run the `app` on your Android phone.
14 |
15 | Make sure Bluetooth is enabled on your phone (use `Enable Bluetooth` button) and advertise a selected device.
16 | The selected device will advertise the services associated to it.
17 | All options available in the `Advertise` section will be used in the Advertisement data.
18 |
19 | Characteristic values can be updated using the `Service Data` screen.
20 | All characteristics will be mapped to a View on the screen to show their current data value.
21 | Some characteristics will allow to update their value and notify all connected Central devices.
22 | Developers do not need to write their own View classes.
23 | Services and characteristics are introspected and Views are created dynamically.
24 |
25 | 
26 | 
27 |
28 | The Gradle configuration uses the `protoc-gen-grpc-java` plugin for `protoc` to generate classes.
29 | Download the compiler from [here](https://repo1.maven.org/maven2/io/grpc/protoc-gen-grpc-java/) for your situation.
30 |
31 | ## gRPC
32 | SimDeviceBLE defines a [gRPC](https://grpc.io/) interface to take control of the BLE device.
33 | Generate a client [in your preferred language](https://grpc.io/docs/languages/) to interact with the gRPC server in the app.
34 | All devices supported in the app can be advertised with a simple command.
35 | Update the advertised characteristic data to match your use case or test scenario.
36 | To notify all connected devices of an updated characteristic in SimDeviceBLE, call the associated `rpc`.
37 |
38 | SimDeviceBLE has a `GrpcServerActivity` that can be used in an easy way.
39 | It is available via the options menu in the app.
40 |
41 | For usage in automation, please follow these steps:
42 | - Install the SimDeviceBLE app on an Android phone and connect to a PC using USB.
43 | - Run command `adb forward tcp:8910 tcp:8910`
44 | - Run command `adb shell am start -n nl.rwslinkman.simdeviceble/.grpc.GrpcServerActivity`
45 |
46 | ```protobuf
47 | service SimDeviceBLE {
48 |
49 | rpc listAvailableSimDevices(google.protobuf.Empty) returns (ListAvailableSimDevicesResponse) {}
50 | rpc startAdvertisement(StartAdvertisementRequest) returns (StartAdvertisementResponse) {}
51 | rpc stopAdvertisement(google.protobuf.Empty) returns (google.protobuf.Empty) {}
52 | rpc listAdvertisedCharacteristics(google.protobuf.Empty) returns (ListAdvertisedCharacteristicsResponse) {}
53 | rpc updateCharacteristicValue(UpdateCharacteristicValueRequest) returns (google.protobuf.Empty) {}
54 | rpc notifyCharacteristic(NotifyCharacteristicRequest) returns (google.protobuf.Empty) {}
55 | }
56 | ```
57 | Please refer to the `grpc/SimDeviceBLE.proto` file for the full specification of the gRPC interface.
58 |
59 | ## Automated testing
60 | The module `cucumbertest` contains a basic app that is used as a testing vehicle to verify SimDeviceBLE.
61 | Tests written using the [Cucumber framework](https://github.com/cucumber/cucumber-android) are executed via this app.
62 |
63 | Execute the `androidTest` tests in the `cucumberTest` module to test the gRPC server.
64 | It requires a separate device running SimDeviceBLE in gRPC Mode.
65 | This mode can be found in the context menu of the SimDeviceBLE app.
66 |
67 | The Cucumber tests demonstrate a way SimDeviceBLE can contribute to your automated testing.
68 | Your project could create a similar configuration using the gRPC client.
69 |
70 | ## Contributing
71 | Please feel free to add any devices in the `nl.rwslinkman.simdeviceble.device` package.
72 | All devices must implement the `Device` interface.
73 | The list of `services` can contain any of the classes implementing `Service`.
74 | They are defined in the `nl.rwslinkman.simdeviceble.service` package.
75 | Don't forget to add the new device in the `AppModel.supportedDevices` list.
76 |
77 | When adding a new `Service`, please implement according to the Bluetooth SIG specification as much as possible.
78 | Please keep in mind that the services might be used by multiple `Device` implementations.
79 |
80 | ### Notice
81 | BLE peripheral mode was introduced in Android 5.0 Lollipop.
82 | Due to hardware chipset dependency, some Android phones don't have access to this feature.
83 | This will be visible in the `Bluetooth` section on the Home screen.
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 |
4 | android {
5 | compileSdkVersion 33
6 |
7 | defaultConfig {
8 | applicationId "nl.rwslinkman.simdeviceble"
9 | minSdkVersion 21
10 | targetSdkVersion 33
11 | versionCode 1
12 | versionName "1.0"
13 | multiDexEnabled true
14 |
15 | testInstrumentationRunner "io.cucumber.android.runner.CucumberAndroidJUnitRunner"
16 | testApplicationId "nl.rwslinkman.simdeviceble.test"
17 | }
18 | sourceSets {
19 | main {
20 | java {
21 | srcDirs += "build/generated/src"
22 | }
23 | }
24 | }
25 | buildTypes {
26 | release {
27 | minifyEnabled false
28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
29 | }
30 | }
31 | compileOptions {
32 | sourceCompatibility JavaVersion.VERSION_1_8
33 | targetCompatibility JavaVersion.VERSION_1_8
34 | }
35 | kotlinOptions {
36 | jvmTarget = '1.8'
37 | }
38 | packagingOptions {
39 | resources {
40 | pickFirsts += ['META-INF/INDEX.LIST', 'META-INF/LICENSE', 'META-INF/io.netty.versions.properties', 'META-INF/services/io.grpc.ManagedChannelProvider']
41 | }
42 | }
43 | buildFeatures {
44 | viewBinding true
45 | }
46 | namespace 'nl.rwslinkman.simdeviceble'
47 | }
48 |
49 | dependencies {
50 | implementation fileTree(dir: "libs", include: ["*.jar"])
51 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
52 | implementation "androidx.core:core-ktx:1.9.0"
53 | implementation "androidx.appcompat:appcompat:1.6.1"
54 | implementation "androidx.constraintlayout:constraintlayout:2.1.4"
55 | implementation "com.google.android.material:material:1.6.0"
56 | implementation "androidx.navigation:navigation-fragment-ktx:2.5.3"
57 | implementation "androidx.navigation:navigation-ui-ktx:2.5.3"
58 | implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
59 | implementation "androidx.navigation:navigation-fragment-ktx:2.5.3"
60 | implementation "androidx.navigation:navigation-ui-ktx:2.5.3"
61 | implementation "androidx.legacy:legacy-support-v4:1.0.0"
62 | implementation "io.grpc:grpc-netty:1.46.0"
63 | implementation "io.grpc:grpc-protobuf:1.53.0"
64 | implementation "io.grpc:grpc-stub:1.53.0"
65 | implementation "com.google.protobuf:protobuf-java:3.21.12"
66 | implementation "javax.annotation:javax.annotation-api:1.3.2"
67 |
68 | testImplementation "junit:junit:4.13.2"
69 | androidTestImplementation "androidx.test.ext:junit:1.1.5"
70 | androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
71 | }
72 |
73 | task compileGrpcClasses {
74 | // TODO: This could be an actual Gradle plugin that takes into account the user's OS
75 | doLast {
76 | def proc = "grpc/generate_java.sh".execute()
77 | proc.waitForProcessOutput(System.out, System.err)
78 | }
79 | }
80 | preBuild.dependsOn compileGrpcClasses
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | -keepnames io.grpc.ServerProvider
24 | -keep io.grpc.netty.NettyServerProvider
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
19 |
20 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/java/nl/rwslinkman/simdeviceble/AppModel.kt:
--------------------------------------------------------------------------------
1 | package nl.rwslinkman.simdeviceble
2 |
3 | import androidx.lifecycle.MutableLiveData
4 | import androidx.lifecycle.ViewModel
5 | import nl.rwslinkman.simdeviceble.bluetooth.AdvertisementManager
6 | import nl.rwslinkman.simdeviceble.bluetooth.BluetoothDelegate
7 | import nl.rwslinkman.simdeviceble.device.DigitalClock
8 | import nl.rwslinkman.simdeviceble.device.EarThermometer
9 | import nl.rwslinkman.simdeviceble.device.HeartRatePeripheral
10 | import nl.rwslinkman.simdeviceble.device.model.Characteristic
11 | import nl.rwslinkman.simdeviceble.device.model.Device
12 | import java.util.*
13 |
14 | class AppModel: ViewModel(), AdvertisementManager.Listener {
15 |
16 | // Data for Bluetooth UI
17 | val bluetoothSupported: MutableLiveData = MutableLiveData(false)
18 | val bluetoothEnabled: MutableLiveData = MutableLiveData(false)
19 | val bluetoothAdvertisingSupported: MutableLiveData = MutableLiveData(false)
20 | // Data for Advertising UI
21 | val isAdvertising: MutableLiveData = MutableLiveData(false)
22 | val advertisementName: MutableLiveData = MutableLiveData("Unknown")
23 | val activeDevice: MutableLiveData = MutableLiveData(supportedDevices.first())
24 | val isConnectable: MutableLiveData = MutableLiveData(true)
25 | val advertiseDeviceName: MutableLiveData = MutableLiveData(true)
26 | // Data for Connections UI
27 | private val _connDevices: MutableSet = mutableSetOf()
28 | val connectedDevices: MutableLiveData> = MutableLiveData()
29 | // Characteristic data manipulation
30 | private val _dataContainer: MutableMap = mutableMapOf()
31 | val presentableDataContainer: MutableLiveData