Simple Tappy Manager - TapTrack/TappyBLE GitHub Wiki

Managing the service that performs Tappy communication is a relatively complex and application-specific process; however, for certain use cases the SimpleTappyManager provided may be sufficient. If you are unfamiliar with communicating via Intents, it might be useful to read the Android Intents and Intent Filters guide as well as the Service guide if you are unfamiliar with them.

Gradle Dependencies

compile "com.taptrack.tcmptappy:tappyble-simplemanager:${latestVersion}"

Limitations

The simple manager only supports communicating with a single Tappy at a time and does not persist its connected Tappy, status, or any messages received. While persisting status or messages is relatively easily accomplished via extending the SimpleTappyManagerService, if you wish to support communicating with multiple Tappies, please see either the Advanced Tappy Management or the Communicating without Services guide.

Setup

The service must first be declared in your AndroidManifest

<service android:name="com.taptrack.tcmptappy.tappy.ble.simplemanager.SimpleTappyManagerService"
    android:exported="false"/>

While the service supports usage as a bound service, it is recommended to instead use it as a started service that is communicated with via Intents in order to simplify managing the service lifecycle. First, we must define our BroadcastReceiver in order to receive communications from the service.

    protected BroadcastReceiver tappyReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            Bundle extras = intent.getExtras();
            if(action != null && extras != null) {
                if(action.equals(SimpleTappyManagerService.Intents.ACTION_NEW_STATUS)) {
                    // the tappy has changed to one of the statuses defined in TappyBleDeviceStatus
                    int status = intent.getIntExtra(SimpleTappyManagerService.Intents.EXTRA_DEVICE_STATUS, TappyBleDeviceStatus.UNKNOWN);
                }
                else if (action.equals(SimpleTappyManagerService.Intents.ACTION_TCMP_RECEIVED)) {
                    // A message was received from the tappy
                    try {
                        TCMPMessage rawMessage = new RawTCMPMessage(intent.getByteArrayExtra(SimpleTappyManagerService.Intents.EXTRA_TCMP_BYTES));
                        
                    } catch (TCMPMessageParseException e) {
                        Log.e("MainActivity", "TCMP error", e);
                    } catch (ResponseCodeNotSupportedException | FamilyCodeNotSupportedException | MalformedPayloadException e) {
                        Log.i("MainActivity", "Unsupported tcmp", e);
                    }
                }
            }
            else if(action.equals(SimpleTappyManagerService.Intents.ACTION_REMOTE_EXCEPTION)) {
                // an exception was thrown by the tappy communication service
                String message = extras.getString(SimpleTappyManagerService.Intents.EXTRA_REMOTE_EXCEPTION_MSG);
                String trace = extras.getString(SimpleTappyManagerService.Intents.EXTRA_REMOTE_EXCEPTION_TRACE);
            }
        }
    };

Once we have defined the BroadcastReceiver in our Activity, we must register it with the system in order to receive communication from the service

    @Override
    protected void onStart() {
        super.onStart();
        IntentFilter filter = new IntentFilter();
        filter.addAction(SimpleTappyManagerService.Intents.ACTION_NEW_STATUS);
        filter.addAction(SimpleTappyManagerService.Intents.ACTION_TCMP_RECEIVED);
        registerReceiver(tappyReceiver, filter);
    }

    @Override
    protected void onStop() {
        super.onStop();
        presenter.registerScanner(scanner);
        unregisterReceiver(tappyReceiver);
    }

Connecting

With the BroadcastReceiver setup and the service defined, all that remains is to start the service and communicate. First we must acquire a TappyBleDeviceDefinition for the Tappy we wish to connect to. If you are unsure of how to do this, please refer back to Searching for Tappies.

    protected void connectTappy(TappyBleDeviceDefinition tappy) {
        Intent intent = new Intent(this,SimpleTappyManagerService.class);
        intent.setAction(SimpleTappyManagerService.Intents.ACTION_CONNECT);
        intent.putExtra(SimpleTappyManagerService.Intents.EXTRA_TAPPY_BLE_DEVICE_DEFINITION,
                new ParcelableTappyBleDeviceDefinition(connectTappy));
        startService(intent);
    }

At this point, the service will kick off the start the process of connecting to the Tappy, and you should start receiving status updates in your BroadcastReceiver. Usually this process is quite fast, but in unusual circumstances and on some devices, it can be very time consuming. Tappies with a firmware version prior to 0.38 will disable BLE advertising for power saving after several minutes. In order to connect to a Tappy that has entered this mode, you will need to power cycle the module. Tappies running firmware version 0.38 or higher will advertise as long as they are in a disconnected state.

    protected void disconnect() {
        Intent intent = new Intent(SimpleTappyManagerService.Intents.ACTION_DISCONNECT);
        broadcastIntent(intent);
    }

Note that this uses broadcastIntent instead of startService although you may use explicit intents to communicate with the service via startService if you wish to. In order to completely shutdown the service, use ACTION_SHUTDOWN as follows:

    protected void disconnect() {
        Intent intent = new Intent(SimpleTappyManagerService.Intents.ACTION_SHUTDOWN);
        broadcastIntent(intent);
    }

Sending Commands

Once the connection broadcasts TappyBleStatus.READY, the Tappy is now fully connected and ready to communicate. Note, you can send messages to the service before this and they will be queued up for sending once the Tappy is ready. However, it is recommended to wait until the Tappy is ready as sending the Tappy a barrage of rapid-fire commands will generally result in it performing only the last one as it can only do one task at a time. In the case of very fast commands like PingCommand or GetBatteryLevelCommand, the command may be executed prior to receiving the next one is received, but it is not guaranteed.

To send a command, first we must construct an instance of the relevant TCMPMessage. The message is then converted into a byte array and passed on to the service for transmission to the Tappy. For further details of TCMP, please refer to the relevant sections in the sidebar.

    public void scanForTag() {
        TCMPMessage message = new ScanTagCommand((byte)0);
        Intent intent = new Intent(SimpleTappyManagerService.Intents.ACTION_SEND_TCMP);
        intent.putExtra(SimpleTappyManagerService.Intents.EXTRA_TCMP_BYTES,message.toByteArray());
        sendBroadcast(intent);
    }

Receiving Responses

Receiving responses requires two steps - first we must receive message followed by resolving it to the actual response that was sent. In our BroadcastRecevier, we already have the code for performing the first step:

    else if (action.equals(SimpleTappyManagerService.Intents.ACTION_TCMP_RECEIVED)) {
        // A message was received from the tappy
        try {
            TCMPMessage rawMessage = new RawTCMPMessage(intent.getByteArrayExtra(SimpleTappyManagerService.Intents.EXTRA_TCMP_BYTES));    
        } catch (TCMPMessageParseException e) {
            Log.e(TAG, "TCMP error", e);
        } 
    }

Notice that this constructs a RawTCMPMessage. RawTCMPMessages are a basic type of TCMPMessage that are only used for parsing valid TCMP messages from byte arrays. If we wish to actually make sense of the payload, we must construct a CommandFamilyMessageResolver to resolve our message to a specific response type.

    CommandFamilyMessageResolver resolver = new CommandFamilyMessageResolver();
    {
        resolver.registerCommandLibrary(new SystemCommandLibrary());
        resolver.registerCommandLibrary(new BasicNfcCommandLibrary());
    }

All command families you care about resolving to should be registered with the resolver instance. Resolution of the RawTCMPMessage can then proceed:

    try {
        TCMPMessage resolved = resolver.parseResponse(rawMessage);
        if(resolved instanceof TagFoundResponse) {
            // Handle tag found
        }
        // add additional checks for response types we care about
    }
    catch (ResponseCodeNotSupportedException | FamilyCodeNotSupportedException | MalformedPayloadException e) {
        Log.i(TAG, "Unsupported tcmp", e);
    }

For further details of TCMP command families, please refer to the relevant sections of the sidebar.