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
36 changes: 24 additions & 12 deletions .github/workflows/android-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,36 @@ on:
pull_request:
branches: [ main ]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '17'
java-version: '21'
distribution: 'temurin'
- name: Set up Gradle cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Run unit tests
run: ./gradlew test --no-daemon
run: ./gradlew test
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: app/build/reports/tests/
retention-days: 7
- name: Build debug APK
run: ./gradlew assembleDebug
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: debug-apk
path: app/build/outputs/apk/debug/*.apk
retention-days: 14
79 changes: 79 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

VCOPT (OpenPrintTag Writer) is an Android NFC application for reading, writing, and generating standardized data tags for 3D printing materials (filament spools). It implements the OpenPrintTag specification, storing material metadata (brand, type, temperatures, colors, certifications) on NFC tags using CBOR encoding and NDEF formatting.

## Build Commands

```bash
./gradlew assembleDebug # Build debug APK (outputs to app/build/outputs/apk/debug/)
./gradlew assembleRelease # Build release APK
./gradlew test # Run unit tests (in app/src/test/)
./gradlew installDebug # Install to connected device
./gradlew clean # Clean build directory
```

Note: Requires `ANDROID_HOME` environment variable set (e.g., `/Users/<user>/Library/Android/sdk`).

## Architecture

**Package:** `org.openprinttag.app` (namespace), `org.openprinttag` (code)

### Core Components

- **MainActivity** - Entry point handling NFC tag detection, reading, writing, and file import/export. Uses coroutines for async NFC operations with read/write mode toggle. Decodes and displays human-readable tag data.
- **GeneratorActivity** - Material data editor with complex UI for configuring material properties. Implements smart tag selection logic (implies/hints system) and returns generated binary data to MainActivity.
- **NfcHelper** - Abstraction layer for NFC operations supporting both NfcA and NfcV tag technologies. Handles page-based reading (pages 4-129).
- **OpenPrintTagModel** (`model/`) - Data model with nested regions (Meta, Main, Aux, URL). Uses Kotlinx.serialization with custom serializers and `@SerialName` for CBOR integer key mapping. `MainRegion` and `AuxRegion` are top-level classes, not nested.
- **Serializer** (`model/`) - Bidirectional CBOR serialization with NDEF record formatting. MIME type: `application/vnd.openprinttag`. Key methods: `serialize()`, `deserialize()`, `generateDualRecordBin()`.

### Data Configuration

YAML files in `app/src/main/assets/data/` define enum maps loaded at runtime:
- `material_class_enum.yaml` - Material classes (FFF, SLA, SLS)
- `material_type_enum.yaml` - Material types (PLA, PETG, ABS, TPU, etc.)
- `tags_enum.yaml` - Material property tags with implies/hints relationships
- `material_certifications_enum.yaml` - Certifications (FDA, REACH, etc.)
- `tag_categories_enum.yaml` - Tag categories with display names and emojis

### Data Flow

**Write flow:**
1. MainActivity -> GeneratorActivity (with optional cached tag data for pre-fill)
2. GeneratorActivity loads YAML configs -> builds dynamic UI
3. User fills form -> clicks Generate
4. Serializer encodes to CBOR -> returns binary to MainActivity
5. User taps NFC tag -> NfcHelper writes data

**Read flow:**
1. User taps NFC tag -> NfcHelper reads raw bytes
2. Serializer.deserialize() parses CBOR -> OpenPrintTagModel
3. MainActivity displays human-readable format (brand, material, temps, etc.)

## Coding Conventions

- **Language**: Kotlin 2.2.0 with Java 17 toolchain
- **Classes**: PascalCase (e.g., `OpenPrintTagModel`)
- **Functions/Variables**: camelCase (e.g., `generateTag`, `nfcHelper`)
- **Layout Files**: snake_case (e.g., `activity_main.xml`)
- **View Binding**: Used throughout - avoid `findViewById`
- **Coroutines**: Dispatchers.IO for NFC/file operations, Dispatchers.Main for UI
- **Theming**: Use Material3 theme attributes (`?attr/colorOnSurface`) instead of hardcoded colors

## Testing

- Unit tests in `app/src/test/java/org/openprinttag/`
- `testOptions.returnDefaultValues = true` in build.gradle for Android mocks
- Focus on covering `model/` logic (Serializer, OpenPrintTagModel)
- Run with `./gradlew test`

## Tech Stack

- Kotlin 2.2.0 / JVM toolchain 17
- Min SDK 25 / Target SDK 35
- Kotlinx.serialization + Jackson CBOR
- SnakeYAML for config parsing
- Material Design 3 with explicit Light/Dark themes (`values/styles.xml`, `values-night/styles.xml`)
32 changes: 12 additions & 20 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ android {
compileSdk 35

compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
coreLibraryDesugaringEnabled true
}

Expand All @@ -34,22 +34,27 @@ android {
applicationId "org.openprinttag.app"
minSdk 25
targetSdk 35
versionCode 1
versionName "0.1.0"
}
// 2. Add this block to force Kotlin to match Java (outside the android block)
kotlin {
jvmToolchain(21)
jvmToolchain(17)
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
}
}
buildFeatures {
viewBinding true
}

testOptions {
unitTests.returnDefaultValues = true
}
}


Expand All @@ -72,25 +77,12 @@ dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.18.2'
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2'
/*
implementation 'androidx.compose.runtime:runtime:1.10.0'
implementation 'com.google.android.material:material:1.13.0'
implementation 'com.android.identity:identity-jvm:202411.1'
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.5" // or latest
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.0.0"
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation("org.yaml:snakeyaml:2.5")
implementation("com.github.skydoves:colorpickerview:2.3.0")
implementation("com.github.skydoves:colorpicker-compose:1.1.3")

// Test dependencies
testImplementation 'junit:junit:4.13.2'
testImplementation "org.jetbrains.kotlin:kotlin-reflect:2.2.0"
androidTestImplementation 'androidx.test:runner:1.7.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.20.1'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.20.1'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.9.0"
implementation("org.jetbrains.kotlin:kotlin-reflect")
*/
}

configurations.configureEach {
Expand Down
16 changes: 16 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Kotlin Serialization
-keepattributes *Annotation*, InnerClasses
-keepclassmembers class kotlinx.serialization.json.** { *** Companion; }
-keep,includedescriptorclasses class org.openprinttag.model.**$$serializer { *; }
-keepclassmembers class org.openprinttag.model.** { *** Companion; }

# Jackson
-keep class com.fasterxml.jackson.databind.ObjectMapper { public <methods>; }
-keepnames class com.fasterxml.jackson.** { *; }
-dontwarn com.fasterxml.jackson.databind.**

# SnakeYAML
-keep class org.yaml.snakeyaml.** { *; }

# Model classes
-keep class org.openprinttag.model.** { *; }
19 changes: 15 additions & 4 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.openprinttag.app">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="true"/>

<application
android:label="OpenPrintTag Writer"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="true"
android:theme="@style/Theme.OpenPrintTag" parent="Theme.Material3.DayNight">
android:theme="@style/Theme.OpenPrintTag">
<activity android:name="org.openprinttag.MainActivity" android:exported="true" android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="application/vnd.openprinttag"/>
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED"/>
</intent-filter>
<meta-data android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter"/>
</activity>
<activity android:name="org.openprinttag.GeneratorActivity" android:exported="true"/>
<activity android:name="org.openprinttag.GeneratorActivity" android:exported="false"/>
</application>
</manifest>
Loading