Apple AirTag RSSI Wert erfassen

OP #7579651
Lesenswert?

Zunächst möchte ich mich dafür entschuldigen, dass diese Anfrage ein wenig off-Topic ist, aber ich gerife mittleweile nach jedem Strohhalm. In anderen diversen (Programmierer)Foren kam bisher nichts brauchbares heraus. Offenbar auch weil viele Programmierer nichts von Hochfequenz und Protokollen verstehen. Auch ChatGPT hat nichts funktionierendes hervorgebracht.

Nun zu meinem eigentlichen Anliegen : Ich entwickle derzeit für mich (und später auch für andere) eine Android-App mit Android-Studio und Kotlin, die sportliche Leistungen aufzeichnet. Diese App läuft bisher excellent ist aber noch nicht vollständig. Ich muss es noch schaffen, zu erkennen, ob sich ein Apple AirTag in meiner Nähe befindet. Leider muss es ein AirTag sein, weil nur das zuverlässig wasserfest und klein und leicht genug ist.

Ich habe mittlerweile mehr als 40 Stunden mit Programmierexperimenten und Internetrecherchen verbracht und bin kurz vor der Aufgabe des Projets. Hiiiiiiiillllllffffffeeeeee !!!!

Ich habe bisher mit der "altbeacon Android Beacon Libary" getestet. Im Referenz Demoprogramm sehe ich den AirTag, allerdings ohne RSSI-Wert. Das Demoprogramm ist dermassen aufgebläht, dass es trotz einiger Kommentierungen nur schwer nachzuvollziehen ist. Ausserdem bin ich Kotlin Anfänger, kenne mich aber gut in anderen Programmiersprachen, Bluetooth und Elektronik aus.

Gibt es nicht einen einfacheren Weg den RSSI-Wert eines empfangenen Bluetooth-Pakets zu ermitteln. Mein Wunsch wäre es, ca. jede Sekunde nachzuschauen ob sich das AirTag im Bereich von ca. 3 Metern befindet. Also sowas wie empfangene Bluetooth Pakete nach der der AirTag UUID durchsuchen und den dazugehörigen RSSI-Wert übergeben.

Im Voraus Danke für Eure Antworten und nochmals sorry für offtopic. Auch Vorschläge für andere kompetente Foren sind willommen.

Gruß Armin

Ich weis noch auf welcher Seite man den Lötkolben anfasst :-)

(Firma: Q3) #7579733
Lesenswert?

Ich kann auch nur vermuten:

Die funktechnische "Anwesenheit" (und damit der RSSI-Wert) ist doch nicht das Gleiche, wie eine Kommunikation (OSI lässt grüßen). Das Eine setzt natürlich das Andere voraus ...

Worauf ich hinaus will: Kann man den RSSI-Wert nicht aus "weiter unten" liegenden Schichten des Betriebssystems auslesen? Per Syscall oder über eine Systemvariable ... oder wie immer das bei Android gelöst ist. Also näher an die Hardware.

Android ist doch Java? Spontan gefunden:

https://stackoverflow.com/questions/15312858/get-bluetooth-signal-strength

OP #7580306
Lesenswert?

Danke @Frank E. Der Hinweis auf den Stackflow Artikel ist sehr interessant. Das war ja genau mein Gedanke, einfach "weiter unten" ab zu greifen. Ich brauche keine echte Kommunikation, sondern nur den Empfang. Leider fehlt mir heute die Zeit die Vorschläge des Artikels zu testen. Ich werde das aber baldmöglichst testen und berichten.

Android ist nicht Java, aber die Sprache Kotlin in der ich schreibe, ist eine Weiterentwicklung von Java. Android ist ein Betriebssystem und im weitesten Sinne Linux ähnlich.

Danke auch @Harald A. Wie oben gesagt ich entwickle für ein Android Samrtphone, und das Betriebssystem ist Android, und Linux nur im weitesten Sinne ähnlich. Danke auch für den Tip mit externer Hardware. So bin ich mit meinem Projekt angefangen, nämlich mit einem auf ESP32 basierendem Bluetooth Interface. Da war das Auslesen der UUID und des RSSI Wertes ganz einfach. Ich hatte sogar schon ein Frontend mit großen 7-Segment Ziffern dazu gebaut. Als ich mein Projekt dann bei befreundeten Sportlern ansprach ergab sich so großes Interesse daran, dass ich beschlossen habe, das Rad nicht ein zweites mal zu erfinden sondern auf Massenware, nämlich ein Smartphone zu setzen. Große oder stromfressende zusatz-Hardware scheidet aus. Das ganze muss klein und praktikabel zum Mitnehmen sein. Ich hatte auch schon den Ansatz eine Tastatur zu schlachten und die üblicherweise kleine Platine per USB an das Smartphone zu koppeln oder sofort eine Bluetooth Tastatur zu schlachten und dann damit die App zu steuern. Aber das sind dann halt Individuallösungen, die nie für die Allgemeinheit brauchbar sind.

Noch mal herzlichen Dank an Euch. Gruß Armin

Ich weis noch auf welcher Seite man den Lötkolben anfasst :-)

OP #7581659
Lesenswert?

@Frank E.

Heureka ! Es funktioniert !!!

Das Programm in dem Link den Du gepostet hattest funktionierte zwar auch nicht, aber es hat mich auf den richtigen Weg gebracht. Ich habe jetzt ein neues Programm selbst geschrieben. Der Trick besteht darin, das man den "BluetoothLeScanner" nutzen muss. Sobald ich meinen Test etwas geordnet habe werde ich ihn hier veröffentlichen. Vielleicht helfe ich damit ja auch anderen, denn über die Zusammenarbeit von AirTag und Android gibt es nur wenig im Netz.

Noch mal herzlichen Dank an Frank E. Gruß Armin

Ich weis noch auf welcher Seite man den Lötkolben anfasst :-)

OP #7586594
Lesenswert?

Wie versprochen hier nun die Lösung wie man den RSSI-Wert eines Apple AirTag als Beacon ausliest. Es funktioniert für nahezu alle BLE Beacons. Der RSSI Wert des Beacon wird bei jedem gesendeten Paket angezeigt, ohne sich per Bluetooth mit dem Beacon zu verbinden. Ein Apple AirTag Beacon sendet im unkonfigurierten Zustand ca. 1x pro Sekunde. Somit sind auch schnelle Erfassungen möglich.

Als 'Bonbon' speichert die App auch noch die angezeigten Werte ab wenn man auf die Stop Taste drückt. Die Datei befindet sich in dem Ordner Dokumente/Beacontest/

Es ist ein Smartphone mit mindestens API34 extension 7 erforderlich. Das ist mit Android 14 erfüllt.

Beim Debug unter AndroidStudio ist es relativ einfach die angeforderten Rechte frei zu geben. Dazu einfach in Debug Kopfzeile auf "app" clicken, dann Edit Configuations und im Feld "Install Flags" "-g" eintragen.

Nun zum Code. Zuerst die activity_main.xml :

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">

1
<TextView
2
    android:id="@+id/textView1"
3
    android:layout_width="match_parent"
4
    android:layout_height="wrap_content"
5
    android:text="-XX dBm"
6
    android:textAlignment="center"
7
    android:textColor="@color/purple_700"
8
    android:textSize="60sp"
9
    android:textStyle="bold" />
10

11
<ScrollView
12
    android:id="@+id/TimelineScrollbar"
13
    android:layout_width="361dp"
14
    android:layout_height="545dp"
15
    android:layout_marginLeft="10dp"
16
    android:scrollIndicators="right"
17
    android:scrollbarAlwaysDrawVerticalTrack="true"
18
    android:scrollbarSize="6dp"
19
    android:scrollbarStyle="insideOverlay"
20
    android:scrollbarThumbHorizontal="@color/purple_700"
21
    android:scrollbarThumbVertical="@color/purple_700"
22
    android:scrollbars="vertical"
23
    android:textAlignment="gravity">
24

25
    <TextView
26
        android:id="@+id/textView2"
27
        android:layout_width="match_parent"
28
        android:layout_height="match_parent"
29
        android:text="BeaconList"
30
        android:textColor="@color/black"
31
        android:textSize="24sp" />
32

33
</ScrollView>
34

35
<LinearLayout
36
    android:layout_width="match_parent"
37
    android:layout_height="wrap_content"
38
    android:layout_marginTop="20dp"
39
    android:gravity="center"
40
    android:orientation="horizontal">
41

42
    <Button
43
        android:id="@+id/button_start"
44
        android:layout_width="150dp"
45
        android:layout_height="wrap_content"
46
        android:layout_marginLeft="15dp"
47
        android:layout_marginRight="10dp"
48
        android:background="#009688"
49
        android:gravity="center|center_horizontal|center_vertical"
50
        android:text="Start"
51
        android:textSize="24sp" />
52

53
    <Button
54
        android:id="@+id/button_stop"
55
        android:layout_width="150dp"
56
        android:layout_height="wrap_content"
57
        android:layout_marginLeft="10dp"
58
        android:layout_marginRight="10dp"
59
        android:background="#E91E63"
60
        android:gravity="center|center_horizontal|center_vertical"
61
        android:text="Stop"
62
        android:textSize="24sp" />
63

64
</LinearLayout>
</LinearLayout>

Dann die MainActivity.kt :

package com.example.beacontest6

import android.Manifest import android.annotation.SuppressLint import android.app.Activity import android.bluetooth.BluetoothManager import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanFilter import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanSettings import android.content.ContentValues import android.content.Context import android.content.pm.PackageManager import android.os.Bundle import android.os.Environment import android.provider.MediaStore import android.util.Log import android.view.View import android.widget.Button import android.widget.ScrollView import android.widget.TextView import android.widget.Toast import androidx.core.app.ActivityCompat

/* ----------------------------------------------------------

  • Programm : Beacontest6 -
  • Usage : Recognize Bluetooth BLE Becaon and get Infos -
  • 1
         like Devicename, Deviceadress and RSSI Value  -
    
  • Copyright : 2024 Armin Graewe, Muenster, Germany - ----------------------------------------------------------*/

class MainActivity : Activity() {

1
public override fun onCreate(savedInstanceState: Bundle?) {
2
    super.onCreate(savedInstanceState)
3
    setContentView(R.layout.activity_main)
4

5
    val bluetoothLeScanner = (getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter.bluetoothLeScanner
6

7
    val rssi_msg = findViewById<View>(R.id.textView1) as TextView
8
    val rssi_list = findViewById<View>(R.id.textView2) as TextView
9
    val scroll = findViewById<View>(R.id.TimelineScrollbar) as ScrollView
10

11
    val BuStart = findViewById<View>(R.id.button_start) as Button
12
    val BuStop = findViewById<View>(R.id.button_stop) as Button
13

14
    val settings = ScanSettings.Builder()
15
        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
16
        .build()
17

18
    val filters = mutableListOf<ScanFilter>()   // Leere Liste von Filtern sieht alle devices
19
    //filters.add(ScanFilter.Builder().setDeviceName("AirTag").build())
20
    filters.add(ScanFilter.Builder().setDeviceAddress("D6:49:48:05:D3:51").build())
21
    // Replace MAC Address with your own value or rem the above line out to see all Beacon
22

23
    val scanCallback = object : ScanCallback() {
24
        override fun onScanResult(callbackType: Int, result: ScanResult) {
25
            //Toast.makeText(getApplicationContext(), " Callback triggered ", Toast.LENGTH_SHORT).show()
26
            //rssi_msg.text = "Callback Triggered"
27
            //if (result.device.name != null && result.device.name == "AirTag") {
28
            //if (result.device.name != null) {
29
            //val name = result.device.name //"" //result.device.name
30
            val deviceAddress = result.device.address
31
            val rssi = result.rssi
32
            // Hier können Sie den RSSI-Wert des AirTag-Beacons verwenden
33
            Log.d("RSSI", "Device: $deviceAddress" + " " + "RSSI: $rssi")
34
            rssi_list.text = rssi_list.text.toString() + deviceAddress + " => " + rssi + "dBm\n"
35
            scroll.fullScroll(View.FOCUS_DOWN)
36
            rssi_msg.text = String.format("%02d  dBm", rssi)
37
            //}
38
        }
39
    }
40

41
    BuStart.setOnClickListener {
42
        if (ActivityCompat.checkSelfPermission(
43
                this,
44
                Manifest.permission.BLUETOOTH_SCAN
45
            ) != PackageManager.PERMISSION_GRANTED
46
        ) {
47
            // TODO: Consider calling
48
            //    ActivityCompat#requestPermissions
49
            // here to request the missing permissions, and then overriding
50
            //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
51
            //                                          int[] grantResults)
52
            // to handle the case where the user grants the permission. See the documentation
53
            // for ActivityCompat#requestPermissions for more details.
54
            Toast.makeText(getApplicationContext(), " No Scan Permission ", Toast.LENGTH_SHORT)
55
                .show()
56
            rssi_list.text = " No Scan Permission \n"
57
            return@setOnClickListener
58

59
        } else {
60
            // Hier wird der Scanner gestartet
61
            //bluetoothLeScanner?.startScan(filters, settings, scanCallback)
62
            bluetoothLeScanner?.startScan(filters, settings, scanCallback)
63
            Toast.makeText(getApplicationContext(), " Scanner Started ", Toast.LENGTH_SHORT)
64
                .show()
65
            rssi_list.text = " Scanner Started \n"
66
        }
67
    }
68

69
    BuStop.setOnClickListener {
70
        // Hier wird der Scanner gestopped
71
        bluetoothLeScanner?.stopScan(scanCallback)
72
        Toast.makeText(getApplicationContext(), " Scanner Stopped ", Toast.LENGTH_SHORT)
73
            .show()
74
        rssi_list.text = rssi_list.text.toString() + " Scanner Stopped \n"
75
        scroll.fullScroll(View.FOCUS_DOWN)
76

77
        // Hier wird die Liste respeichert (for API 34 ext7 only)
78
        // Zu finden unter : /storage/emulated/0/Documents/YourCustomDirectory/RSSIwerte.txt
79
        val content = rssi_list.text as String
80
        val fileName = "RSSIwerte.txt"
81
        saveStringToMediaStore(this, fileName, content)
82
    }
83
}
84

85
    // allgemeine File-Speicherfunktion (for API 34 ext7 only)
86
    // Files werden in : Documents/BeaconTest/ abgelegt
87
fun saveStringToMediaStore(context: Context, fileName: String, content: String) {
88
    val values = ContentValues().apply {
89
        put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
90
        put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
91
        put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS + "/BeaconTest")
92
    }
93

94
    val uri = context.contentResolver.insert(MediaStore.Files.getContentUri("external"), values)
95

96
    uri?.let { outputStream ->
97
        context.contentResolver.openOutputStream(outputStream).use { it?.write(content.toByteArray()) }
98
    }
99
}
100

101
@SuppressLint("MissingPermission")
102
override fun onDestroy() {
103
    super.onDestroy()
104
    //bluetoothLeScanner?.stopScan(scanCallback)
105
}

}

Und zuletzt die AndroidManifest.xml :

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">

1
<uses-permission android:name="android.permission.BLUETOOTH"/>
2
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
3
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
4
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
5
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
6
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
7
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
8

9
<application
10
    android:allowBackup="true"
11
    android:dataExtractionRules="@xml/data_extraction_rules"
12
    android:fullBackupContent="@xml/backup_rules"
13
    android:icon="@mipmap/ic_launcher"
14
    android:label="@string/app_name"
15
    android:roundIcon="@mipmap/ic_launcher_round"
16
    android:supportsRtl="true"
17
    android:theme="@style/Theme.BeaconTest6"
18
    tools:targetApi="31">
19
    <activity
20
        android:name=".MainActivity"
21
        android:exported="true"
22
        android:label="@string/app_name"
23
        android:theme="@style/Theme.BeaconTest6">
24
        <intent-filter>
25
            <action android:name="android.intent.action.MAIN" />
26

27
            <category android:name="android.intent.category.LAUNCHER" />
28
        </intent-filter>
29
    </activity>
30
</application>
</manifest>

Ich hoffe jemandem damit geholfen zu haben :-) Kommentare sind willkommen. Gruß Armin

Ich weis noch auf welcher Seite man den Lötkolben anfasst :-)

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren