Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5aa040f
import NFC Plugin
juehv Apr 3, 2026
e4ef74f
cleaning up
juehv Apr 4, 2026
ef3109e
improved test coverage
juehv Apr 4, 2026
e47cf96
plugins/main: adapt NFC command tests
juehv Apr 4, 2026
b244ecf
migrate UI and merge upstream dev branch
juehv May 17, 2026
dbc7d0d
move from jwt to uid only
juehv May 17, 2026
30aea7e
app opens on tag scan
juehv May 18, 2026
38233dd
add feature to execute a command chain from menu without tag
juehv May 18, 2026
fdbb152
extended nfc reaction to all possible nfc techs by subscribing ACTION…
juehv May 18, 2026
8eb5ebe
add foreground dispatch feature (opt-in) for generic tags
juehv May 18, 2026
09401cd
restored CLAUDE.md
juehv May 18, 2026
b1f184d
generated basic documentation
juehv May 18, 2026
2b139fd
bugfixing, ui still crap
juehv May 18, 2026
1a70f90
bugfixing / ui ploish
juehv May 19, 2026
3ba10ac
merge with upstream dev including behavior fix
juehv May 31, 2026
9451ee5
fix ignoring ISO15693 tags by explicitly naming them in manifest and …
juehv May 31, 2026
5abeb85
NFC new Actions commands and UI
Philoul May 31, 2026
f718e18
NFC Fix Tests
Philoul Jun 7, 2026
25fb743
NFC Refactoring main.general -> sync
Philoul Jun 12, 2026
02c25cc
NFC Improve Plugin Architecture
Philoul Jun 14, 2026
7d4505a
Fix tests
Philoul Jul 7, 2026
c441136
Update NFC_COMMANDS.md file
Philoul Jul 11, 2026
de77fb8
BolusWizard Source management
Philoul Jul 2, 2026
fddb079
NFC improve PS and Scene UI for log and Confirmation Popup
Philoul Jul 12, 2026
2873219
NFC Add confirmation Popup when registering an existing tag
Philoul Jul 12, 2026
d92c7e3
NFC Fix BolusProgress Cancel
Philoul Jul 12, 2026
b03c54e
Fix tests and Cancel Bolus Alert
Philoul Jul 12, 2026
94c847d
NFC Default preferences for BolusWizard Default param
Philoul Jul 13, 2026
33c213d
Merge remote-tracking branch 'Nightscout/dev' into nfc/new-nfc-plugin
Philoul Jul 14, 2026
d736225
NFC Fix BolusWizard mmol
Philoul Jul 14, 2026
374a8bb
Merge remote-tracking branch 'Nightscout/dev' into nfc/new-nfc-plugin
Philoul Jul 16, 2026
4046f1d
NFC Fix ElementType and cleaning
Philoul Jul 16, 2026
0951052
Fix BolusWizard
Philoul Jul 19, 2026
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
97 changes: 97 additions & 0 deletions _docs/NFC_COMMANDS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# NFC Commands Plugin

Allows AAPS to execute command cascades by scanning a registered NFC tag or by manually triggering execution from the My Tags screen.

## Screens

### My Tags tab
Lists all registered NFC tags. Each card shows the tag name, command icons for quick identification, and the last scanned timestamp.
- **▶ Play**: Manual execution of the command chain.
- **Edit**: Opens Edit mode in the Build screen.
- **🗑 Delete**: Remove the tag from the database.

### Log tab
Chronological history of interactions:
- **Read**: Physical scan results.
- **Write**: New tag registration.
- **Manual**: Execution triggered from the UI.

Each entry shows the tag name, timestamp, success status (color-coded), and the execution result message.

### Build screen
Wizard for assembling command chains.
- **Tag Name**: Optional name, defaults to the first command if left blank.
- **Commands**: Commands can be added, edited or removed.
- **Pump Compatibility**: Basal commands (Absolute vs Percent) are automatically filtered based on the active pump's driver capabilities.

---

## Available Actions by Category

| Category | Command | Description |
| :--- | :--- | :--- |
| **LOOP** | `LoopStopAction` | Stops the loop. |
| | `LoopResumeAction` | Resumes the loop. |
| | `LoopSuspendAction` | Suspends the loop for a specified duration. |
| | `LoopCloseAction` | Switches to Closed Loop mode. |
| | `LoopLgsAction` | Switches to Low Glucose Suspend mode. |
| **PUMP** | `PumpConnectAction` | Connects/Resumes the pump. |
| | `PumpDisconnectAction` | Disconnects/Suspends the pump for a duration. |
| **BASAL** | `BasalCancelAction` | Cancels any active temporary basal. |
| | `TempBasalAbsoluteAction` | Sets an absolute temporary basal rate (if supported). |
| | `TempBasalPercentAction` | Sets a percentage temporary basal rate (if supported). |
| **TREATMENTS** | `BolusAction` | Delivers a standard bolus (optionally marked as Meal). |
| | `CarbsAction` | Records a carb entry. |
| | `BolusWizardAction` | Launches BolusWizard on predefined parameters |
| | `ExtendedSetAction` | Starts an extended bolus. |
| | `ExtendedCancelAction` | Cancels an active extended bolus. |
| **PROFILE**| `ProfileSwitchAction` | Switches the active profile (with percentage). |
| **SCENES** | `RunSceneAction` | Runs a scene |
| **TARGETS**| `TempTargetMealAction` | Sets "Eating Soon" temporary target. |
| | `TempTargetActivityAction` | Sets "Activity" temporary target. |
| | `TempTargetHypoAction` | Sets "Hypo" temporary target. |
| | `TempTargetManuelAction` | Sets a "Manual" temporary target (with Glucose and duration) |
| | `TempTargetCancelAction` | Cancels active temporary target. |
| **SYSTEM** | `AAPSCLIENT_RESTART`| Triggers an immediate synchronization with Nightscout. (command disabled) |
| | `RESTART` | Restarts the AAPS application. (command disabled) |

---

## Technical Details

### User Entry Logging (UEL)
Every successful execution is logged in the AAPS Treatments history:
- **Source**: `Sources.NfcCommands`.
- **Note**: Contains the **Tag Name** for traceability (allows identifying which physical tag was used).
- **Color**: Matches the `userEntry` theme color.

### Intent Handling
- **NDEF_DISCOVERED**: For tags written by AAPS.
- **TAG_DISCOVERED**: Fallback for unregistered/blank tags or finished sensors whose UID is manually registered.

### Intent Filter Configuration
The plugin requires a `nfc_tech_filter.xml` (usually in `app/src/main/res/xml/`) to handle various tag technologies (IsoDep, NfcA, NfcB, etc.).

---

## Key Files & Responsibilities

| File | Core Responsibility |
| :--- | :--- |
| `NfcCommandsPlugin` | Main entry point; handles lifecycle, command routing, and feedback (vibration/toast). |
| `NfcControlActivity`| Translucent activity that intercepts System Intents and displays the confirmation dialog. |
| `NfcAction` | Abstract base class for all individual command logic. |
| `NfcCommandCode` | Central Enum defining available commands, and categories. |
| `NfcTagStore` | Handles JSON serialization and persistence of tags and logs in SharedPreferences. |
| `NfcForegroundDispatch`| Manages NFC foreground priority to intercept scans while AAPS is open. |
| `NfcBuildScreen` | UI for the command chain builder. |
| `NfcCommandsScreen` | Main UI container for the My Tags and Log tabs. |
| `NfcCommonUI` | Common UI (confirmation Popup, Commmand Icon display) |
| `NfcJsonKeys` | List of json keys for command parameters |

---

## Settings
- **Allow commands via NFC**: Master switch for execution.
- **NFC foreground priority**: Prioritizes AAPS for NFC scans over other apps (e.g. LibreLink) when AAPS is in the foreground.
- **Clear Log**: Wipes the NFC interaction history.
12 changes: 6 additions & 6 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,10 @@ println("isMaster: ${isMaster()}")
println("gitAvailable: ${gitAvailable()}")
println("allCommitted: ${allCommitted()}")
println("-------------------")
if (!gitAvailable()) {
throw GradleException("GIT system is not available. On Windows try to run Android Studio as an Administrator. Check if GIT is installed and Studio have permissions to use it")
}
if (isMaster() && !allCommitted()) {
throw GradleException("There are uncommitted changes. Clone sources again as described in wiki and do not allow gradle update")
}
// if (!gitAvailable()) {
// throw GradleException("GIT system is not available. On Windows try to run Android Studio as an Administrator. Check if GIT is installed and Studio have permissions to use it")
// }
// if (isMaster() && !allCommitted()) {
// throw GradleException("There are uncommitted changes. Clone sources again as described in wiki and do not allow gradle update")
// }

28 changes: 28 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Permission required to use the phone's NFC hardware -->
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
Expand Down Expand Up @@ -73,6 +75,32 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- NfcControlActivity processes NFC tags with AAPS commands.
The device must be unlocked before the activity can run.
NDEF_DISCOVERED fires for tags carrying the app MIME type.
TAG_DISCOVERED is a fallback for blank tags, finished Libre sensors,
and any other tag whose UID is registered in My Tags. -->
<activity
android:name="app.aaps.plugins.sync.nfcCommands.NfcControlActivity"
android:exported="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.app.aaps.command" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>

