Skip to content

Commit

Permalink
Improve test bench for bluetooth, some refactorings
Browse files Browse the repository at this point in the history
  • Loading branch information
skjolber committed Mar 17, 2020
1 parent c91db10 commit 82b3e79
Show file tree
Hide file tree
Showing 42 changed files with 1,554 additions and 249 deletions.
39 changes: 8 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@


# External NFC Service (native style) for Android
Library for interaction with ACS NFC readers over USB; external NFC support Android devices.

Features:
- External NFC reader management and interaction
- Parallell use of external and/or internal NFC (i.e. in the same activity, both enabled at the same time)
- Support for both tags and Android devices (Host Card Emulation), simultaneously
- Use of forked `android.nfc` classes ([Ndef], [MifareUltralight], [IsoDep], etc).
- Use of forked `android.nfc` classes ([Ndef], [MifareUltralight], [IsoDep], etc) for Android 10+ support.

As this project very much simplifies implementation for use-cases requiring external NFC readers, it saves a lot of development time (2-8 weeks depending on use-case and previous knowledge).

Bugs, feature suggestions and help requests can be filed with the [issue-tracker].
Bugs, feature suggestions and help requests can be filed with the [issue-tracker]. __DO NOT send me emails unless you're prepared to pay for my time.__

## License
[Apache 2.0]
Expand All @@ -22,21 +20,21 @@ This repository contains source code for
* [A server library](externalNFCCore); services for interaction with the readers & tags
* [A client library](externalNFCAPI) (i.e. API), receiving NFC-related intents
* [An NFC library](externalNFCTools) - Android adaptation of NFC Tools
* Demo apps
* [Basic server app](externalNFCService)
* [Basic client app](externalNFCClient)
* Demonstration apps
* [Basic server app](externalNFCService) for activation of the USB and/or bluetooth NFC background service. The rest of the examples interacts with the services exported by this app.
* [Basic client app](externalNFCClient).
* [NXP API client](externalNFCNxpClient) for [MIFARE SDK](http://www.mifare.net/en/products/mifare-sdk/). Deprecated for version 2.0.0 of the library; due to Android security for hidden classes in Android 9+.
* [Web Kiosk client](externalNFCWebKiosk) with javascript bindings

There is also a [Host Card Emulation client app](externalNFCHostCardEmulationClient) for use with the [Basic client app](externalNFCClient).
There is also a [Host Card Emulation client app](externalNFCHostCardEmulationClient) for use with the [Basic client app](externalNFCClient) as well as Android-to-Android communication.

# External NFC reader API
The API defines
* broadcast actions
* service start / stop and status
* reader open / close and status
* tag connect / disconnect
* 'extras' objects for interaction with readers
* `extras` objects for interaction with readers
* disable beeps
* display text
* configure NFC tech types (PICC)
Expand Down Expand Up @@ -77,17 +75,14 @@ The readers can for the __most part can be enabled for all tag types at the same
Please note:
- Some readers only support a subset of the above tags
- For ACR 122U the Mifare Classic does not work well.
- No built-in NDEF support for Desfire EV1 cards (let me know it this is interesting to you).
- No built-in NDEF support for Desfire EV1 cards

Configuration options
- assume all NTAG21x Mifare Ultralight targets. This improves read speed, particullary for the tags which have legacy equivalents, like NTAG 210 and 213
- read only tag UIDs, ignore other tag data. This improves read speed.
- read NDEF data automatically
- read UID for Desfire EV1 targets automatically

# Troubleshooting
Please report any issues to [email protected].

### Reader connection
Note that not all Android devices actually have an USB hub, in which case no USB devices work.

Expand Down Expand Up @@ -116,24 +111,6 @@ This project contains adapted code from
* NFC Tools for Java
* SMARTRAC SDK for Android NFC NTAG

# Support
If you need professional, cost-efficient assistance with an NFC project, get in touch. I also do

* Desfire EV1 tech (with encryption) - [example app](https://play.google.com/store/apps/details?id=com.github.skjolber.nfc.skjolberg.mifare.desfiretool)
* WebView-based apps, either visiting ULRs and/or interaction over Javascript.
* NFC-initiated [wifi connectivity](https://play.google.com/store/apps/details?id=w.i)
* More advanced Host Card Emulation (HCE) for Android
* NFC development tools (some are a bit outdatet now)
* [NFC Developer](https://play.google.com/store/apps/details?id=com.antares.nfc)
* [NDEF Tools for Android](https://play.google.com/store/apps/details?id=org.ndeftools.boilerplate&hl=no)
* [Mifare Classic refactor](https://play.google.com/store/apps/details?id=com.github.skjolber.nfc.mifareclassic)
* NFC supplimented with QR codes
* Smartcard-related workflows and integrations over [ESB](http://camel.apache.org/) or [BPM](https://camunda.com/) modelling
* Custom binary formats for NDEF or raw tag data - fit your data for __minimal read/write time and best form factor selection__
* [Apache Cordova] plugins for Android

Feel free to connect with me on [LinkedIn](http://lnkd.in/r7PWDz), see also my [Github page](https://skjolber.github.io).

# History
- 2.0.0: Moved to wrapped `android.nfc` NFC android classes + various refactorings.
- 1.0.0: Library using native NFC android classes
Expand Down
2 changes: 1 addition & 1 deletion api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ android {
}

defaultConfig {
minSdkVersion 19
minSdkVersion 24
targetSdkVersion 28
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@ interface IAcr1255UReaderControl {

byte[] setAutomaticPolling(boolean on);

byte[] getBatteryLevel();

}
11 changes: 11 additions & 0 deletions api/src/main/java/com/github/skjolber/nfc/acs/Acr1255UReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -597,4 +597,15 @@ public boolean setAutomaticPolling(boolean on) {
return readBoolean(response);
}

public int getBatteryLevel() {
byte[] response;
try {
response = readerControl.getBatteryLevel();
} catch (RemoteException e) {
throw new AcrReaderException(e);
}

return readInteger(response);
}

}
37 changes: 37 additions & 0 deletions core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# core

## Usage

### USB background service
Launch the service __BackgroundUsbService__. If started without a connected reader, the service remains alive and looks for a reader. If there is no USB permission granted already, the app will ask for such a permission whenever a reader is connected.

For some Android devices the reader permission is not saved via the `request usb permission` operation in the service. To stop the 'permission box' from reappearing, add an activity which triggers on the right filter

```
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/accessory_filter" />
with accessory_filter
<resources>
<usb-device vendor-id="1839" />
</resources>
```

where 1839 is ACS.

### Bluetooth background service
Launch the service __BluetoothBackgroundService__.

## Configuration options
The service has some configuration options, loaded from shared preferences. These are listed in the top of the service class.

## Bluetooth
There is a basic implementation of bluetooth reader support

Note on bluetooth: See the docs folder for info how to reset the devices. The tool can also be using via a Windows virtual machine.


5 changes: 3 additions & 2 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ android {
}

defaultConfig {
minSdkVersion 19
minSdkVersion 24
targetSdkVersion 28
}

Expand All @@ -22,7 +22,8 @@ android {
dependencies {
api project(':tools')
compileOnly files('../libs/acssmc-1.1.5.jar')
compileOnly files('../libs/acsbt-1.0.1.aar')
//compileOnly files('../libs/acsbt-1.0.1.aar')
compileOnly files('../libs/acsbt-1.0.1.ext.jar')

// Required -- JUnit 4 framework
testImplementation 'junit:junit:4.12'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ public byte[] control(int slotNum, int controlCode, byte[] request) throws Reade
Log.d(TAG, "Raw control request: " + Utils.toHexString(request));
try {

long time = System.currentTimeMillis();

this.in = null;
this.latch = new CountDownLatch(1);

Expand All @@ -400,7 +402,7 @@ public byte[] control(int slotNum, int controlCode, byte[] request) throws Reade
throw new NfcException("Problem waiting for response");
}

Log.d(TAG, "Raw control response: " + Utils.toHexString(in));
Log.d(TAG, "Raw control response: " + Utils.toHexString(in) + " in " + (System.currentTimeMillis() - time) + " millis");

return in;
} catch (Exception e) {
Expand Down Expand Up @@ -468,15 +470,16 @@ public String getName() {
public boolean setAutomaticPolling(int slot, boolean on) throws ReaderException {
byte b = (byte) (on ? 0x01 : 0x00);

CommandAPDU command = new CommandAPDU(0xE0, 0x00, 0x00, 0x40, new byte[]{b});
byte[] command = new byte[]{(byte) 0xE0, 0x00, 0x00, 0x40, b};

CommandAPDU response = control(slot, Reader.IOCTL_CCID_ESCAPE, command);
byte[] response = control(slot, Reader.IOCTL_CCID_ESCAPE, command);

if (!isSuccess(response)) {
throw new IllegalArgumentException("Card responded with error code");
if (!isSuccessForP2(response, 0x40)) {
throw new IllegalArgumentException("Card responded with error code ");
}

boolean result = response.getData()[0] == 0x01;
// e1 00 00 40 01
boolean result = response[4] == 0x01;

if (result != on) {
Log.w(TAG, "Unable to properly enable/disable automatic polling: Expected " + on + " got " + result);
Expand All @@ -488,4 +491,23 @@ public boolean setAutomaticPolling(int slot, boolean on) throws ReaderException
return Boolean.TRUE;
}
}

public int getBatteryLevel(int slot) throws ReaderException {
// This is only applicable to firmware version 2.03.xx and above, and when the reader is in Bluetooth mode.
byte[] command = new byte[]{(byte) 0xE0, 0x00, 0x00, 0x52, 0x00};

CommandAPDU response = control(slot, Reader.IOCTL_CCID_ESCAPE, new CommandAPDU(command));

if (!isSuccess(response)) {
throw new IllegalArgumentException();
}

byte[] data = response.getData();
if(data.length > 0) {
return data[0] & 0xFF;
}
Log.d(TAG, "Unable to read battery level");

return -1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,7 @@ public interface ACR1255Commands {
byte[] transmit(int slotNum, byte[] command) throws ReaderException;

boolean setAutomaticPolling(int slotNum, boolean b) throws ReaderException;

int getBatteryLevel(int slot) throws ReaderException;

}
Original file line number Diff line number Diff line change
Expand Up @@ -362,26 +362,34 @@ public boolean setSleepModeOption(int slot, byte option) throws ReaderException
}

public boolean setAutomaticPolling(int slot, boolean on) throws ReaderException {


byte b = (byte) (on ? 0x01 : 0x00);

CommandAPDU command = new CommandAPDU(0xE0, 0x00, 0x00, 0x40, new byte[]{b});
byte[] command = new byte[]{(byte) 0xE0, 0x00, 0x00, 0x40, b};

CommandAPDU response = reader.control2(slot, Reader.IOCTL_CCID_ESCAPE, command);
byte[] response = control(slot, Reader.IOCTL_CCID_ESCAPE, command);

if (!isSuccess(response)) {
throw new IllegalArgumentException("Card responded with error code");
throw new IllegalArgumentException("Card responded with error code ");
}

boolean result = response.getData()[0] == 0x01;
// e1 00 00 40 01
boolean result = response[4] == 0x01;

if (result != on) {
Log.w(TAG, "Unable to properly update antenna field: Expected " + on + " got " + result);
Log.w(TAG, "Unable to properly enable/disable automatic polling: Expected " + on + " got " + result);

return Boolean.FALSE;
} else {
Log.d(TAG, "Updated antenna field to " + result);
Log.d(TAG, "Updated automatic polling to " + result);

return Boolean.TRUE;
}
}

public int getBatteryLevel(int slot) throws ReaderException {
Log.d(TAG, "Getting battery level is only possible in bluetooth mode");
return -1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ public static boolean isSuccessControl(ResponseAPDU in) {
return in.getSW1() == 0x90 && in.getSW2() == 0x00;
}

public static boolean isSuccess(byte[] response) {
return (response[0] & 0xFF) == 0xE1 && (response[1] & 0xFF) == 0x00 && (response[2] & 0xFF) == 0x00 && (response[3] & 0xFF) == 0x00;
}

public static boolean isSuccessForP2(byte[] response, int p2) {
return (response[0] & 0xFF) == 0xE1 && (response[1] & 0xFF) == 0x00 && (response[2] & 0xFF) == 0x00 && (response[3] & 0xFF) == p2;
}

public static boolean isSuccessForP2(CommandAPDU response, int p2) {
return response.getCLA() == 0xE1 && response.getP1() == 0x00 && response.getP2() == p2 && response.getINS() == 0x00;
}

public static boolean isZero(byte[] in, int value) {
return is(in, 0, value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,4 +301,17 @@ public byte[] setAutomaticPolling(boolean b) {
return returnValue(result, exception);
}

public byte[] getBatteryLevel() {
Integer level = null;
Exception exception = null;
try {
level = commands.getBatteryLevel(0);
} catch (Exception e) {
Log.d(TAG, "Problem reading battery level", e);

exception = e;
}

return returnValue(level, exception);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -206,5 +206,12 @@ public byte[] setAutomaticPolling(boolean b) throws RemoteException {
return wrapper.setAutomaticPolling(b);
}

@Override
public byte[] getBatteryLevel() throws RemoteException {
if (wrapper == null) {
return noReaderException();
}
return wrapper.getBatteryLevel();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public BluetoothReader getReader() {
}

public synchronized byte[] transceive(byte[] request) {
Log.d(TAG, "Raw request: " + com.github.skjolber.nfc.command.Utils.toHexString(request));
//Log.d(TAG, "Raw request: " + com.github.skjolber.nfc.command.Utils.toHexString(request));

try {

Expand All @@ -43,12 +43,14 @@ public synchronized byte[] transceive(byte[] request) {
}

try {
latch.await(commandTimeout, TimeUnit.MILLISECONDS);
if(!latch.await(commandTimeout, TimeUnit.MILLISECONDS)) {
throw new NfcException("Timeout");
}
} catch (InterruptedException e) {
throw new NfcException("Problem waiting for response");
}

Log.d(TAG, "Raw response: " + com.github.skjolber.nfc.command.Utils.toHexString(in));
//Log.d(TAG, "Raw response: " + com.github.skjolber.nfc.command.Utils.toHexString(in));

return in;
} catch (Exception e) {
Expand All @@ -63,11 +65,10 @@ public synchronized byte[] transmitPassThrough(byte[] req) throws ReaderExceptio

@Override
public void onResponseApduAvailable(BluetoothReader bluetoothReader, byte[] apdu, int errorCode) {
Log.d(TAG, "onResponseApduAvailable: " + BluetoothBackgroundService.getResponseString(apdu, errorCode));
//Log.d(TAG, "onResponseApduAvailable: " + BluetoothBackgroundService.getResponseString(apdu, errorCode));

if (errorCode == BluetoothReader.ERROR_SUCCESS) {
this.in = apdu;

}
latch.countDown();
}
Expand Down
Loading

0 comments on commit 82b3e79

Please sign in to comment.