diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..8e66629b848a5cf2a05b5eead95d9218631420ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.idea +*.vscode diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..24dd27ff93cbaacc37203aa89493414354661fc6 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,39 @@ +image: docker.uibk.ac.at:443/csat2410/skeleton-bleclient + +stages: +- Static Analysis +- Test + +before_script: + - mvn -version + - java -version + - echo $JAVA_HOME + - echo $MAVEN_HOME + - python3 --version + - pip3 --version + - echo $CI_PROJECT_DIR + +# See: https://pmd.github.io/ +pmd: + stage: Static Analysis + allow_failure: true + script: + - cd bleclient + - /opt/pmd-bin-6.31.0/bin/run.sh pmd -d src/main/java/ -f text -R rulesets/java/quickstart.xml -cache pmd_cache.txt + + +bleclient-test: + stage: Test + script: + - mkdir bleclient-test + - cd bleclient + - mvn clean install + - JACOCO_SCORE=$(awk -F, '{ instructions += $4 + $5; covered += $5 } END { printf "%.2f\n", 100*covered/instructions }' target/site/jacoco/jacoco.csv) + - anybadge --label=jacoco-coverage --file=target/site/jacoco/jacoco.svg --value=$JACOCO_SCORE coverage + - mvn dependency:tree -DoutputFile="$CI_PROJECT_DIR/bleclient-test/mvn_dependencies.txt" + - cp -r target/site/jacoco $CI_PROJECT_DIR/bleclient-test/jacoco + - cp -r target/apidocs $CI_PROJECT_DIR/bleclient-test/apidocs + artifacts: + paths: + - bleclient-test + diff --git a/README.md b/README.md index 4d9b6375ab08d667bbe6373591ec47c6d57bf47a..edc15f9bbbce8dc44c7fd95c6bc70e489eeada6d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,287 @@ -# skeleton-bleclient +# Java und Bluetooth Low Energy auf dem Raspberry Pi +Installation von Bluetooth auf dem Raspberry Pi und Verwendung von Java, um mit einem +bluetooth-fähigen Gerät zu kommunizieren. + +## Voraussetzungen + +### Grundsätzliche Einstellungen + +- `Raspberry Pi OS Lite` auf SD-Karte von Raspberry Pi geflashed +- Anmeldung mit Benutzername `pi` und Passwort `raspberry` und Änderung des Passworts mit `passwd` +- Bluetooth and WLAN sind aktiviert (weder `hard` noch `soft`-blocked) + + sudo rfkill list all + +- Aktivierung von z.B. WiFi mit (sollte WiFi der erste Eintrag sein): + + sudo rfkill unblock 0 + +### Verwendung von WiFI + +- Setzen des WiFi-Landes mit: + + sudo raspi-config nonint do_wifi_country AT + sudo raspi-config nonint get_wifi_country + +- Siehe: [Setting up a wireless LAN via the command line](https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md) + +- Ãœberprüfung, ob man mit dem richtigen Netzwerk verbunden ist: + + iw wlan0 link + +- Ãœberprüfung, ob Internetverbindung besteht mit: + + ping google.com + +### Verbindung mit Raspberry Pi + +- SSH wurde aktiviert (Ausgabe soll 0 sein): + + sudo raspi-config nonint get_ssh + +- Aktivierung von SSH mit: + + sudo raspi-config nonint do_ssh 0 + +- Hostname des Raspberry Pi ist bekannt: + + hostname -I + +### Optional: Private/Public Key Authentifizierung + +Siehe: [Passwordless SSH access](https://www.raspberrypi.org/documentation/remote-access/ssh/passwordless.md) + +So vermeidet man beim Verbinden zum Raspberry Pi jedes Mal das Passwort einzugeben. + +## 1) Installation + +Mit Raspberry Pi verbinden: + + ssh pi@<RASPBERRY_IP_ADDRESS> + +### a) Initiale Packages + +Auf den neuesten Stand bringen: + + sudo apt update + sudo apt upgrade + +Essentielle Tools installieren: + + sudo apt install git + sudo apt install cmake + +### b) Maven und JDK + +Installation der JDK 1.8: + + sudo apt install openjdk-8-jdk + +Sicherstellen, dass tatsächlich JDK 1.8 verwendet wird: + + sudo update-alternatives --config java + +Version überprüfen: + + java -version + +Sicherstellen, dass sich in `usr/lib/jvm` nun `java-8-openjdk-armhf` befindet: + + sudo find / -name "java" + +Editieren von `bashrc`: + + sudo nano ~/.bashrc + + +Es soll die Zeile `export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-armhf/` am Ende hinzugefügt werden und gespeichert werden. + +Nun müssen wir das Terminal neu laden, um zu überprüfen, ob die Änderungen wirksam waren – notwendig für nächste Schritte: + + bash + echo $JAVA_HOME + +Maven installieren: + + sudo apt install maven + +### c) Installation von BlueZ 5.47 + +Installation der Build-Tools: + + sudo apt install libglib2.0-dev libdbus-1-dev libudev-dev libical-dev libreadline-dev + +Download des BlueZ Source-Codes (in geeignetem Directory): + + wget http://www.kernel.org/pub/linux/bluetooth/bluez-5.47.tar.xz + +Das tar Archiv extrahieren und in den Ordner gehen: + + tar -xf bluez-5.47.tar.xz && cd bluez-5.47 + +Konfigurieren des BlueZ Projekts: + + ./configure --prefix=/usr --mandir=/usr/share/man --sysconfdir=/etc --localstatedir=/var + +BlueZ builden: + + make + sudo make install + +#### BlueZ Installation überprüfen + +Der BlueZ Start-up Service soll nun auf das neu gebuildete BlueZ zeigen: + + cat /lib/systemd/system/bluetooth.service + +Dort sollte man die Zeile `ExecStart=/usr/libexec/bluetooth/bluetoothd` vorfinden. + +Ausgabe der BlueZ Version – sollte nun `5.47` sein: + + /usr/libexec/bluetooth/bluetoothd --version + +#### Erlaubnis für BlueZ hinzufügen + +Damit BlueZ Zugriff auf die Bluetooth-Gruppe hat, müssen wir eine eigene Erlaubnis hinzufügen. Dafür +editiert man die BlueZ DBus Konfiguration: + + sudo nano /etc/dbus-1/system.d/bluetooth.conf + +Anschließend fügt man nur die Policy für die Gruppe `bluetooth` hinzu: + +```shell +<busconfig> + <policy user="root"> + ... + </policy> + <policy group="bluetooth"> + <allow send_destination="org.bluez"/> + </policy> + ... +</busconfig> +``` + +#### Zusätzliche Konfiguration und neu starten + +OpenHab User zur Bluetooth-Gruppe hinzufügen (wir benötigten zwar OpenHab nicht, aber zur Vollständigkeit): + + sudo adduser --system --no-create-home --group --disabled-login openhab + sudo usermod -a -G bluetooth openhab + +Service Definitionen neu laden: + + sudo systemctl daemon-reload + +BlueZ neu starten: + + sudo systemctl restart bluetooth + +Ãœberprüfen, ob Bluetooth-Service aktiv ist, die richtige Version läuft (`5.47`) und es keine Fehler gibt: + + sudo systemctl status bluetooth + +### d) Installation von tinyb + +Installation der Abhängigkeiten von `tinyb`: + + sudo apt install graphviz + sudo apt install doxygen + +Klonen von tinyb (an geeigneter Stelle) und in den Ordner gehen: + + git clone https://github.com/intel-iot-devkit/tinyb.git && cd tinyb + +Ordner `build` erstellen und hineingehen: + + mkdir build + cd build + +Builden von `tinyb` mit `cmake` (`-E` steht für experimental und stellt sicher, dass `JAVA_HOME` verwendet wird, `cmake ..` generiert das Makefile im aktuellen Verzeichnis basierend auf `CMakeLists.txt` im parent-Verzeichnis, der Prefix `/usr/` stellt sicher, dass sich die native Libraries `libjavatinyb.so` und `libtinyb.so` im Java Library Path befinden): + + sudo -E cmake -DBUILDJAVA=ON -DCMAKE_INSTALL_PREFIX=/usr .. + +Ausführen von `make` und `make install`: + + sudo make + sudo make install + +## 2) Ausführen + +Die Ausführung ist eher umständlich, da wir `tinyb.jar` nicht nur zur Compile-Zeit, sondern auch dynamisch zur Laufzeit laden müssen. Für diese Bluetooth-Library ist dies leider notwendig. Siehe auch: [Java* for Bluetooth® Low Energy Applications](https://web.archive.org/web/20190414051809/https:/software.intel.com/en-us/java-for-bluetooth-le-apps) + +Sollten alle Befehle erfolgreich sein, dann können wir das Beispiel-Maven-Programm `bleclient` ausführen. +Dafür gehen wir in das Verzeichnis `bleclient` und führen das Programm basierend auf dem dort liegendem [README.md](bleclient/README.md) aus. + +## 3) Optional: Installation z.B. auf Ubuntu 18.04/20.04 + +Führen Sie die Schritte `1a` und `1b` aus: + +- Für `1b` ändert sich, dass wir statt `java-8-openjdk-armhf` die `java-8-openjdk-armhf` verwenden + +Führen Sie Schritt `1c` aus: + +- Sollte es bei `make` zum Fehler `error: ‘SIOCGSTAMP’ undeclared (first use in this function)` kommen, dann includen Sie `#include <linux/sockios.h>` in den nötigen Dateien z.B. `bluez-5.47/tools/rctest.c` und `bluez-5.47/tools/l2test.c` +- Dieses Problem scheint ab Ubuntu 20.04 aufzutreten. In Ubuntu 18.04 kann es sein, dass dieses Problem nicht auftritt. + +Führen Sie die restlichen Schritte aus. Fertig. + +Hinweis: Passen Sie beim Ausführen von Updaten/Upgrades auf. Es kann sein, dass dann Bluetooth upgegradet wird. Dies wollen wir vermeiden. Wir wollen maximal Version `5.47` verwenden. + +## Fragen und Antworten + +### Was mache ich, wenn irgendetwas in der Installation schief läuft? + +- Sicherstellen, dass alle Befehle richtig ausgeführt wurden. +- Generell kann es helfen, noch einmal alle Befehle von vorne auszuführen. + +### Was wenn der der Java BluetoothManager eine Exception wirf (z.B. NullPointerException)? + +- Sicherstellen, dass das gebuildete `tinyb.jar`zur Verfügung steht +- `tinyb.jar` muss zur Laufzeit als Parameter übergeben werden z.B. `–cp target/<JAR_FILE>:./lib/tinyb.jar:./target/dependencies/*` + +### Was wenn ich beim Ausführen ein Problem mit der nativen API Version bekomme? + +- Wahrscheinlich wurde die `tinyb.jar` nicht korrekt zur Laufzeit geladen +- Sicherstellen, dass sie richtig geladen wird z.B. `–cp target/<JAR_FILE>:./lib/tinyb.jar:./target/dependencies/*` + +### Wieso kann ich das Programm nicht mit -jar ausführen? + +- Entweder man verwendet `-cp` für Classpath oder `-jar`, aber nicht beides +- Für `-jar` benötigt man weiters auch noch eine Manifest-Datei (diese müsste man in Bezug auf das `tinyb.jar` auch konfigurieren) + +### Wieso können die native Libraries nicht gefunden werden? + +- Sollte grundsätzlich kein Problem sein, wenn man der Anleitung gefolgt hat +- Durch `-DCMAKE_INSTALL_PREFIX=/usr` sollten diese korrekt gesetzt sein +- Siehe auch bei Installation `tinyb/build/install_manifest.txt` +- Mit `java -cp` korrekt ausführen und nicht mit `-jar` +- Wenn es immer noch nicht möglich ist, dann muss man wirklich sicherstellen, dass sich `libjavatinyb.so` und `libtinyb.so` tatsächlich im Java Library Path befinden. + - Prinzipiell könnte man diese Dateien `tinyb/build/java/jni/libjavatinyb.so` und `tinyb/build/src/libtinby.so` auch direkt nach `/usr/lib` kopieren + +### Wieso bekomme ich eine BluetoothException mit Timeout was reached? + +- Sicherstellen, dass sich der TimeFlip tatsächlich in Reichweite befindet +- Sicherstellen, dass die Batterie richtig im TimeFlip ist + +### Wieso bekomme ich Exceptions beim Auslesen von Charakteristiken? + +- Sicherstellen, dass das Passwort richtig vorher an den TimeFlip geschrieben wurde, dann sollte man alle Charakteristiken auslesen können +- Es kann sein, dass man versehentlich das Passwort über die TimeFlip App gesetzt hat. Wenn man die Batterie kurz herausnimmt und noch einmal einsetzt, dann sollte das Passwort zurückgesetzt sein. + +### Was mache ich, wenn das bleclient Programm keinen TimeFlip ausgibt? + +- Sicherstellen, dass der TimeFlip eingeschalten ist (z.B. Ãœberprüfung mit Handy-App wie `nRF Connect`) + - Am besten notiert man sich die UUID des TimeFlips +- Sicherstellen, dass der TimeFlip mit keinem anderen Gerät gekoppelt ist + - Entkoppeln von Bluetooth, TimeFlip App, etc. +- Theoretisch ist es möglich, dass aus irgendeinem Grund das Passwort auf dem TimeFlip z.B. mit der TimeFlip App gesetzt wurde. In diesem Fall sollte man die Batterie kurz entfernen und wieder reinstecken + + +## Links +* [Raspberry Pi Bluetooth Manager TinyB - Building bluez 5.47 from sources](https://github.com/sputnikdev/bluetooth-manager-tinyb) +* [TinyB Bluetooth LE Library](https://github.com/intel-iot-devkit/tinyb) +* [Raspberry Pi Installation of TinyB (Note: do not install bluez)](http://www.martinnaughton.com/2017/07/install-intel-tinyb-java-bluetooth.html) +* [Java for Bluetooth LE applications](https://www.codeproject.com/Articles/1086361/Java-for-Bluetooth-LE-applications) +* [TinyB Java examples (HelloTinyB.java, etc.)](https://github.com/intel-iot-devkit/tinyb/tree/master/examples/java) +* [Non-interactive raspi-config interface](https://github.com/raspberrypi-ui/rc_gui/blob/master/src/rc_gui.c#L23-L70) diff --git a/bleclient/.gitignore b/bleclient/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..b7b7593721ce96564d7fab23d2135781cdeba1bb --- /dev/null +++ b/bleclient/.gitignore @@ -0,0 +1,7 @@ +/target/ +.idea/ +.settings/ +.classpath +.project +*.iml + diff --git a/bleclient/README.md b/bleclient/README.md new file mode 100644 index 0000000000000000000000000000000000000000..939639e283b10c38529973b1b13d6bf2cf6c914e --- /dev/null +++ b/bleclient/README.md @@ -0,0 +1,30 @@ +# Bluetooth Low Energy auf dem Raspberry Pi + +### Voraussetzungen +* Die Installation von `tinyb` war erfolgreich +* Ein bluetooth-fähiges Gerät befindet sich in der Nähe +* Im Ordner `lib` befindet sich die gebuildete Datei `tinyb.jar` von der Installation (`tinyb/build/java/tinyb.jar`) + +## Builden + +Zuerst müssen wir das JAR builden, damit wir `bleclient.jar` im `target`-Verzeichnis bekommen. +Zusätzlich werden alle Dependencies im `pom.xml` in `target/dependencies` abgelegt. Wir erreichen dies durch die Plugins +`maven-install-plugin` und `maven-dependency-plugin`. + +### Builden + + mvn clean package -Dmaven.test.skip=true -Dmaven.javadoc.skip=true + +### Builden mit Tests und Javadoc + + mvn clean install + +## Ausführen + +Ausführung des Programms `bleclient.jar` mit `tinyb.jar` und den Dependencies in `target/dependencies/*`. +Auch Angabe des `fully-qualified name` der auszuführenden Java-Klasse: + + sudo java -cp target/bleclient.jar:./lib/tinyb.jar:./target/dependencies/* at.qe.skeleton.bleclient.Main + + + diff --git a/bleclient/lib/tinyb.jar b/bleclient/lib/tinyb.jar new file mode 100644 index 0000000000000000000000000000000000000000..66023b9a9c69d380b4c579eb921f37328461e48b Binary files /dev/null and b/bleclient/lib/tinyb.jar differ diff --git a/bleclient/pom.xml b/bleclient/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e443f7edf73f98f205279f884545dc89f4ab05b --- /dev/null +++ b/bleclient/pom.xml @@ -0,0 +1,136 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>at.qe.skeleton</groupId> + <artifactId>bleclient</artifactId> + <version>1.0.0</version> + <packaging>jar</packaging> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <maven.compiler.source>8</maven.compiler.source> + <maven.compiler.target>8</maven.compiler.target> + </properties> + + <dependencies> + <!-- TinyB --> + <dependency> + <groupId>org.sputnikdev</groupId> + <artifactId>bluetooth-manager-tinyb</artifactId> + <version>1.3.3</version> + </dependency> + <!-- Tests --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.13</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>3.7.7</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <!-- Remove version from JAR file to keep consistent to keep execution consistent even when version changes --> + <finalName>${project.artifactId}</finalName> + <plugins> + <!-- Install TinyB library into local maven repository --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-install-plugin</artifactId> + <version>2.4</version> + <executions> + <execution> + <id>install-tinyb</id> + <phase>package</phase> + <goals> + <goal>install-file</goal> + </goals> + <configuration> + <file>lib/tinyb.jar</file> + <groupId>intel-iot-devkit</groupId> + <artifactId>tinyb</artifactId> + <version>0.6.0</version> + <packaging>jar</packaging> + </configuration> + </execution> + </executions> + </plugin> + <!-- Copy all dependencies into separate directory --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-dependency-plugin</artifactId> + <version>2.1</version> + <executions> + <execution> + <id>copy-dependencies</id> + <phase>package</phase> + <goals> + <goal>copy-dependencies</goal> + </goals> + <configuration> + <outputDirectory>${project.build.directory}/dependencies</outputDirectory> + <overWriteReleases>false</overWriteReleases> + <overWriteSnapshots>false</overWriteSnapshots> + <overWriteIfNewer>true</overWriteIfNewer> + </configuration> + </execution> + </executions> + </plugin> + <!-- Enable jacoco analysis --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>2.22.2</version> + <configuration> + <forkMode>once</forkMode> + <argLine> + @{coverageAgent} + </argLine> + </configuration> + </plugin> + <!-- Test and generate coverage with Jacoco --> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <version>0.8.6</version> + <configuration> + <propertyName>coverageAgent</propertyName> + </configuration> + <executions> + <execution> + <goals> + <goal>prepare-agent</goal> + </goals> + </execution> + <execution> + <id>report</id> + <phase>prepare-package</phase> + <goals> + <goal>report</goal> + </goals> + </execution> + </executions> + </plugin> + <!-- Generate javadoc --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <version>3.2.0</version> + <executions> + <execution> + <id>attach-javadocs</id> + <goals> + <goal>jar</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> \ No newline at end of file diff --git a/bleclient/src/main/java/at/qe/skeleton/bleclient/Main.java b/bleclient/src/main/java/at/qe/skeleton/bleclient/Main.java new file mode 100644 index 0000000000000000000000000000000000000000..6542133036cf46064bb19065020cf764816b52fe --- /dev/null +++ b/bleclient/src/main/java/at/qe/skeleton/bleclient/Main.java @@ -0,0 +1,62 @@ +package at.qe.skeleton.bleclient; + +import tinyb.BluetoothDevice; +import tinyb.BluetoothException; +import tinyb.BluetoothManager; + +import java.util.List; + +// TODO: use logging instead of System.out/System.err + +/** + * Entry point for program to search for Bluetooth devices and communicate with them + */ +public final class Main { + + private Main() { + } + + /** + * This program should connect to TimeFlip devices and read the facet characteristic exposed by the devices + * over Bluetooth Low Energy. + * + * @param args the program arguments + * @see <a href="https://github.com/DI-GROUP/TimeFlip.Docs/blob/master/Hardware/BLE_device_commutication_protocol_v3.0_en.md" target="_top">BLE device communication protocol v3.0</a> + */ + public static void main(String[] args) { + BluetoothManager manager = BluetoothManager.getBluetoothManager(); + + final String findDeviceName = "TimeFlip"; + + final boolean discoveryStarted = manager.startDiscovery(); + System.out.println("The discovery started: " + (discoveryStarted ? "true" : "false")); + try { + manager.stopDiscovery(); + } catch (BluetoothException e) { + System.err.println("Discovery could not be stopped."); + } + + System.out.println("All found devices:"); + manager.getDevices().forEach(d -> System.out.println(d.getAddress() + " - " + d.getName() + " (" + d.getRSSI() + ")")); + + List<BluetoothDevice> filteredDevices = TinybUtil.getFilteredDevices(manager, findDeviceName); + if (filteredDevices.isEmpty()) { + System.err.println("No " + findDeviceName + " devices found during discovery."); + System.exit(-1); + } + + System.out.println("Found " + filteredDevices.size() + " " + findDeviceName + " device(s)."); + for (BluetoothDevice device : filteredDevices) { + System.out.println("Found " + findDeviceName + " device with address " + device.getAddress() + " and RSSI " + + device.getRSSI()); + + if (device.connect()) { + System.out.println("Connection established"); + // TODO: read from device + device.disconnect(); + } else { + System.out.println("Connection not established - trying next one"); + } + } + } +} diff --git a/bleclient/src/main/java/at/qe/skeleton/bleclient/TinybUtil.java b/bleclient/src/main/java/at/qe/skeleton/bleclient/TinybUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..e8c7c6a327dab3b081824a8f7c12a93b438039eb --- /dev/null +++ b/bleclient/src/main/java/at/qe/skeleton/bleclient/TinybUtil.java @@ -0,0 +1,53 @@ +package at.qe.skeleton.bleclient; + +import com.google.common.base.Preconditions; +import tinyb.BluetoothDevice; +import tinyb.BluetoothManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +// TODO: use logging instead of System.out + +/** + * An utility class for bluetooth low energy devices + */ +public final class TinybUtil { + + public static final int MIN_RSSI_ALLOWED = -80; + + private TinybUtil() { + } + + /** + * Filter Bluetooth devices based on searchDevice + * <p> + * If the signal strength is too low then we should not connect to the device. The communication may + * be unstable. + * + * @param manager the Bluetooth manager from which we get the Bluetooth devices + * @param searchDevice the devices we want to search for + * @return filtered bluetooth devices + */ + public static List<BluetoothDevice> getFilteredDevices(final BluetoothManager manager, final String searchDevice) { + Preconditions.checkNotNull(manager, "Precondition violation - argument 'manager' must not be NULL!"); + Preconditions.checkNotNull(searchDevice, "Precondition violation - argument 'searchDevice' must not be NULL!"); + + List<BluetoothDevice> devices = new ArrayList<>(); + for (BluetoothDevice device : manager.getDevices()) { + if (device.getName().toLowerCase(Locale.ROOT).contains(searchDevice)) { + final int rssi = device.getRSSI(); + if (rssi == 0) { + System.out.println(searchDevice + " with address " + device.getAddress() + " has no signal."); + } else if (rssi < MIN_RSSI_ALLOWED) { + System.out.println(searchDevice + " with address" + device.getAddress() + " has a very low signal (" + + rssi + ")"); + } else { + devices.add(device); + } + } + } + return devices; + } +} diff --git a/bleclient/src/test/java/at/qe/skeleton/bleclient/TinybUtilTest.java b/bleclient/src/test/java/at/qe/skeleton/bleclient/TinybUtilTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1a05ee5bf90f290afa370ce73786e15e301c278c --- /dev/null +++ b/bleclient/src/test/java/at/qe/skeleton/bleclient/TinybUtilTest.java @@ -0,0 +1,69 @@ +package at.qe.skeleton.bleclient; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import tinyb.BluetoothDevice; +import tinyb.BluetoothManager; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.Mockito.when; + +public class TinybUtilTest { + + @Test + public void testGetFilteredDevices() { + BluetoothManager bluetoothManager = Mockito.mock(BluetoothManager.class); + + List<BluetoothDevice> mockDevices = utilMockDevices(); + when(bluetoothManager.getDevices()).thenReturn(mockDevices); + + List<BluetoothDevice> devices = TinybUtil.getFilteredDevices(bluetoothManager, "timeflip"); + Assert.assertNotNull(devices); + Assert.assertEquals(2, devices.size()); + + for (BluetoothDevice device : devices) { + if (device.getName().equals("timeflip")) { + Assert.assertEquals("A8:A8:9F:B9:28:AD", device.getAddress()); + Assert.assertEquals(-20, device.getRSSI()); + } else if (device.getName().equals("TimeFlip2")) { + Assert.assertEquals("B8:A8:9F:B9:28:AD", device.getAddress()); + Assert.assertEquals(-44, device.getRSSI()); + } else { + Assert.fail("Unexpected device " + device.getName()); + } + } + } + + private List<BluetoothDevice> utilMockDevices() { + BluetoothDevice mockTimeFlip1 = Mockito.mock(BluetoothDevice.class); + when(mockTimeFlip1.getName()).thenReturn("timeflip"); + when(mockTimeFlip1.getAddress()).thenReturn("A8:A8:9F:B9:28:AD"); + when(mockTimeFlip1.getRSSI()).thenReturn((short) -20); + + BluetoothDevice mockTimeFlip2 = Mockito.mock(BluetoothDevice.class); + when(mockTimeFlip2.getName()).thenReturn("TimeFlip2"); + when(mockTimeFlip2.getAddress()).thenReturn("B8:A8:9F:B9:28:AD"); + when(mockTimeFlip2.getRSSI()).thenReturn((short) -44); + + BluetoothDevice mockTimeFlip3 = Mockito.mock(BluetoothDevice.class); + when(mockTimeFlip3.getName()).thenReturn("timeflip3"); + when(mockTimeFlip3.getAddress()).thenReturn("C8:A8:9F:B9:28:AD"); + when(mockTimeFlip3.getRSSI()).thenReturn((short) -91); + + BluetoothDevice mockHeadphones = Mockito.mock(BluetoothDevice.class); + when(mockHeadphones.getName()).thenReturn("headphones"); + when(mockHeadphones.getAddress()).thenReturn("D8:A8:9F:B9:28:AD"); + when(mockHeadphones.getRSSI()).thenReturn((short) -35); + + List<BluetoothDevice> mockDevices = new ArrayList<>(); + mockDevices.add(mockTimeFlip1); + mockDevices.add(mockTimeFlip2); + mockDevices.add(mockTimeFlip3); + mockDevices.add(mockHeadphones); + + return mockDevices; + } +} diff --git a/gitlab-ci/Dockerfile b/gitlab-ci/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..c7bb1b5ea7318b04ffba02c09b1a34b6debace5a --- /dev/null +++ b/gitlab-ci/Dockerfile @@ -0,0 +1,62 @@ +FROM debian:buster-slim + +ENV TZ=UTC +ENV DEBIAN_FRONTEND=noninteractive + +ARG MAVEN_VERSION=3.6.1 +ARG JDK_VERSION=8 +ARG PMD_VERSION=6.31.0 + +WORKDIR /opt + +# Install basic dependencies and utility packages +RUN \ + apt-get update && apt-get -y --no-install-recommends install \ + apt-utils \ + ca-certificates \ + apt-transport-https \ + git \ + zip \ + unzip \ + curl \ + make \ + wget + +# Install Python 3 (not essential, but quite useful) +RUN \ + apt-get update && apt-get -y --no-install-recommends install \ + python3-software-properties \ + python3-pip + +# Install Python package(s) +RUN pip3 install anybadge + +# Install OpenJDK (See: https://adoptopenjdk.net/) +RUN \ + wget -O jdk-${JDK_VERSION}.tar.gz https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u282-b08/OpenJDK8U-jdk_x64_linux_hotspot_8u282b08.tar.gz \ + && mkdir jdk-${JDK_VERSION} && tar zxvf jdk-${JDK_VERSION}.tar.gz -C jdk-${JDK_VERSION} --strip-components 1 \ + && mv jdk-${JDK_VERSION}/ /usr/local/ \ + && rm jdk-${JDK_VERSION}.tar.gz + +# Install maven +RUN \ + wget --no-verbose -O /tmp/apache-maven-${MAVEN_VERSION}.tar.gz http://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz \ + && tar xzf /tmp/apache-maven-${MAVEN_VERSION}.tar.gz -C /opt/ \ + && ln -s /opt/apache-maven-${MAVEN_VERSION} /opt/maven \ + && ln -s /opt/maven/bin/mvn /usr/local/bin \ + && rm -f /tmp/apache-maven-${MAVEN_VERSION}.tar.gz + +# Set environment variables +ENV MAVEN_HOME /opt/maven +ENV JAVA_HOME=/usr/local/jdk-${JDK_VERSION} +ENV PATH=$PATH:$JAVA_HOME/bin + +# Install Code Quality tool(s) +RUN \ + wget https://github.com/pmd/pmd/releases/download/pmd_releases%2F${PMD_VERSION}/pmd-bin-${PMD_VERSION}.zip && \ + unzip pmd-bin-${PMD_VERSION}.zip && rm pmd-bin-${PMD_VERSION}.zip + +RUN apt-get clean + + + diff --git a/gitlab-ci/Makefile b/gitlab-ci/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..fea9a25a702f8660f4ede40eb71b48e9673f6040 --- /dev/null +++ b/gitlab-ci/Makefile @@ -0,0 +1,14 @@ +TARGET_REPOSITORY=csat2410/skeleton-bleclient + +# Login and build docker image +docker-build: + docker login docker.uibk.ac.at:443 + docker build -t docker.uibk.ac.at:443/${TARGET_REPOSITORY} . + +# Upload docker images to Gitlab registry +docker-push: + docker push docker.uibk.ac.at:443/${TARGET_REPOSITORY} + +# Run locally +docker-run: + docker run -it docker.uibk.ac.at:443/${TARGET_REPOSITORY} bash diff --git a/timeflip/BLE_device_communication_protocol_v3.0_en.md b/timeflip/BLE_device_communication_protocol_v3.0_en.md new file mode 100644 index 0000000000000000000000000000000000000000..010aa13c1a4327af8326d4bf756cf570f8f67549 --- /dev/null +++ b/timeflip/BLE_device_communication_protocol_v3.0_en.md @@ -0,0 +1,157 @@ + +# DATA TRANSFER PROTOCOL + +From: https://github.com/DI-GROUP/TimeFlip.Docs/blob/master/Hardware/BLE_device_commutication_protocol_v3.0_en.md + +All values are stored in TimeFlip on-board RAM memory and are reset to default when the battery is taken out or replaced. + +TimeFlip device uses Bluetooth Low Energy (BLE) protocol. Services and specifications are listed in the table: + +| Service name / UUID | Description | +|:----------------------------|:-------------------------------------| +|Device Information / 0x180A | Contains device specific info | +|Battery Service / 0x180F | Battery charge | +|TimeFlip / F1196F50-71A4-11E6-BDF4-0800200C9A66 | Time and facets | + +#### Device Information Service / 0x180A +| Characteristic's name | Size, bytes | Properties, R/W/N | +| :----------------- |:------------:|:---------------:| +| Firmware Revision String / 0x2A26| 6 | R | +R – reading W – writing N – notification + +#### Battery Service / 0x180F +| Characteristic's name | Size, bytes | Properties, R/W/N | +| :----------------- |:------------:|:---------------:| +| Battery Level / 0x2A19| 1 | R,N | +R – reading W – writing N – notification + +#### TimeFlip Service / F1196F50-71A4-11E6-BDF4-0800200C9A66 +| Characteristic's name | Size, bytes | Properties, R/W/N | +| :----------------- |:------------:|:---------------:| +| Accelerometer data / F1196F51-71A4-11E6-BDF4-0800200C9A66 | 6 | R | +| Facets / F1196F52-71A4-11E6-BDF4-0800200C9A66 | 1 | R, N | +| Command result output / F1196F53-71A4-11E6-BDF4-0800200C9A66 | 21 | R | +| Command / F1196F54-71A4-11E6-BDF4-0800200C9A66 | 21 | R, W | +| Double tap definition / F1196F55-71A4-11E6-BDF4-0800200C9A66 | 1 | N | +| Calibration version / F1196F56-71A4-11E6-BDF4-0800200C9A66 | 4 | R, W | +| Password / F1196F57-71A4-11E6-BDF4-0800200C9A66 | 6 | W | +R – reading W – writing N – notification + +### Firmware Revision String / 0x2A26 +Contains stock firmware version. +0x544676332E31 = “TFv3.1†+ +### Battery Level / 0x2A19 +Battery charge + +### Accelerometer values characteristic / F1196F51-71A4-11E6-BDF4-0800200C9A66 +_big-endian_ 0xXXYYZZ - (x,y,z) acceleration vector. + +### Facets characteristic / F1196F52-71A4-11E6-BDF4-0800200C9A66 +ID value of notified facet (0..47) + +### Command result output characteristic / F1196F53-71A4-11E6-BDF4-0800200C9A66 +Output of command result, for example history read request "0x01" returns result in "Command result output characteristic" + +History is read out in packages of 21 bytes. +History block contains 3 bytes. Example: + +| Byte number | 0 | | | | | | | | 1 | | | | | | | | 2 | | | | | | | | +|:-----------:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:| +| Bit number | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | +| Time | X | X | X | X | X | X | X | X | X | X | X | X | X | X | X | X | X | X | | | | | | | +| Facet | | | | | | | | | | | | | | | | | | | X | X | X | X | X | X | + +Maximum amount of time stored in history – 262144 seconds or ~ 3,03 days. New interval created on exceed. +Maximum facets number – 48 +One package contains 7 history blocks. + +#### History read-out protocol +After history read-out request is sent, the first package of 7 history blocks will be written in the characteristics. History blocks will be updated along with history reading-out until the very last one. Example: + +| Byte number | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | +|--------------|---|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:| +| History block | 0 | 0 | 0 | 1 | 1 | 1 | 2 | 2 | 2 | 3 | 3 | 3 | 4 | 4 | 4 | 5 | 5 | 5 | 6 | 6 | 6 | + +If the last history package is not full, missing values will be filled with zeros. +Penultimate package will contain the information on the number of sent history packages. Example: + +| Byte number | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | +|:--------------:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:| +| History block count | X | X | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + +The last package contains zeros, it thus communicates the end of history data transmission. + +| Byte number | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | +|:--------------:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:| +| History block | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + +### Command characteristic / F1196F54-71A4-11E6-BDF4-0800200C9A66 + +Executed command is shown in characteristics as 0xXX 0x02 where 0хХХ – is command message. +Unexecuted command is shown in characteristics as 0xXX 0x01 where 0хХХ – is command message. + +|Type | Access | Size, bytes | Description | +|---------|--------|----|----------| +| Command | Write | 21 | 0xXXYY..YY, where XX - command, YY - data. response is in “**command result output**†| +| | Read | 2 | 0xXXYY, where XX - command YY - error code (2 - OK, 1 - ERROR)| + +#### Commands: + +0x01 – history read out request +0x02 – delete history + +0x03 – calibration reset. +Resets values for TimeFlip facets and resets characteristics Calibration version (UUID: F1196F56-71A4-11E6-BDF4-0800200C9A66) to zero. + +0x04 0x01 – lock function* on +0x04 0x02 – lock function off + +0x05 0xXX 0xXX – auto-pause** (off by default). +0xXX 0xXX – timer value in minutes after which the auto-pause is activated (0 = auto-pause off) +Auto-pause timer is automatically reset when TimeFlip is flipped to another facet or on successful password write. + +0x06 0x01 - pause*** function on +0x06 0x02 – pause function off + +0x10 – status request (response in “command result outup†shown as 0xXXYYZZZZ) +0xXX – lock function (0x01 – on, 0x02 – off) +0xYY - pause (0x01 – on, 0x02 – off) +0xZZ 0xZZ – auto-pause timer value (in minutes) + +0x15 0xXX 0xZZ … 0xZZ - write name +0xXX – number of symbols in name +0xZZ … 0xZZ - name (19 symbols MAX. ASCII coding) + +0Ñ…30 0xZZ … 0xZZ – set new password +0xZZ … 0xZZ – password set, length is 6 symbols + +0x50 0xAA – Delete current firmware and reboot to firmware loader. + +\* lock function – locks TimeFlip to count time on current active facet and blocks the device from switching facets when TimeFlip is turned or flipped. + +\*\* Autopause function – automatically sets time count on pause after pre-set period of time (timer value). + +\*\*\* Pause – time count is set on pause, but the facets continue to be notified (user can turn/flip TimeFlip and assign new tasks to facets). This appear in history as facet with ID 63 (0b111111). + +### Double tap characteristic / F1196F55-71A4-11E6-BDF4-0800200C9A66 +Reserved for future use + +### Calibration version characteristic / F1196F56-71A4-11E6-BDF4-0800200C9A66 + +| Type | Access | Size, byte | Description | +|-----|----------------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| +| Command | Write, Read | 4 | Saves the value recorded between connections. The value is reset to â€0†when the battery is pulled out or “reset calibration†command is executed (0x03)â€. | + +This characteristics is used to check whether TimeFlip facets calibration corresponds to facets calibration in the mobile app. The check is performed by comparing value read out from this characteristics and the one stored in the mobile app. When the facets are first time assigned in TimeFlip, an arbitrary number is written to present characteristics and in the same time in the mobile app. + +### Password characteristic / F1196F57-71A4-11E6-BDF4-0800200C9A66 + +| Type | Access | Size, byte | Description | +|--------|--------|--------------|---| +| Password | Write | 6 | Requires password to be written in it to allow TimeFlip operation. | + +If the password is not provided, or provided incorrect, TimeFlip service's characteristics will return nothing on reading. Note that: +- TimeFlip requires password input after re-connect to authorize connected device +- password is reset to default every time the battery is taken out or replaced +- default password is ASCII "000000" or {0x30, 0x30, 0x30, 0x30, 0x30, 0x30} \ No newline at end of file