Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
9346184
Add comprehensive CLAUDE.md guide for AI assistants
claude Nov 19, 2025
1a454cc
Fix lock/unlock commands for Tedee cylinders
claude Nov 21, 2025
f8677cc
Add detailed logging for unknown BLE notifications
claude Nov 21, 2025
4d6cdb4
Update CLAUDE.md with complete BLE command reference
claude Nov 21, 2025
cd33b23
Update AndroidX libraries (safe updates)
claude Nov 21, 2025
7084ac1
Update Kotlin and Android Gradle Plugin
claude Nov 21, 2025
193f909
Update AndroidX Core library
claude Nov 21, 2025
be37a1b
Update Material Design and ConstraintLayout
claude Nov 21, 2025
2bc02ce
Update networking libraries (Retrofit & OkHttp)
claude Nov 21, 2025
376f570
Bump version to 2.0
claude Nov 21, 2025
a35fecb
Update Java compatibility to version 17
claude Nov 21, 2025
ca7c2d3
Update compileSdk and targetSdk to 35
claude Nov 21, 2025
141a543
Fix Kotlin 2.1.0 type mismatch in BLE commands
claude Nov 21, 2025
5eb1412
Fix sendCommand signature - pass Byte instead of ByteArray
claude Nov 21, 2025
5bc61f5
Add cylinder configuration to Constants
claude Nov 21, 2025
0d40b39
Update CLAUDE.md with v2.0 configuration changes
claude Nov 21, 2025
ce4f7cf
Add Flutter app with Platform Channels integration
claude Nov 21, 2025
ad71f02
Integrate certificate generation into Flutter app
claude Nov 21, 2025
6401db4
Fix compilation errors for Tedee SDK 1.0.0
claude Nov 21, 2025
525417b
Add Gradle 8.9 wrapper configuration
claude Nov 21, 2025
1296fbb
Fix getMobilePublicKey call - remove context parameter
claude Nov 21, 2025
c13cd1a
Add advanced features to Flutter app
claude Nov 21, 2025
70dd66d
Fix compilation errors for SDK methods
claude Nov 21, 2025
7730302
Fix crash when sending BLE commands
claude Nov 21, 2025
3534472
Add missing withContext import
claude Nov 21, 2025
c5c1e68
Request Bluetooth permissions on app start
claude Nov 21, 2025
0a1ff3a
Fix NotProvidedSignedTime crash
claude Nov 21, 2025
ae03eb4
Remove withContext wrapper from SDK calls
claude Nov 21, 2025
e971e3b
Add ProGuard rules to preserve SDK classes
claude Nov 21, 2025
a4e522a
Downgrade Kotlin from 2.1.0 to 1.9.22 for SDK compatibility
claude Nov 21, 2025
6abb721
Disable R8 and enable Multidex for SDK compatibility
claude Nov 21, 2025
28a0898
Fix ILockInteractor DefaultImpls crash by explicitly providing all me…
claude Nov 21, 2025
0a736b2
Add documentation explaining DefaultImpls crash fix
claude Nov 21, 2025
2cbd2b1
Update Flutter and Android dependencies to latest versions
claude Nov 21, 2025
8781238
Fix getLockState to use getReadableLockStatusResult instead of getRea…
claude Nov 25, 2025
9a69538
Add HAS_ACTIVITY_LOGS (0xA5) notification detection and handling
claude Nov 25, 2025
1b2ed74
Implement activity logs download (GET_LOGS_TLV 0x2D) in both apps
claude Nov 25, 2025
d84b28f
Fix activity logs download: read result code from byte[1] instead of …
claude Nov 25, 2025
6d893f3
Add detailed logging for activity logs download and fix button text c…
claude Nov 25, 2025
9f87645
Replace Device Settings with Get Battery (0x0C) in both apps
claude Nov 25, 2025
f875a23
Add background BLE auto-connect with foreground service and persisten…
claude Nov 25, 2025
b1736c0
Fix missing Intent import in MainActivity.kt
claude Nov 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
831 changes: 831 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