<!-- Receive new BG readings from other local apps -->
<receiver
android:name="app.aaps.receivers.DataReceiver"
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/kotlin/app/aaps/ComposeMainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ import app.aaps.plugins.automation.AutomationRuntime
import app.aaps.plugins.configuration.setupwizard.SWDefinition
import app.aaps.plugins.source.DexcomPlugin
import app.aaps.plugins.source.activities.RequestDexcomPermissionActivity
import app.aaps.plugins.sync.nfcCommands.NfcForegroundDispatch
import app.aaps.ui.compose.careDialog.CareportalEventType
import app.aaps.ui.compose.clientcontrol.ClientControlPendingDialog
import app.aaps.ui.compose.configuration.ConfigurationViewModel
Expand Down Expand Up @@ -220,6 +221,8 @@ class ComposeMainActivity : AppCompatActivity() {
private var requestMultiplePermissions: ActivityResultLauncher<Array<String>>? = null
private var onPermissionResultDenied: ((List<String>) -> Unit)? = null

private val nfcForegroundDispatch by lazy { NfcForegroundDispatch(this, preferences) }

// ViewModels (Hilt-provided via @HiltViewModel)
private val mainViewModel: MainViewModel by viewModels()
private val manageViewModel: ManageViewModel by viewModels()
Expand Down Expand Up @@ -911,10 +914,21 @@ class ComposeMainActivity : AppCompatActivity() {

override fun onResume() {
super.onResume()
nfcForegroundDispatch.onResume()
if (!config.appInitialized) return
refreshOnResume()
}

override fun onPause() {
nfcForegroundDispatch.onPause()
super.onPause()
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
nfcForegroundDispatch.onNewIntent(intent)
}

private fun updateButtons() {
permissionsViewModel.refresh()
}
Expand All @@ -940,6 +954,7 @@ class ComposeMainActivity : AppCompatActivity() {
lifecycleScope.launch {
preferences.observe(StringKey.GeneralLanguage).drop(1).collect { recreate() }
}
nfcForegroundDispatch.observeWarning(lifecycleScope, rxBus, rh)
}

private fun setupWakeLock() {
Expand Down Expand Up @@ -1166,6 +1181,7 @@ class ComposeMainActivity : AppCompatActivity() {
ElementType.QUICK_WIZARD,
ElementType.SCENE,
ElementType.AUTOMATION,
ElementType.NFC,
ElementType.COB,
ElementType.SENSITIVITY,
ElementType.USER_ENTRY,
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/xml/nfc_tech_filter.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list>
<tech>android.nfc.tech.NfcV</tech>
</tech-list>
</resources>
1 change: 1 addition & 0 deletions core/data/src/main/kotlin/app/aaps/core/data/ue/Sources.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ enum class Sources {
VirtualPump,
Random,
SMS, //From SMS plugin
NfcCommands, //From NFC Commands plugin
Treatments, //From Treatments plugin
Wear, //From Wear plugin
Food, //From Food plugin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ enum class LTag(val tag: String, val defaultValue: Boolean = true, val requiresR
GLUCOSE("GLUCOSE", defaultValue = false),
HTTP("HTTP"),
LOCATION("LOCATION"),
NFC("NFC"),
NOTIFICATION("NOTIFICATION"),
NSCLIENT("NSCLIENT"),
NSCLIENT_SYNC("NSCLIENT_SYNC", defaultValue = false),
Expand All @@ -33,4 +34,4 @@ enum class LTag(val tag: String, val defaultValue: Boolean = true, val requiresR
WORKER("WORKER"),
XDRIP("XDRIP"),
XDRIP_SYNC("XDRIP_SYNC", defaultValue = false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ enum class ElementType(
PUMP(category = ElementCategory.SYSTEM, visibility = ElementVisibility { !it.isClient }),
SETTINGS(category = ElementCategory.SYSTEM, protection = ProtectionCheck.Protection.PREFERENCES),
QUICK_LAUNCH_CONFIG(category = ElementCategory.SYSTEM, searchable = true),
NFC(category = ElementCategory.SYSTEM),

// Navigation screens
TREATMENTS(category = ElementCategory.NAVIGATION, searchable = true),
Expand Down
2 changes: 2 additions & 0 deletions core/keys/src/main/kotlin/app/aaps/core/keys/BooleanKey.kt
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ enum class BooleanKey(

SmsAllowRemoteCommands("smscommunicator_remotecommandsallowed", false, R.string.pref_title_sms_allow_remote_commands),
SmsReportPumpUnreachable("smscommunicator_report_pump_unreachable", true, R.string.pref_title_sms_report_pump_unreachable, R.string.pref_summary_sms_report_pump_unreachable),
NfcAllowRemoteCommands("nfccommunicator_remotecommandsallowed", false, R.string.pref_title_nfc_allow_remote_commands),
NfcForegroundPriority("nfccommunicator_foreground_priority", false, R.string.pref_title_nfc_foreground_priority),

VirtualPumpStatusUpload("virtualpump_uploadstatus", false, R.string.pref_title_virtual_pump_status_upload, showInNsClientMode = false),
NsClientUploadData("ns_upload", true, R.string.pref_title_ns_upload_data, R.string.pref_summary_ns_upload_data, showInNsClientMode = false, hideParentScreenIfHidden = true),
Expand Down
2 changes: 2 additions & 0 deletions core/keys/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@
<string name="pref_title_sms_allow_remote_commands">Allow remote commands via SMS</string>
<string name="pref_title_sms_report_pump_unreachable">Report pump unreachable via SMS</string>
<string name="pref_summary_sms_report_pump_unreachable">Send SMS notification when pump becomes unreachable.</string>
<string name="pref_title_nfc_allow_remote_commands">Allow commands via NFC</string>
<string name="pref_title_nfc_foreground_priority">NFC foreground priority</string>

<!-- Virtual pump preferences -->
<string name="pref_title_virtual_pump_status_upload">Upload pump status to NS</string>
Expand Down
10 changes: 10 additions & 0 deletions core/objects/src/main/res/drawable/ic_nfc.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,20L4,20L4,4h16v16zM18,6h-5c-1.1,0 -2,0.9 -2,2v2.28c-0.6,0.35 -1,0.98 -1,1.72 0,1.1 0.9,2 2,2s2,-0.9 2,-2c0,-0.74 -0.4,-1.38 -1,-1.72L13,8h3v8L8,16L8,8h2L10,6L6,6v12h12L18,6z"/>
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package app.aaps.core.ui.compose.icons

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp

val IcPluginNfc: ImageVector by lazy {
ImageVector.Builder(
name = "IcPluginNfc",
defaultWidth = 48.dp,
defaultHeight = 48.dp,
viewportWidth = 24f,
viewportHeight = 24f,
).apply {
path(
fill = SolidColor(Color.Black),
fillAlpha = 1.0f,
stroke = null,
strokeAlpha = 1.0f,
strokeLineWidth = 1.0f,
strokeLineCap = StrokeCap.Butt,
strokeLineJoin = StrokeJoin.Miter,
strokeLineMiter = 1.0f,
) {
// Outer card border (clockwise winding)
moveTo(20f, 2f)
lineTo(4f, 2f)
curveToRelative(-1.1f, 0f, -2f, 0.9f, -2f, 2f)
verticalLineToRelative(16f)
curveToRelative(0f, 1.1f, 0.9f, 2f, 2f, 2f)
horizontalLineToRelative(16f)
curveToRelative(1.1f, 0f, 2f, -0.9f, 2f, -2f)
lineTo(22f, 4f)
curveToRelative(0f, -1.1f, -0.9f, -2f, -2f, -2f)
close()
// Inner square cutout (counter-clockwise winding creates hole)
moveTo(20f, 20f)
lineTo(4f, 20f)
lineTo(4f, 4f)
horizontalLineToRelative(16f)
verticalLineToRelative(16f)
close()
// NFC chip symbol
moveTo(18f, 6f)
horizontalLineToRelative(-5f)
curveToRelative(-1.1f, 0f, -2f, 0.9f, -2f, 2f)
verticalLineToRelative(2.28f)
curveToRelative(-0.6f, 0.35f, -1f, 0.98f, -1f, 1.72f)
curveToRelative(0f, 1.1f, 0.9f, 2f, 2f, 2f)
reflectiveCurveToRelative(2f, -0.9f, 2f, -2f)
curveToRelative(0f, -0.74f, -0.4f, -1.38f, -1f, -1.72f)
lineTo(13f, 8f)
horizontalLineToRelative(3f)
verticalLineToRelative(8f)
lineTo(8f, 16f)
lineTo(8f, 8f)
horizontalLineToRelative(2f)
lineTo(10f, 6f)
lineTo(6f, 6f)
verticalLineToRelative(12f)
horizontalLineToRelative(12f)
lineTo(18f, 6f)
close()
}
}.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ data class ElementColors(
val cob: Color,
val sensitivity: Color,
val automation: Color,
val nfc: Color,
// New fields for ElementType
val bolusWizard: Color,
val note: Color,
Expand Down Expand Up @@ -82,6 +83,7 @@ val LightElementColors = ElementColors(
cob = Color(0xFFFF5722), // deep orange — distinct from COB line (#FB8C00)
sensitivity = Color(0xFF008585), // teal — autosens icon color
automation = Color(0xFF66BB6A), // green — same as userEntry light
nfc = Color(0xFFCF8BFE), //
bolusWizard = Color(0xFF66BB6A), // moved from generalColors.calculator
note = Color(0xFFFEAF05), // was careportal
question = Color(0xFFFF9800), // distinct from note — amber/orange
Expand Down Expand Up @@ -132,6 +134,7 @@ val DarkElementColors = ElementColors(
cob = Color(0xFFFFAB91), // soft salmon — distinct from COB line (#FFB74D)
sensitivity = Color(0xFF008585), // teal — autosens icon color
automation = Color(0xFF6AE86D), // green — same as userEntry dark
nfc = Color(0xFFCF8BFE), //
bolusWizard = Color(0xFF67E86A), // moved from generalColors.calculator (night)
note = Color(0xFFFEAF05), // was careportal
question = Color(0xFFFFB74D), // distinct from note — warm amber
Expand Down
Loading