From 2743ae2f2e2aadbe65ccdf8e16c5c84470bff6a8 Mon Sep 17 00:00:00 2001
From: ttschol <thomas.tschol@gmail.com>
Date: Sat, 27 Feb 2021 13:20:10 +0100
Subject: [PATCH] Added initial bleclient files, READMEs, gitlab-ci files and
 protocol for TimeFlip

---
 .gitignore                                    |   2 +
 .gitlab-ci.yml                                |  39 +++
 README.md                                     | 287 +++++++++++++++++-
 bleclient/.gitignore                          |   7 +
 bleclient/README.md                           |  30 ++
 bleclient/lib/tinyb.jar                       | Bin 0 -> 15124 bytes
 bleclient/pom.xml                             | 136 +++++++++
 .../java/at/qe/skeleton/bleclient/Main.java   |  62 ++++
 .../at/qe/skeleton/bleclient/TinybUtil.java   |  53 ++++
 .../qe/skeleton/bleclient/TinybUtilTest.java  |  69 +++++
 gitlab-ci/Dockerfile                          |  62 ++++
 gitlab-ci/Makefile                            |  14 +
 ...E_device_communication_protocol_v3.0_en.md | 157 ++++++++++
 13 files changed, 917 insertions(+), 1 deletion(-)
 create mode 100644 .gitignore
 create mode 100644 .gitlab-ci.yml
 create mode 100644 bleclient/.gitignore
 create mode 100644 bleclient/README.md
 create mode 100644 bleclient/lib/tinyb.jar
 create mode 100644 bleclient/pom.xml
 create mode 100644 bleclient/src/main/java/at/qe/skeleton/bleclient/Main.java
 create mode 100644 bleclient/src/main/java/at/qe/skeleton/bleclient/TinybUtil.java
 create mode 100644 bleclient/src/test/java/at/qe/skeleton/bleclient/TinybUtilTest.java
 create mode 100644 gitlab-ci/Dockerfile
 create mode 100644 gitlab-ci/Makefile
 create mode 100644 timeflip/BLE_device_communication_protocol_v3.0_en.md

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8e66629
--- /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 0000000..24dd27f
--- /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 4d9b637..edc15f9 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 0000000..b7b7593
--- /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 0000000..939639e
--- /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
GIT binary patch
literal 15124
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

literal 0
HcmV?d00001

diff --git a/bleclient/pom.xml b/bleclient/pom.xml
new file mode 100644
index 0000000..3e443f7
--- /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 0000000..6542133
--- /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 0000000..e8c7c6a
--- /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 0000000..1a05ee5
--- /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 0000000..c7bb1b5
--- /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 0000000..fea9a25
--- /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 0000000..010aa13
--- /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
-- 
GitLab