34 changes: 17 additions & 17 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ plugins {

android {
namespace 'tedee.mobile.demo'
compileSdk 34
compileSdk 35

defaultConfig {
applicationId "tedee.mobile.demo"
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0"
targetSdk 35
versionCode 2
versionName "2.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand All @@ -25,11 +25,11 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '17'
}
buildFeatures {
viewBinding true
Expand All @@ -38,16 +38,16 @@ android {

dependencies {
implementation "com.jakewharton.timber:timber:${timberVersion}"
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "com.squareup.retrofit2:adapter-rxjava2:2.9.0"
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.11.0"
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation "com.squareup.retrofit2:retrofit:2.11.0"
implementation "com.squareup.retrofit2:converter-gson:2.11.0"
implementation "com.squareup.retrofit2:adapter-rxjava2:2.11.0"
implementation "com.squareup.retrofit2:converter-scalars:2.11.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
implementation "androidx.datastore:datastore-preferences:1.2.0"

//Tedee Lock SDK
implementation('com.github.tedee-com:tedee-mobile-sdk-android:1.0.0@aar') { transitive = true }
Expand Down
8 changes: 4 additions & 4 deletions app/src/main/java/tedee/mobile/demo/Constants.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package tedee.mobile.demo

// TODO: fill this data
const val PERSONAL_ACCESS_KEY: String = ""
const val PERSONAL_ACCESS_KEY: String = "snwu6R.eC+Xuad0sx5inRRo0AaZkYe+EURqYpWwrDR3lU5kuNc="
const val PRESET_ACTIVATION_CODE = ""
const val PRESET_SERIAL_NUMBER = ""
const val PRESET_DEVICE_ID = ""
const val PRESET_NAME = ""
const val PRESET_SERIAL_NUMBER = "10530206-030484"
const val PRESET_DEVICE_ID = "273450"
const val PRESET_NAME = "Lock-40C5"
64 changes: 58 additions & 6 deletions app/src/main/java/tedee/mobile/demo/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ class MainActivity : AppCompatActivity(),
uiSetupHelper.setupOpenLockClickListener {
lifecycleScope.launch {
try {
lockConnectionManager.openLock()
// Use direct BLE command 0x51 for cylinder unlock
val result = lockConnectionManager.sendCommand(0x51.toByte())
uiSetupHelper.addMessage("Open lock command sent: ${result?.print()}")
} catch (e: Exception) {
uiSetupHelper.onFailureRequest(e)
}
Expand All @@ -85,7 +87,9 @@ class MainActivity : AppCompatActivity(),
uiSetupHelper.setupCloseLockClickListener {
lifecycleScope.launch {
try {
lockConnectionManager.closeLock()
// Use direct BLE command 0x50 for cylinder lock
val result = lockConnectionManager.sendCommand(0x50.toByte())
uiSetupHelper.addMessage("Close lock command sent: ${result?.print()}")
} catch (e: Exception) {
uiSetupHelper.onFailureRequest(e)
}
Expand All @@ -94,7 +98,9 @@ class MainActivity : AppCompatActivity(),
uiSetupHelper.setupPullLockClickListener {
lifecycleScope.launch {
try {
lockConnectionManager.pullSpring()
// Use direct BLE command 0x52 for pull spring
val result = lockConnectionManager.sendCommand(0x52.toByte())
uiSetupHelper.addMessage("Pull spring command sent: ${result?.print()}")
} catch (e: Exception) {
uiSetupHelper.onFailureRequest(e)
}
Expand All @@ -109,7 +115,8 @@ class MainActivity : AppCompatActivity(),
}
}
}
uiSetupHelper.setupGetDeviceSettingsClickListener(lockConnectionManager::getDeviceSettings)
uiSetupHelper.setupDownloadActivityLogsClickListener(lockConnectionManager::sendCommand)
uiSetupHelper.setupGetBatteryClickListener(lockConnectionManager::sendCommand)
uiSetupHelper.setupGetFirmwareVersionClickListener(lockConnectionManager::getFirmwareVersion)
binding.buttonNavigateToAddDevice.setOnClickListener {
val intent = Intent(this@MainActivity, RegisterLockExampleActivity::class.java)
Expand Down Expand Up @@ -140,8 +147,53 @@ class MainActivity : AppCompatActivity(),
override fun onNotification(message: ByteArray) {
if (message.isEmpty()) return
Timber.d("LOCK LISTENER: notification: ${message.print()}")
val readableNotification = message.getReadableLockNotification()
val formattedText = "onNotification: \n$readableNotification"

// Detailed hex dump for debugging
val hexBytes = message.joinToString(" ") { byte -> "0x%02X".format(byte) }
Timber.d("LOCK LISTENER: notification bytes: $hexBytes")

// Check for HAS_ACTIVITY_LOGS notification (0xA5)
val firstByte = message.first()
val formattedText = when {
firstByte == 0xA5.toByte() -> {
"""
📋 HAS_ACTIVITY_LOGS (0xA5)

Activity logs are ready to be collected from the lock.
You can download them using the GET_LOGS_TLV command (0x2D).

- Triggered after connection
- Indicates logs waiting to download
""".trimIndent()
}
else -> {
val readableNotification = message.getReadableLockNotification()

// Add detailed info for unknown notifications
if (readableNotification.contains("unknown", ignoreCase = true)) {
val firstByteHex = "0x%02X".format(firstByte.toInt() and 0xFF)
val secondByteInfo = if (message.size > 1) {
val secondByte = message[1]
val secondByteHex = "0x%02X".format(secondByte.toInt() and 0xFF)
"$secondByte ($secondByteHex)"
} else {
"N/A"
}
"""
onNotification: $readableNotification

DEBUG INFO:
- First byte (command): $firstByte ($firstByteHex)
- Second byte (status): $secondByteInfo
- Total bytes: ${message.size}
- Full hex: $hexBytes
""".trimIndent()
} else {
"onNotification: \n$readableNotification"
}
}
}

uiSetupHelper.addMessage(formattedText)
}

Expand Down
126 changes: 118 additions & 8 deletions app/src/main/java/tedee/mobile/demo/helper/UiSetupHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,17 +116,127 @@ class UiSetupHelper(
binding.buttonSetSignedTime.setOnClickListener { getAndSetSignedTime(setSignedTime) }
}

fun setupGetDeviceSettingsClickListener(getDeviceSettings: suspend (Boolean) -> DeviceSettings?) {
binding.buttonGetDeviceSettings.setOnClickListener {
fun setupDownloadActivityLogsClickListener(sendCommand: suspend (Byte, ByteArray?) -> ByteArray?) {
binding.buttonDownloadActivityLogs.setOnClickListener {
lifecycleScope.launch {
try {
val deviceSettings = getDeviceSettings(isSecureConnected)
Timber.d("Device settings: $deviceSettings")
Toast.makeText(context, "$deviceSettings", Toast.LENGTH_SHORT).show()
} catch (e: DeviceNeedsResetError) {
Timber.e(e, "Device settings: DeviceNeedsResetError = $e")
val allLogs = mutableListOf<String>()
var packageCount = 0
var resultCode: Byte

addMessage("📋 Starting activity logs download...")

// Loop to download all log packages
do {
packageCount++
val response = sendCommand(0x2D.toByte(), null)

if (response == null || response.size < 2) {
addMessage("❌ Invalid response from lock")
return@launch
}

// Debug: Log the full response
val hexBytes = response.joinToString(" ") { byte ->
"0x%02X".format(byte.toInt() and 0xFF)
}
Timber.d("GET_LOGS response: $hexBytes (size=${response.size})")

// Response format: [COMMAND_ECHO, RESULT_CODE, DATA...]
// Byte 0: 0x2D (command echo)
// Byte 1: Result code
val commandEcho = response[0]
resultCode = response[1]

Timber.d("Command echo: 0x%02X, Result code: 0x%02X".format(
commandEcho.toInt() and 0xFF,
resultCode.toInt() and 0xFF
))

when (resultCode) {
0x00.toByte() -> {
// SUCCESS - more logs available
val logData = response.print()
allLogs.add("Package $packageCount (MORE): $logData")
Timber.d("Activity logs package $packageCount: $logData")
}
0x04.toByte() -> {
// NOT_FOUND - last package or no logs
if (response.size > 1) {
val logData = response.print()
allLogs.add("Package $packageCount (LAST): $logData")
Timber.d("Activity logs last package: $logData")
} else {
allLogs.add("Package $packageCount: No more logs available")
}
}
0x03.toByte() -> {
// BUSY - wait and retry
allLogs.add("Package $packageCount: Lock busy, waiting 200ms...")
kotlinx.coroutines.delay(200)
}
0x02.toByte() -> {
addMessage("❌ MTU too small (minimum 98 bytes required)")
return@launch
}
0x07.toByte() -> {
addMessage("❌ No permission to read logs")
return@launch
}
else -> {
val codeHex = "0x%02X".format(resultCode.toInt() and 0xFF)
addMessage("❌ Unknown result code: $codeHex\nFull response: $hexBytes")
return@launch
}
}

// Safety limit to prevent infinite loops
if (packageCount >= 100) {
allLogs.add("⚠️ Stopped after 100 packages (safety limit)")
break
}

} while (resultCode != 0x04.toByte()) // Continue until NOT_FOUND

val summary = """
|📋 Activity Logs Downloaded
|
|Total packages: $packageCount
|${allLogs.joinToString("\n")}
""".trimMargin()

addMessage(summary)
} catch (e: Exception) {
addMessage("❌ Failed to download logs: ${e.message}")
Timber.e(e, "Error downloading activity logs")
}
}
}
}

fun setupGetBatteryClickListener(sendCommand: suspend (Byte, ByteArray?) -> ByteArray?) {
binding.buttonGetBattery.setOnClickListener {
lifecycleScope.launch {
try {
// GET_BATTERY command (0x0C)
val response = sendCommand(0x0C.toByte(), null)

if (response == null || response.size < 4) {
addMessage("❌ Invalid battery response")
return@launch
}

// Response: [COMMAND_ECHO, RESULT, BATTERY_LEVEL, CHARGING_STATUS]
val batteryLevel = response[2].toInt() and 0xFF
val chargingStatus = response[3].toInt() and 0xFF
val chargingText = if (chargingStatus == 1) "⚡ Charging" else "🔌 Discharging"

val batteryInfo = "🔋 Battery: $batteryLevel% - $chargingText"
addMessage(batteryInfo)
Timber.d("Battery info: $batteryLevel% charging=$chargingStatus")
} catch (e: Exception) {
Timber.e(e, "Device settings: Other exception = $e")
addMessage("❌ Failed to get battery: ${e.message}")
Timber.e(e, "Error getting battery")
}
}
}
Expand Down
17 changes: 14 additions & 3 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -291,11 +291,11 @@
tools:visibility="visible">

<Button
android:id="@+id/buttonGetDeviceSettings"
android:id="@+id/buttonGetBattery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Get Device\nSettings"
android:text="Get Battery\n(0x0C)"
android:textSize="10sp"
app:layout_constraintEnd_toStartOf="@+id/buttonGetFirmwareVersion"
app:layout_constraintStart_toStartOf="parent"
Expand All @@ -309,7 +309,7 @@
android:text="Get Firmware\nVersion"
android:textSize="10sp"
app:layout_constraintEnd_toStartOf="@+id/buttonGetSignature"
app:layout_constraintStart_toEndOf="@+id/buttonGetDeviceSettings"
app:layout_constraintStart_toEndOf="@+id/buttonGetBattery"
app:layout_constraintTop_toTopOf="parent" />

<Button
Expand All @@ -323,6 +323,17 @@
app:layout_constraintStart_toEndOf="@+id/buttonGetFirmwareVersion"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/buttonDownloadActivityLogs"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Download Activity Logs (0x2D)"
android:textSize="10sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/buttonGetBattery" />

</androidx.constraintlayout.widget.ConstraintLayout>

<androidx.recyclerview.widget.RecyclerView
Expand Down
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.2.2' apply false
id 'com.android.library' version '8.2.2' apply false
id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
id 'com.android.application' version '8.7.3' apply false
id 'com.android.library' version '8.7.3' apply false
id 'org.jetbrains.kotlin.android' version '2.1.0' apply false
}

ext {
Expand Down
Loading