Skip to content
Open
4 changes: 4 additions & 0 deletions catroid/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ android {
buildConfigField "boolean", "FEATURE_NFC_ENABLED", "true"
buildConfigField "boolean", "FEATURE_POCKETMUSIC_ENABLED", "true"
buildConfigField "boolean", "FEATURE_RASPI_ENABLED", "true"
buildConfigField "boolean", "FEATURE_MQTT_ENABLED", "true"
buildConfigField "boolean", "FEATURE_SCRATCH_CONVERTER_ENABLED", "true"
buildConfigField "boolean", "FEATURE_USER_REPORTERS_ENABLED", "true"
buildConfigField "boolean", "FEATURE_MULTIPLAYER_VARIABLES_ENABLED", "true"
Expand Down Expand Up @@ -423,6 +424,9 @@ dependencies {
implementation 'com.google.guava:guava:28.2-android'
implementation 'com.google.code.gson:gson:2.8.7'

// MQTT
implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5'

implementation 'com.koushikdutta.async:androidasync:2.2.1'
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'ar.com.hjg:pngj:2.1.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
import static org.catrobat.catroid.ui.settingsfragments.SettingsFragment.SETTINGS_SHOW_PHIRO_BRICKS_CHECKBOX_PREFERENCE;
import static org.catrobat.catroid.ui.settingsfragments.SettingsFragment.SETTINGS_SHOW_PLOT_BRICKS;
import static org.catrobat.catroid.ui.settingsfragments.SettingsFragment.SETTINGS_SHOW_RASPI_BRICKS;
import static org.catrobat.catroid.ui.settingsfragments.SettingsFragment.SETTINGS_SHOW_MQTT_BRICKS;
import static org.hamcrest.Matchers.hasToString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.core.AllOf.allOf;
Expand Down Expand Up @@ -123,7 +124,7 @@ public class SettingsFragmentTest {
SETTINGS_CAST_GLOBALLY_ENABLED, SETTINGS_SHOW_AI_SPEECH_RECOGNITION_SENSORS,
SETTINGS_SHOW_AI_SPEECH_SYNTHETIZATION_SENSORS,
SETTINGS_SHOW_AI_FACE_DETECTION_SENSORS, SETTINGS_SHOW_AI_POSE_DETECTION_SENSORS,
SETTINGS_SHOW_AI_TEXT_RECOGNITION_SENSORS));
SETTINGS_SHOW_AI_TEXT_RECOGNITION_SENSORS, SETTINGS_SHOW_MQTT_BRICKS));
private Map<String, Boolean> initialSettings = new HashMap<>();
private Matcher<Intent> expectedBrowserIntent;

Expand Down Expand Up @@ -258,6 +259,15 @@ public void rasPiSettingsTest() {
checkPreference(R.string.preference_title_enable_raspi_bricks, SETTINGS_SHOW_RASPI_BRICKS);
}

@Category({Cat.AppUi.class, Level.Smoke.class, Cat.Gadgets.class})
@Test
public void mqttSettingsTest() {
onData(PreferenceMatchers.withTitle(R.string.preference_title_enable_mqtt_bricks))
.perform(click());

checkPreference(R.string.preference_title_enable_mqtt_bricks, SETTINGS_SHOW_MQTT_BRICKS);
}

@Category({Cat.AppUi.class, Level.Smoke.class, Cat.Gadgets.class})
@Test
public void aiSettingsTest() {
Expand Down
2 changes: 1 addition & 1 deletion catroid/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Catroid: An on-device visual programming system for Android devices
~ Copyright (C) 2010-2025 The Catrobat Team
~ Copyright (C) 2010-2026 The Catrobat Team
~ (<http://developer.catrobat.org/credits>)
~
~ This program is free software: you can redistribute it and/or modify
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Catroid: An on-device visual programming system for Android devices
* Copyright (C) 2010-2026 The Catrobat Team
* (<http://developer.catrobat.org/credits>)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* An additional term exception under section 7 of the GNU Affero
* General Public License, version 3, is available at
* http://developer.catrobat.org/license_additional_term
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.catrobat.catroid.devices.mqtt

import org.eclipse.paho.client.mqttv3.MqttCallback
import org.eclipse.paho.client.mqttv3.MqttConnectOptions

interface MqttClientInterface {
val isConnected: Boolean
fun connect(options: MqttConnectOptions)
fun disconnect()
fun close()
fun setCallback(callback: MqttCallback)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Catroid: An on-device visual programming system for Android devices
* Copyright (C) 2010-2026 The Catrobat Team
* (<http://developer.catrobat.org/credits>)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* An additional term exception under section 7 of the GNU Affero
* General Public License, version 3, is available at
* http://developer.catrobat.org/license_additional_term
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.catrobat.catroid.devices.mqtt

import android.content.Context
import org.catrobat.catroid.ui.settingsfragments.SettingsFragment

data class MqttConnectionConfig(
val host: String,
val port: Int,
val clientId: String,
val username: String,
val password: String,
val useTls: Boolean
) {
companion object {
fun fromContext(context: Context) = MqttConnectionConfig(
host = SettingsFragment.getMqttHost(context),
port = SettingsFragment.getMqttPort(context),
clientId = SettingsFragment.getMqttClientId(context),
username = SettingsFragment.getMqttUsername(context),
password = SettingsFragment.getMqttPassword(context),
useTls = SettingsFragment.isMqttTlsEnabled(context)
)
}
}
115 changes: 115 additions & 0 deletions catroid/src/main/java/org/catrobat/catroid/devices/mqtt/MqttManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Catroid: An on-device visual programming system for Android devices
* Copyright (C) 2010-2026 The Catrobat Team
* (<http://developer.catrobat.org/credits>)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* An additional term exception under section 7 of the GNU Affero
* General Public License, version 3, is available at
* http://developer.catrobat.org/license_additional_term
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.catrobat.catroid.devices.mqtt

import android.content.Context
import android.util.Log
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken
import org.eclipse.paho.client.mqttv3.MqttCallback
import org.eclipse.paho.client.mqttv3.MqttClient
import org.eclipse.paho.client.mqttv3.MqttConnectOptions
import org.eclipse.paho.client.mqttv3.MqttException
import org.eclipse.paho.client.mqttv3.MqttMessage

class MqttManager(private var mqttClient: MqttClientInterface? = null) {

val isConnected: Boolean
get() = mqttClient?.isConnected == true

companion object {
private val TAG = MqttManager::class.simpleName
private const val TCP_SCHEME = "tcp"
private const val SSL_SCHEME = "ssl"
private const val CONNECTION_TIMEOUT = 5

@JvmStatic
val instance: MqttManager by lazy { MqttManager() }
}

fun connectFromContext(context: Context) = connect(MqttConnectionConfig.fromContext(context))

fun connect(config: MqttConnectionConfig): Boolean {
if (isConnected) return true
if (config.host.isBlank()) {
Log.e(TAG, "Cannot connect: host is blank")
return false
}
return try {
val brokerUrl = buildServerUri(config.host, config.port, config.useTls)
val resolvedClientId = config.clientId.ifEmpty { MqttClient.generateClientId() }
val client = mqttClient ?: PahoMqttClient(brokerUrl, resolvedClientId).also { mqttClient = it }
client.setCallback(callback)
client.connect(buildConnectOptions(config.username, config.password))
val connected = client.isConnected
Log.d(TAG, "Connect result for clientId=$resolvedClientId at $brokerUrl: connected=$connected")
connected
} catch (e: MqttException) {
Log.e(TAG, "Failed to connect to ${config.host}:${config.port}", e)
try {
mqttClient?.close()
} catch (closeEx: MqttException) {
Log.e(TAG, "Failed to close client after connect error", closeEx)
}
mqttClient = null
false
}
}

fun disconnect() {
if (!isConnected) return
try {
mqttClient?.disconnect()
mqttClient?.close()
Log.d(TAG, "Disconnected and closed client")
} catch (e: MqttException) {
Log.e(TAG, "Error during disconnect", e)
} finally {
mqttClient = null
}
}

internal fun buildServerUri(host: String, port: Int, useTls: Boolean): String {
val scheme = if (useTls) SSL_SCHEME else TCP_SCHEME
return "$scheme://$host:$port"
}

internal fun buildConnectOptions(username: String, password: String) = MqttConnectOptions().apply {
isCleanSession = true
connectionTimeout = CONNECTION_TIMEOUT
if (username.isNotBlank()) {
this.userName = username
this.password = password.toCharArray()
}
}

private val callback = object : MqttCallback {
override fun connectionLost(cause: Throwable?) {
Log.e(TAG, "Connection lost: ${cause?.message}")
}
// Message handling is implemented in a later ticket.
override fun messageArrived(topic: String, message: MqttMessage) = Unit
// Delivery tokens are not used until publish is implemented in a later ticket.
override fun deliveryComplete(token: IMqttDeliveryToken?) = Unit
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Catroid: An on-device visual programming system for Android devices
* Copyright (C) 2010-2026 The Catrobat Team
* (<http://developer.catrobat.org/credits>)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* An additional term exception under section 7 of the GNU Affero
* General Public License, version 3, is available at
* http://developer.catrobat.org/license_additional_term
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package org.catrobat.catroid.devices.mqtt

import org.eclipse.paho.client.mqttv3.MqttCallback
import org.eclipse.paho.client.mqttv3.MqttClient
import org.eclipse.paho.client.mqttv3.MqttConnectOptions
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence

class PahoMqttClient(brokerUrl: String, clientId: String) : MqttClientInterface {
private val client = MqttClient(brokerUrl, clientId, MemoryPersistence())
override val isConnected get() = client.isConnected
override fun connect(options: MqttConnectOptions) = client.connect(options)
override fun disconnect() = client.disconnect()
override fun close() = client.close()
override fun setCallback(callback: MqttCallback) = client.setCallback(callback)
}
Loading
Loading