Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .idea/AndroidProjectSystem.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ BambuTagScanner is an Android application designed to read Bambu filament NFC ta
- **Bambu Tag Scanning**: Detect and process Bambu filament tags.
- **Data Dump Creation**: Extract and save tag data, including sector-specific keys.
- **View and Manage Dumps**: Browse, view details, and delete saved dumps.
- **Import dumps**: Import existing dumps in bin format.
- **Write Dumps**: Write dumps to blank "magic" tags.
- **Colour Recognition**: Extract and interpret RGB values from Bambu tag data, mapping them to predefined colour names.
- **Export Functionality**: Package dumps and associated keys into a ZIP file for easy sharing.

## Screenshot

![Image](https://github.com/user-attachments/assets/bdd6cbb2-a61b-43b1-a5e4-948905d99dd5)
![Image](https://github.com/user-attachments/assets/5f04ef67-ab94-4aff-aa37-44cf7bc8ba3d)

## Getting Started

Expand All @@ -39,6 +40,11 @@ BambuTagScanner is an Android application designed to read Bambu filament NFC ta
1. Tap the "VIEW EXISTING DUMPS" button to toggle the list of saved dumps.
2. Select a dump to view its details, including extracted tag data and colour.

### Import Dumps
1. Tap the "IMPORT DUMP" button.
2. In the file picker, navigate to your dump file and select it.
3. Dump file must be a valid bin file

### Write Dumps
1. Tap the "WRITE DUMP" button to begin the write process.
2. Bring a blank "magic" gen 2 tag close to your device.
Expand Down
124 changes: 91 additions & 33 deletions app/src/main/java/app/cherryduck/bambutagscanner/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class MainActivity : Activity() {
private lateinit var colorSwatch: View // A visual indicator, potentially for status or selection
private lateinit var writeTagButton: Button // Button for writing tag
private lateinit var exportButton: Button // Button for exporting data
private lateinit var importDumpButton: Button // Button for importing dump files

// Declare variables for managing the data and state
private var dumpsListAdapter: ArrayAdapter<String>? = null // Adapter for populating the dumpsListView
Expand All @@ -48,6 +49,9 @@ class MainActivity : Activity() {
private var waitingForTagDialog: AlertDialog? = null // Variable to hold the reference to the AlertDialog used for "waiting for tag" functionality
private var isWriteMode = false // Track whether we are in write mode

// Constants for file picker requests
private val importDumpRequestCode = 1001 // Request code for importing .bin file

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Expand All @@ -63,6 +67,8 @@ class MainActivity : Activity() {
colorSwatch = findViewById(R.id.colorSwatch) // View for displaying a color swatch
writeTagButton = findViewById(R.id.writeTagButton) // Button to write the current dump
exportButton = findViewById(R.id.exportButton) // Button for exporting the current dump
importDumpButton = findViewById(R.id.importDumpButton) // Button for importing a dump

updateButtonVisibility() // Initially hide export and write buttons

// Set click listener for the "Create Dump" button
Expand Down Expand Up @@ -108,6 +114,11 @@ class MainActivity : Activity() {
exportCurrentDump() // Export the currently loaded dump
}

// Set click listener for the "Import Dump" button
importDumpButton.setOnClickListener {
openBinFilePicker() // Start the import flow for selecting a .bin file
}

// Set item click listener for the dumps list
dumpsListView.setOnItemClickListener { _, _, position, _ ->
val fileName = dumpsListAdapter?.getItem(position) // Get the file name of the selected dump
Expand Down Expand Up @@ -528,7 +539,7 @@ class MainActivity : Activity() {

// Write all padding (00s) after the keys
val totalPaddingSize = keys.sumOf { it.size }
val padding = ByteArray(totalPaddingSize) { 0x00 }
val padding = ByteArray(totalPaddingSize)
outputStream.write(padding)
}

Expand Down Expand Up @@ -773,70 +784,117 @@ class MainActivity : Activity() {
try {
// Ensure a dump is currently loaded
val dumpFileName = currentDumpFileName ?: throw IllegalStateException("No dump loaded")
val keyFileName = dumpFileName.replace(".bin", ".dic") // Derive the key file name
val rawKeyFileName = dumpFileName.replace(".bin", "-key.bin") // Derive the raw key file name
val keyFileName = dumpFileName.replace(".bin", ".dic")
val rawKeyFileName = dumpFileName.replace(".bin", "-key.bin")

// Locate the required files in internal storage
// Locate files in internal storage
val dumpFile = File(filesDir, dumpFileName)
val keyFile = File(filesDir, keyFileName)
val rawKeyFile = File(filesDir, rawKeyFileName)
if (!dumpFile.exists() || !keyFile.exists() || !rawKeyFile.exists()) {
throw IllegalStateException("Required files not found") // Throw error if files are missing

// Collect only the files that exist
val filesToExport = mutableListOf<Pair<File, String>>()
if (dumpFile.exists()) filesToExport.add(dumpFile to dumpFileName)
if (keyFile.exists()) filesToExport.add(keyFile to keyFileName)
if (rawKeyFile.exists()) filesToExport.add(rawKeyFile to rawKeyFileName)

if (filesToExport.isEmpty()) {
throw IllegalStateException("No files found to export.")
}

// Create a ZIP file containing the dump and key files
val zipFile = createZipFile(dumpFileName, keyFileName, rawKeyFileName)
// Create a ZIP with only available files
val zipFile = createZipFileSelective(dumpFileName, filesToExport)

// Generate a content URI for the ZIP file using FileProvider
// Share as before
val uri = FileProvider.getUriForFile(
this,
"app.cherryduck.bambutagscanner.fileprovider", // FileProvider authority
"app.cherryduck.bambutagscanner.fileprovider",
zipFile
)

// Create an intent to share the ZIP file
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "application/zip" // Set the MIME type to ZIP
putExtra(Intent.EXTRA_STREAM, uri) // Attach the ZIP file URI
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) // Grant temporary read permission
type = "application/zip"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}

// Launch the share intent with a chooser dialog
startActivity(Intent.createChooser(shareIntent, "Export Dump and Keys"))
} catch (e: Exception) {
// Handle errors during export
Toast.makeText(this, "Error exporting dump: ${e.message}", Toast.LENGTH_SHORT).show()
Log.e("MainActivity", "Error in exportCurrentDump: ${e.message}", e) // Log the error
Log.e("MainActivity", "Error in exportCurrentDump: ${e.message}", e)
}
}

private fun createZipFile(dumpFileName: String, keyFileName: String, rawKeyFileName: String): File {
private fun createZipFileSelective(dumpFileName: String, filesToExport: List<Pair<File, String>>): File {
// Derive the ZIP file name from the dump file name
val zipFileName = dumpFileName.replace(".bin", ".zip")
val zipFile = File(cacheDir, zipFileName) // Create the ZIP file in the cache directory
val zipFile = File(cacheDir, zipFileName)

// Use a ZipOutputStream to create the ZIP file
ZipOutputStream(zipFile.outputStream()).use { zipOut ->
// List of files to be added to the ZIP archive, with their respective entry names
val filesToZip = listOf(
File(filesDir, dumpFileName) to dumpFileName, // Preserve the original dump file name
File(filesDir, keyFileName) to keyFileName, // Preserve the original key file name
File(filesDir, rawKeyFileName) to rawKeyFileName // Preserve the original raw key file name
)

// Iterate through the files and add them to the ZIP
for ((file, zipEntryName) in filesToZip) {
zipOut.putNextEntry(ZipEntry(zipEntryName)) // Create a new entry in the ZIP
file.inputStream().use { it.copyTo(zipOut) } // Copy the file contents to the ZIP
zipOut.closeEntry() // Close the current entry
for ((file, zipEntryName) in filesToExport) {
zipOut.putNextEntry(ZipEntry(zipEntryName))
file.inputStream().use { it.copyTo(zipOut) }
zipOut.closeEntry()
}
}

// Log the success of ZIP file creation
Log.d("MainActivity", "ZIP file created: ${zipFile.absolutePath}")

// Return the created ZIP file
return zipFile
}

// Start file picker for .bin file import
private fun openBinFilePicker() {
// Restrict to files with .bin extension using MIME and EXTRA_MIME_TYPES
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/octet-stream"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/octet-stream"))
}
startActivityForResult(intent, importDumpRequestCode)
}

// Handle the result from file pickers
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode != RESULT_OK || data?.data == null) return
when (requestCode) {
importDumpRequestCode -> importBinFile(data.data!!)
}
}

// Import a .bin file and prompt for the matching -key.bin file
private fun importBinFile(uri: android.net.Uri) {
try {
// Read the .bin file bytes from the selected URI
val binBytes = contentResolver.openInputStream(uri)?.readBytes() ?: throw Exception("Failed to read .bin file")

// Parse filament type and color name for naming
val (filamentType, colorName) = parseTagDetails(binBytes)

// Extract UID for naming
val uidBytes = binBytes.copyOfRange(0, 4)
val uidHex = uidBytes.joinToString("") { "%02X".format(it) }
val baseName = "${uidHex}-${sanitizeString("${filamentType}-${colorName}")}"

// Save the .bin file to internal storage with the app's naming convention
val fileName = "$baseName.bin"
saveInternalDump(fileName, binBytes)

// Refresh the dumps list
displayExistingDumps()

// Show a confirmation toast
Toast.makeText(this, "Import successful: $fileName", Toast.LENGTH_SHORT).show()

// Automatically load the imported dump details into the UI
loadDumpDetails(fileName)

} catch (e: Exception) {
Toast.makeText(this, "Error importing dump: ${e.message}", Toast.LENGTH_SHORT).show()
Log.e("MainActivity", "Import error", e)
}
}

}
8 changes: 7 additions & 1 deletion app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
android:text="@string/view_existing_dumps"
android:layout_marginTop="8dp" />

<Button
android:id="@+id/importDumpButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/import_dump"
android:layout_marginTop="8dp" />

<ListView
android:id="@+id/dumpsListView"
android:layout_width="match_parent"
Expand Down Expand Up @@ -52,7 +59,6 @@
android:layout_height="wrap_content"
android:text="@string/write_dump" />


<Button
android:id="@+id/exportButton"
android:layout_width="match_parent"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<string name="details_text">UID: %1$s\nFilament Type: %2$s\nColor: %3$s</string>
<string name="create_new_dump">Create New Dump</string>
<string name="view_existing_dumps">View Existing Dumps</string>
<string name="import_dump">Import Dump</string>
<string name="details_placeholder">Details will appear here</string>
<string name="export">Export</string>
<string name="waiting_for_tag">Waiting for tag…</string>
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
agp = "8.7.1"
agp = "8.11.1"
appcompat = "1.7.0"
bcprovJdk15to18 = "1.72"
kotlin = "2.0.0"
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Sat Jan 18 00:33:30 GMT 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists