This blog post has been written as a result of the growing interest in implementing middleware solutions around ATAK, allowing integration of third party system components without having to continually develop Plugins!
See my blog post about soldier systems and middleware if you want a little background on the subject.
I have built an ATAK Plugin that is fairly simple. It allows a MQTT client to be spun up in ATAK and it publishes the outgoing COT messages to a public MQTT broker in order to show how this is achievable. For a full implementation we would probably want to split all the COT messages out, define a new message format and push that content over MQTT as opposed to the raw COT. This is just a proof of concept to show utility and not a full blow implementation!
Setting Things up
This plugin is based upon the ATAK 4.1 SDK found here and using the Eclipse implementation of MQTT client for Android located here. First things first, let’s add some dependencies to our build.gradle (Module:app) file..
def lifecycle_version = "2.2.0"
implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.4'
implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
Re-sync Gradle and we can now get going!
MQTT Service Start
The first thing we need to do is to start the Paho background MQTT service, this keeps things running in the background and looks after some of the complexity for us. Within plugintemplate.plugin we are going to modify PluginTemplateLifecycle and add the following function;
fun startMqttService(){ val serviceIntent = Intent(mapView?.context, org.eclipse.paho.android.service.MqttService::class.java) try{ mapView?.context?.startService(serviceIntent) }catch(e: java.lang.Exception){ println("MQTT: service didnt start $e") } }
This uses the Intent service to launch the Paho MQTT service. We now just need to call this function within PluginTemplateLifecycle’s onCreate method with startMqttService(). Every time the plugin is loaded and ATAK launches, the MQTT service will spin up in the background.
MQTT MAnager
Next step is to generate a MQTT manager that we can use to encapsulate the various methods we will use to send and receive via MQTT. Generate a new class file Mqttmanager and put the following content in;
package com.atakmap.android.plugintemplate.mqtt
import android.content.Context
import org.eclipse.paho.android.service.MqttAndroidClient
import org.eclipse.paho.client.mqttv3.*
import java.lang.Exception
class Mqttmanager(
context: Context
) {
private lateinit var mqttClient: MqttAndroidClient
private var context = context
fun connectMqtt() {
val serverURI = "tcp://broker.hivemq.com:1883"
mqttClient = MqttAndroidClient(context, serverURI, "atak_client")
mqttClient.setCallback(object : MqttCallback {
override fun messageArrived(topic: String?, message: MqttMessage?) {
println("MQTT: MQTT Message Arrived $message : $topic")
}
override fun connectionLost(cause: Throwable?) {
println("MQTT: MQTT Broker Connection Lost")
}
override fun deliveryComplete(token: IMqttDeliveryToken?) {
println("MQTT: MQTT delivery complete")
}
})
val options = MqttConnectOptions()
try {
mqttClient.connect(options, context, object : IMqttActionListener {
override fun onSuccess(asyncActionToken: IMqttToken?) {
println("MQTT: MQTT Broker connection success")
subscribe()
}
override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) {
println("MQTT: MQTT Broker connection failure $exception")
}
})
} catch (e: Exception) {
println("MQTT: Mqttclinent.connect error $e")
}
}
fun subscribe() {
val topic = "testtopic/atak"
val qos = 2 // Mention your qos value
try {
mqttClient.subscribe(topic, qos, context, object : IMqttActionListener {
override fun onSuccess(asyncActionToken: IMqttToken) {
// Give your callback on Subscription here
if(topic.contains("atak")){
println("MQTT: subscribed to $topic")
}
}
override fun onFailure(
asyncActionToken: IMqttToken,
exception: Throwable
) {
// Give your subscription failure callback here
println("MQTT: Subscription failure")
}
})
} catch (e: MqttException) {
// Give your subscription failure callback here
println("MQTT: $e : exception")
}
}
fun publish(topic: String, data: String) {
val encodedPayload : ByteArray
try {
encodedPayload = data.toByteArray(charset("UTF-8"))
val message = MqttMessage(encodedPayload)
message.qos = 2
message.isRetained = false
mqttClient.publish(topic, message)
} catch (e: Exception) {
println("MQTT: publish error $e")
// Give Callback on error here
} catch (e: MqttException) {
// Give Callback on error here
println("MQTT: publish error $e")
}
}
}
This class implements all the methods we will require, within the function subscribe we are declaring a fixed topic, I would expect the topics to be passed in as part of the constructor if this was to be developed beyond a proof of concept.
Live Data Singleton
Here is a little hack I am going to use, effectively I want to set up an observer so that every time the CotHandler is triggered it updates a live data and allows the MQTTmanager to transmit a message. To do this I implement a singleton class in order to have a single implementation that can be used to shuttle data around as and when we require. This isn’t a design pattern to use liberally, but for a few of these projects it has been handy with all of the differing application/service and plugin contexts!
Generate a new class file called uiPassThrough;
package com.atakmap.android.plugintemplate
import androidx.lifecycle.MutableLiveData
object uiPassThrough {
val ownReport = MutableLiveData<String>()
fun ownLocationReport(report: String){
ownReport.postValue(report)
}
}
All this does is generate a public live data object we can access along with a public function in order to write to that live data object. For a production system we would want to use encapsulation to make the exposed object immutable, however as this is Proof of Concept, meh!
Cot Event Handler
The next component we are going to implement is a COT Event handler, this will be used to collect COT events being transmitted either internally or externally, allowing us to collect the event and then transmit it via the MQTT.
So we now need a new class file called CotHandler with the following content;
package com.atakmap.android.plugintemplate
import android.content.Context
import com.atakmap.comms.CommsMapComponent
import com.atakmap.coremap.cot.event.CotEvent
class CotHandler: CommsMapComponent.PreSendProcessor {
private var commsMapComponent1: CommsMapComponent? = null
private var atakContext1: Context? = null
fun OutboundCotMessageHandler(commsMapComponent: CommsMapComponent,
atakContext: Context
) {
commsMapComponent1 = commsMapComponent
atakContext1 = atakContext
commsMapComponent.registerPreSendProcessor(this)
}
override fun processCotEvent(cotEvent: CotEvent?, toUID: Array<out String>?) {
println("MQTT: COT Event Processed")
val casEvent = "b-r-f-h-c"
val cotMessage = cotEvent!!
val type = cotMessage.type
//For the demo we just send the entire COT event
uiPassThrough.ownLocationReport(cotEvent.toString())
//This is a method to separate out different COT events, could be used to generate different topics.
when{
cotMessage.type =="u-d-f" -> println("MQTT: Display Map Drawn Polygon ")
cotMessage.type == "u-d-r" -> println("MQTT: Display Map Drawn Square ")
cotMessage.type == "u-d-c-c" -> println("MQTT: Display Map Drawn Circle ")
cotMessage.type == "u-d-f-m" -> println("MQTT: Display Map freehand ")
cotMessage.type == "b-m-r" -> println("MQTT: Display Map Route ")
cotMessage.type == "b-t-f" -> println("MQTT: Display Chat Event ")
cotMessage.type == "b-r-f-h-c" -> println("MQTT: Display Casualty Request ")
cotMessage.type == "a-f-G-U-C" -> println("MQTT: own position report")
else -> println("MQTT: point item ")
}
}
}
The code only grabs the raw COT event and then turns it into a string whilst passing it to our Live Data object. uiPassThrough.ownLocationReport(cotEvent.toString()) is the code that does this for us.
The when{} block is an example of how to split and handle the differing cot messages, using this you could, for example, develop different MQTT topics and use this method to publish to differing MQTT topics as the COT event types change.
Now we have all the plumbing, we just need to update our pluginTemplateDropDownReceiver in order to get this running.
Plugin Template DropDown Receiver
There are a couple of things we need to do here; we need to define a lifecycle owner in order to get Live Data to work, we do this via declaring the following:
//initiate lifeCycleOwner val
private val lifeCycleOwner: LifecycleOwner
//initiate the class so we can use it
lifeCycleOwner = mapView?.context as LifecycleOwner
We can now instantiate our CotEvent handler;
val handler = CotHandler()
handler.OutboundCotMessageHandler(CommsMapComponent.getInstance(),mapView.context)
Initialise our MQTT Client;
val mqttManager = Mqttmanager(mapView.context)
mqttManager.connectMqtt()
Initialise our Live Data object and set up and observer;
val ownloc = uiPassThrough.ownReport
ownloc.observe(lifeCycleOwner, Observer {ownCot ->
//when live data observation occurs send a MQTT message
mqttManager.publish(topic, ownCot)
Toast.makeText(mapView.context, "MQTT Send to $topic",Toast.LENGTH_SHORT).show()
})
With all the code above in the onRecieve method of pluginTemplateDropDownReceiver, the code will subscribe and start transmitting cot events whenever the plugin user interface is open. I have added a button that transmits the ATAK call sign. This is featured in the the full code here:
package com.atakmap.android.plugintemplate
import android.content.Context
import android.content.Intent
import android.view.View
import android.widget.Button
import android.widget.Toast
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import com.atak.plugins.impl.PluginLayoutInflater
import com.atakmap.android.dropdown.DropDown.OnStateListener
import com.atakmap.android.dropdown.DropDownReceiver
import com.atakmap.android.maps.MapView
import com.atakmap.android.plugintemplate.mqtt.Mqttmanager
import com.atakmap.android.plugintemplate.plugin.R
import com.atakmap.comms.CommsMapComponent
import com.atakmap.coremap.log.Log
class PluginTemplateDropDownReceiver(mapView: MapView?,
private val pluginContext: Context) : DropDownReceiver(mapView), OnStateListener {
private val templateView: View
private val lifeCycleOwner: LifecycleOwner
/**************************** CONSTRUCTOR */
init {
// Remember to use the PluginLayoutInflator if you are actually inflating a custom view
// In this case, using it is not necessary - but I am putting it here to remind
// developers to look at this Inflator
templateView = PluginLayoutInflater.inflate(pluginContext, R.layout.main_layout, null)
lifeCycleOwner = mapView?.context as LifecycleOwner
}
/**************************** PUBLIC METHODS */
public override fun disposeImpl() {}
/**************************** INHERITED METHODS */
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
if (action == SHOW_PLUGIN) {
Log.d(TAG, "showing plugin drop down")
showDropDown(templateView, HALF_WIDTH, FULL_HEIGHT, FULL_WIDTH,
HALF_HEIGHT, false)
//declare our COT handler, allowing the plugin to pick up on outgoing COT
val handler = CotHandler()
handler.OutboundCotMessageHandler(CommsMapComponent.getInstance(),mapView.context)
//initialise out MQTT client
val mqttManager = Mqttmanager(mapView.context)
mqttManager.connectMqtt()
//Grab our call sign for the test MQTT button
val callSign = mapView.deviceCallsign
//a random made up topic
val topic = "testtopic/atak"
val mqttButton = templateView.findViewById<Button>(R.id.mqttPublish)
//initialise out singleton, allowing us to trigger live data updates from Cot events.
val ownloc = uiPassThrough.ownReport
ownloc.observe(lifeCycleOwner, Observer {ownCot ->
//when live data observation occurs send a MQTT message
mqttManager.publish(topic, ownCot)
Toast.makeText(mapView.context, "MQTT Send to $topic",Toast.LENGTH_SHORT).show()
})
mqttButton.setOnClickListener {
//publish a MQTT topic when we hit the test button.
mqttManager.publish(topic, callSign)
}
}
}
override fun onDropDownSelectionRemoved() {}
override fun onDropDownVisible(v: Boolean) {}
override fun onDropDownSizeChanged(width: Double, height: Double) {}
override fun onDropDownClose() {}
companion object {
val TAG = PluginTemplateDropDownReceiver::class.java
.simpleName
const val SHOW_PLUGIN = "com.atakmap.android.plugintemplate.SHOW_PLUGIN"
}
}
Summary
With that, our proof of concept is complete. I have published a public repo with a debug release of the plugin and the source code.
Note
This was developed and tested within a AVD virtual device. Since migrating testing to a real device it appears that the Paho library has a few issues with the way it launches intent services, leading to connection issues. Let me know if you have any issues/fixes I would be interested to figure out why the issues are occurring with seemingly identical API level operating systems. In the meantime I will continue to work to figure this out!