diff --git a/app/build.gradle b/app/build.gradle
index e0a1f1a..3208907 100755
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -73,11 +73,8 @@ dependencies {
// JSON processing - updated Gson
implementation 'com.google.code.gson:gson:2.10.1'
- // MQTT - updated to latest stable versions
+ // MQTT - pure Java client only (Android 12+ compatible)
implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5'
- implementation('org.eclipse.paho:org.eclipse.paho.android.service:1.1.1') {
- exclude module: 'support-v4'
- }
// IBM App ID SDK for authentication - TEMPORARILY REMOVED
// implementation 'com.github.ibm-cloud-security:appid-clientsdk-android:6.+'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 77110a5..685fb4c 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -46,10 +46,6 @@
android:name=".ble.BluetoothLeService"
android:enabled="true" />
-
-
-
@@ -81,5 +77,11 @@
android:exported="false"
android:launchMode="singleInstance" />
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/pyrrha_platform/DeviceDashboard.java b/app/src/main/java/org/pyrrha_platform/DeviceDashboard.java
index 505f675..d7ddf2e 100644
--- a/app/src/main/java/org/pyrrha_platform/DeviceDashboard.java
+++ b/app/src/main/java/org/pyrrha_platform/DeviceDashboard.java
@@ -45,6 +45,7 @@
import org.pyrrha_platform.utils.MyIoTActionListener;
import org.pyrrha_platform.utils.PyrrhaEvent;
import org.pyrrha_platform.galaxy.ProviderService;
+import org.pyrrha_platform.simulation.BluetoothDataSimulator;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -78,6 +79,7 @@ public class DeviceDashboard extends AppCompatActivity {
Button valueHumidity;
Button valueCO;
Button valueNO2;
+ Button btTest;
ImageView imgBluetooh;
ImageView imgConnectivity;
final Handler handler = new Handler();
@@ -98,6 +100,10 @@ public class DeviceDashboard extends AppCompatActivity {
private ProviderService mProviderService;
private boolean mIsProviderServiceBound = false;
+ // Bluetooth simulation components
+ private BluetoothDataSimulator mBluetoothSimulator;
+ private boolean mIsSimulationMode = false;
+
// Code to manage Service lifecycle.
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@@ -290,9 +296,13 @@ public void onCreate(Bundle savedInstanceState) {
valueHumidity = findViewById(R.id.btHumidity);
valueCO = findViewById(R.id.btCO);
valueNO2 = findViewById(R.id.btNO2);
+ btTest = findViewById(R.id.btTest);
imgBluetooh = findViewById(R.id.imgBluetooth);
imgConnectivity = findViewById(R.id.imgConnectivity);
+
+ // Initialize Bluetooth data simulator
+ initializeBluetoothSimulator();
Intent gattServiceIntent = new Intent(this, BluetoothLeService.class);
bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE);
@@ -390,22 +400,39 @@ private void displayData(String data) {
// We display the data in the mobile screen
String[] parts = data.split(" ");
-
- // System.out.println("RS CO: " + parts[8]);
+
+ // Handle different data formats (real Bluetooth vs simulated)
+ String tempValue, humValue, coValue, no2Value;
+
+ if (mIsSimulationMode || data.startsWith("SimBT")) {
+ // Simulated data format: "SimBT tempValue tempStdDev humValue humStdDev coValue coStdDev no2Value no2StdDev"
+ tempValue = parts[1];
+ humValue = parts[3];
+ coValue = parts[5];
+ no2Value = parts[7];
+ Log.d(TAG, "Processing simulated data - T:" + tempValue + " H:" + humValue + " CO:" + coValue + " NO2:" + no2Value);
+ } else {
+ // Real Bluetooth data format: assume original format
+ tempValue = parts[2];
+ humValue = parts[4];
+ coValue = parts[6];
+ no2Value = parts[8];
+ Log.d(TAG, "Processing real Bluetooth data");
+ }
// tempValue, tempValueStDev, humValue, humValueStDev, coValue, coValueStDev, no2Value, no2ValueStDev
- valueTemperature.setText(parts[2] + "\n celsius");
- valueHumidity.setText(parts[4] + "\n %");
+ valueTemperature.setText(tempValue + "\n celsius");
+ valueHumidity.setText(humValue + "\n %");
- if (Float.parseFloat(parts[6]) < 0 || Float.parseFloat(parts[6]) > 1000)
+ if (Float.parseFloat(coValue) < 0 || Float.parseFloat(coValue) > 1000)
valueCO.setText("##.## \n ppm");
else
- valueCO.setText(parts[6] + "\n ppm");
+ valueCO.setText(coValue + "\n ppm");
- if (Float.parseFloat(parts[8]) < 0 || Float.parseFloat(parts[8]) > 10)
+ if (Float.parseFloat(no2Value) < 0 || Float.parseFloat(no2Value) > 10)
valueNO2.setText("##.## \n ppm");
else
- valueNO2.setText(parts[8] + "\n ppm");
+ valueNO2.setText(no2Value + "\n ppm");
// System.out.println("RS CO: " + parts[8]);
// System.out.println("RS NO2: " + parts[9]);
@@ -417,35 +444,38 @@ private void displayData(String data) {
Date device_timestamp = new Date();
- // create a random PyrrhaEvent
+ // create a PyrrhaEvent from the sensor data
PyrrhaEvent pe = new PyrrhaEvent();
pe.setFirefighter_id(user_id);
- pe.setDevice_id(mDeviceName);
+ pe.setDevice_id(mIsSimulationMode ? "Prometeo:00:00:00:00:00:01" : mDeviceName);
pe.setDevice_battery_level("0");
pe.setAcrolein((float) 0.0);
pe.setBenzene((float) 0.0);
- pe.setCarbon_monoxide(Float.parseFloat(parts[6]));
+ pe.setCarbon_monoxide(Float.parseFloat(coValue));
pe.setFormaldehyde((float) 0.0);
- pe.setNitrogen_dioxide(Float.parseFloat(parts[8]));
- pe.setTemperature(Float.parseFloat(parts[2]));
- pe.setHumidity(Float.parseFloat(parts[4]));
+ pe.setNitrogen_dioxide(Float.parseFloat(no2Value));
+ pe.setTemperature(Float.parseFloat(tempValue));
+ pe.setHumidity(Float.parseFloat(humValue));
pe.setDevice_timestamp(f.format(device_timestamp));
// Send sensor data to Galaxy Watch
if (mIsProviderServiceBound && mProviderService != null) {
try {
- float temperature = Float.parseFloat(parts[2]);
- float humidity = Float.parseFloat(parts[4]);
- float co = Float.parseFloat(parts[6]);
- float no2 = Float.parseFloat(parts[8]);
+ float temperature = Float.parseFloat(tempValue);
+ float humidity = Float.parseFloat(humValue);
+ float co = Float.parseFloat(coValue);
+ float no2 = Float.parseFloat(no2Value);
// Validate and sanitize CO and NO2 readings
if (co < 0 || co > 1000) co = 0.0f;
if (no2 < 0 || no2 > 10) no2 = 0.0f;
- mProviderService.updateSensorData(temperature, humidity, co, no2, mDeviceName);
- Log.d(TAG, "Sent sensor data to Galaxy Watch: T=" + temperature + "°C, H=" + humidity + "%, CO=" + co + "ppm, NO2=" + no2 + "ppm");
+ String deviceName = mIsSimulationMode ? "Prometeo-Simulator" : mDeviceName;
+ mProviderService.updateSensorData(temperature, humidity, co, no2, deviceName);
+
+ String dataSource = mIsSimulationMode ? "SIMULATED" : "BLUETOOTH";
+ Log.i(TAG, "Sent " + dataSource + " sensor data to Galaxy Watch: T=" + temperature + "°C, H=" + humidity + "%, CO=" + co + "ppm, NO2=" + no2 + "ppm");
} catch (NumberFormatException e) {
Log.w(TAG, "Failed to parse sensor data for Galaxy Watch: " + e.getMessage());
}
@@ -576,6 +606,11 @@ private void sendData(PyrrhaEvent pe, Date device_timestamp) throws Exception {
IoTClient iotClient = IoTClient.getInstance(context);
String messageData = MessageFactory.getPyrrhaDeviceMessage(pe);
+ if (mIsSimulationMode) {
+ Log.i(TAG, "Publishing SIMULATED sensor data to MQTT: " + messageData);
+ } else {
+ Log.i(TAG, "Publishing real Bluetooth sensor data to MQTT: " + messageData);
+ }
System.out.println(messageData);
iotClient.publishEvent(Constants.TEXT_EVENT, "json", messageData, 0, false, listener);
@@ -758,6 +793,28 @@ public void scanClicked(View view) {
}
+ public void testClicked(View view) {
+ if (mBluetoothSimulator.isRunning()) {
+ // Stop simulation
+ mBluetoothSimulator.stopSimulation();
+ mIsSimulationMode = false;
+ btTest.setBackgroundResource(R.drawable.ic_test);
+ Log.i(TAG, "Bluetooth simulation stopped by user");
+
+ // Show Bluetooth icon as we're no longer receiving simulated data
+ imgBluetooh.setVisibility(View.VISIBLE);
+ } else {
+ // Start simulation
+ mBluetoothSimulator.startSimulation();
+ mIsSimulationMode = true;
+ btTest.setBackgroundResource(R.drawable.ic_test_active);
+ Log.i(TAG, "Bluetooth simulation started by user");
+
+ // Hide Bluetooth icon as we're now receiving simulated data
+ imgBluetooh.setVisibility(View.INVISIBLE);
+ }
+ }
+
public void loginClicked(View view) {
final Intent intent;
@@ -817,9 +874,35 @@ protected void onDestroy() {
mBluetoothLeService.close();
handler.removeCallbacksAndMessages(null);
+ // Cleanup simulator
+ if (mBluetoothSimulator != null) {
+ mBluetoothSimulator.cleanup();
+ }
+
// we have to release the variablre to maintain the app connected
mWakeLock.release();
}
+ /**
+ * Initialize the Bluetooth data simulator
+ */
+ private void initializeBluetoothSimulator() {
+ mBluetoothSimulator = new BluetoothDataSimulator(this);
+ mBluetoothSimulator.setDataCallback(new BluetoothDataSimulator.DataCallback() {
+ @Override
+ public void onDataReceived(String simulatedData) {
+ Log.d(TAG, "Received simulated Bluetooth data: " + simulatedData);
+
+ // Process simulated data the same way as real Bluetooth data
+ displayData(simulatedData);
+
+ // Log that this is simulated data for watch integration
+ if (mIsProviderServiceBound && mProviderService != null) {
+ Log.d(TAG, "Simulated sensor data forwarded to Galaxy Watch integration");
+ }
+ }
+ });
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/org/pyrrha_platform/DeviceScanActivity.java b/app/src/main/java/org/pyrrha_platform/DeviceScanActivity.java
index cf63246..48e29a8 100644
--- a/app/src/main/java/org/pyrrha_platform/DeviceScanActivity.java
+++ b/app/src/main/java/org/pyrrha_platform/DeviceScanActivity.java
@@ -56,7 +56,7 @@ public void run() {
addresses.add(address);
bluetoothDevices.add(device);
deviceNames.add(name);
- mLeDeviceListAdapter.notifyDataSetChanged();
+ if (mLeDeviceListAdapter != null) { mLeDeviceListAdapter.notifyDataSetChanged(); }
}
}
}
@@ -195,7 +195,7 @@ public void run() {
public void scanClicked(View view) {
buttonScanDevice.setEnabled(false);
buttonAddDevice.setEnabled(false);
- mLeDeviceListAdapter.clear();
+ if (mLeDeviceListAdapter != null) { mLeDeviceListAdapter.clear(); }
bluetoothDevices.clear();
addresses.clear();
deviceNames.clear();
@@ -223,10 +223,15 @@ public void addClicked(View view) {
}
+ public void simulateClicked(View view) {
+ Intent intent = new Intent(DeviceScanActivity.this, org.pyrrha_platform.simulation.SimulationTestActivity.class);
+ startActivity(intent);
+ }
+
@Override
protected void onDestroy() {
super.onDestroy();
- mLeDeviceListAdapter.clear();
+ if (mLeDeviceListAdapter != null) { mLeDeviceListAdapter.clear(); }
bluetoothDevices.clear();
addresses.clear();
deviceNames.clear();
diff --git a/app/src/main/java/org/pyrrha_platform/iot/IoTClient.java b/app/src/main/java/org/pyrrha_platform/iot/IoTClient.java
index 891cb86..a676917 100644
--- a/app/src/main/java/org/pyrrha_platform/iot/IoTClient.java
+++ b/app/src/main/java/org/pyrrha_platform/iot/IoTClient.java
@@ -18,14 +18,15 @@
import android.content.Context;
import android.util.Log;
-import org.eclipse.paho.android.service.MqttAndroidClient;
import org.eclipse.paho.client.mqttv3.IMqttActionListener;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.IMqttToken;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.MqttPersistenceException;
+import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import javax.net.SocketFactory;
@@ -39,10 +40,13 @@ public class IoTClient {
private static final String IOT_ORGANIZATION_TCP = ".messaging.internetofthings.ibmcloud.com:1883";
private static final String IOT_ORGANIZATION_SSL = ".messaging.internetofthings.ibmcloud.com:8883";
private static final String IOT_DEVICE_USERNAME = "use-token-auth";
-
+
+ // Local MQTT broker configuration
+ private static final String LOCAL_MQTT_HOST = "10.0.2.2";
+ private static final String LOCAL_MQTT_PORT = "1883";
private static IoTClient instance;
private final Context context;
- private MqttAndroidClient client;
+ private MqttAsyncClient client;
private String organization;
private String deviceType;
private String deviceID;
@@ -123,45 +127,79 @@ public static String getCommandTopic(String command, String format) {
*/
public IMqttToken connectDevice(IoTCallbacks callbacks, IoTActionListener listener, SocketFactory factory) throws MqttException {
Log.d(TAG, ".connectDevice() entered");
- String clientID = "d:" + this.getOrganization() + ":" + this.getDeviceType() + ":" + this.getDeviceID();
+
+ // Use local MQTT broker configuration instead of IBM Watson IoT
+ String clientID;
String connectionURI;
- if (factory == null || this.getOrganization().equals("quickstart")) {
- connectionURI = "tcp://" + this.getOrganization() + IOT_ORGANIZATION_TCP;
+ String username;
+ String password;
+
+ // Check if we should use local MQTT broker (based on properties configuration)
+ if (this.getOrganization() != null && this.getOrganization().equals("local")) {
+ // Local MQTT broker configuration
+ // Map any device ID to one of the 4 available Prometeo devices (01-04)
+ String deviceNumber = "1"; // Default to device 1 for now
+ if (this.getDeviceID() != null && this.getDeviceID().length() > 0) {
+ // Simple hash-based mapping to distribute devices across 01-04
+ int hash = Math.abs(this.getDeviceID().hashCode()) % 4;
+ deviceNumber = String.valueOf(hash + 1);
+ }
+ clientID = "Prometeo:00:00:00:00:00:0" + deviceNumber;
+ connectionURI = "tcp://" + LOCAL_MQTT_HOST + ":" + LOCAL_MQTT_PORT;
+ username = clientID; // VerneMQ uses client ID as username
+ password = "password"; // Standard password for local development
+ Log.d(TAG, "Using local MQTT broker: " + connectionURI + " with device: " + clientID);
} else {
- connectionURI = "ssl://" + this.getOrganization() + IOT_ORGANIZATION_SSL;
+ // Original IBM Watson IoT configuration for backward compatibility
+ clientID = "d:" + this.getOrganization() + ":" + this.getDeviceType() + ":" + this.getDeviceID();
+ if (factory == null || this.getOrganization().equals("quickstart")) {
+ connectionURI = "tcp://" + this.getOrganization() + IOT_ORGANIZATION_TCP;
+ } else {
+ connectionURI = "ssl://" + this.getOrganization() + IOT_ORGANIZATION_SSL;
+ }
+ username = IOT_DEVICE_USERNAME;
+ password = this.getAuthorizationToken();
+ Log.d(TAG, "Using IBM Watson IoT: " + connectionURI);
}
if (!isMqttConnected()) {
- if (client != null) {
- client.unregisterResources();
- client = null;
+ if (client != null && client.isConnected()) {
+ try {
+ client.disconnect();
+ } catch (MqttException e) {
+ Log.w(TAG, "Error disconnecting existing client", e);
+ }
}
- client = new MqttAndroidClient(context, connectionURI, clientID);
- client.setCallback(callbacks);
-
- char[] password = this.getAuthorizationToken().toCharArray();
-
- MqttConnectOptions options = new MqttConnectOptions();
- options.setCleanSession(true);
- options.setUserName(IOT_DEVICE_USERNAME);
- options.setPassword(password);
-
- if (factory != null && !this.getOrganization().equals("quickstart")) {
- options.setSocketFactory(factory);
- }
-
- Log.d(TAG, "Connecting to server: " + connectionURI);
+
try {
- // connect
- return client.connect(options, context, listener);
+ // Use pure Java MQTT client with memory persistence
+ MemoryPersistence persistence = new MemoryPersistence();
+ client = new MqttAsyncClient(connectionURI, clientID, persistence);
+ client.setCallback(callbacks);
+
+ MqttConnectOptions options = new MqttConnectOptions();
+ options.setCleanSession(true);
+ options.setUserName(username);
+ options.setPassword(password.toCharArray());
+ options.setConnectionTimeout(30);
+ options.setKeepAliveInterval(60);
+ options.setAutomaticReconnect(true);
+
+ if (factory != null && !this.getOrganization().equals("quickstart")) {
+ options.setSocketFactory(factory);
+ }
+
+ Log.d(TAG, "Connecting to server: " + connectionURI + " with clientID: " + clientID);
+
+ // Connect with pure Java client
+ return client.connect(options, null, listener);
} catch (MqttException e) {
- Log.e(TAG, "Exception caught while attempting to connect to server", e.getCause());
+ Log.e(TAG, "Exception caught while attempting to connect to server", e);
throw e;
}
}
return null;
}
-
/**
* Disconnect the device from the Watson Internet of Things Platform
*
@@ -173,9 +211,9 @@ public IMqttToken disconnectDevice(IoTActionListener listener) throws MqttExcept
Log.d(TAG, ".disconnectDevice() entered");
if (isMqttConnected()) {
try {
- return client.disconnect(context, listener);
+ return client.disconnect(null, listener);
} catch (MqttException e) {
- Log.e(TAG, "Exception caught while attempting to disconnect from server", e.getCause());
+ Log.e(TAG, "Exception caught while attempting to disconnect from server", e);
throw e;
}
}
@@ -356,7 +394,7 @@ private IMqttDeliveryToken publish(String topic, String payload, int qos, boolea
try {
// create ActionListener to handle message published results
Log.d(TAG, ".publish() - Publishing " + payload + " to: " + topic + ", with QoS: " + qos + " with retained flag set to " + retained);
- return client.publish(topic, mqttMsg, context, listener);
+ return client.publish(topic, mqttMsg, null, listener);
} catch (MqttPersistenceException e) {
Log.e(TAG, "MqttPersistenceException caught while attempting to publish a message", e.getCause());
throw e;
diff --git a/app/src/main/java/org/pyrrha_platform/simulation/BluetoothDataSimulator.java b/app/src/main/java/org/pyrrha_platform/simulation/BluetoothDataSimulator.java
new file mode 100644
index 0000000..8f0dc49
--- /dev/null
+++ b/app/src/main/java/org/pyrrha_platform/simulation/BluetoothDataSimulator.java
@@ -0,0 +1,158 @@
+package org.pyrrha_platform.simulation;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.util.Random;
+
+/**
+ * Simulates Bluetooth data reception from a Prometeo device
+ * Generates realistic sensor readings with proper timing intervals
+ */
+public class BluetoothDataSimulator {
+ private static final String TAG = "BluetoothDataSimulator";
+
+ // Simulation parameters
+ private static final int SIMULATION_INTERVAL_MS = 2000; // 2 seconds
+
+ // Realistic sensor ranges for Prometeo device
+ private static final float TEMP_MIN = 20.0f;
+ private static final float TEMP_MAX = 40.0f;
+ private static final float HUMIDITY_MIN = 30.0f;
+ private static final float HUMIDITY_MAX = 90.0f;
+ private static final float CO_MIN = 0.0f;
+ private static final float CO_MAX = 50.0f;
+ private static final float NO2_MIN = 0.0f;
+ private static final float NO2_MAX = 2.0f;
+
+ private Handler mHandler;
+ private Runnable mSimulationRunnable;
+ private Random mRandom;
+ private boolean mIsRunning = false;
+ private Context mContext;
+ private DataCallback mDataCallback;
+
+ // Current sensor values (for gradual changes)
+ private float mCurrentTemp = 25.0f;
+ private float mCurrentHumidity = 50.0f;
+ private float mCurrentCO = 5.0f;
+ private float mCurrentNO2 = 0.5f;
+
+ public interface DataCallback {
+ void onDataReceived(String simulatedBluetoothData);
+ }
+
+ public BluetoothDataSimulator(Context context) {
+ mContext = context;
+ mHandler = new Handler(Looper.getMainLooper());
+ mRandom = new Random();
+ }
+
+ public void setDataCallback(DataCallback callback) {
+ mDataCallback = callback;
+ }
+
+ public void startSimulation() {
+ if (mIsRunning) {
+ Log.w(TAG, "Simulation already running");
+ return;
+ }
+
+ mIsRunning = true;
+ Log.i(TAG, "Starting Bluetooth data simulation - generating readings every " +
+ SIMULATION_INTERVAL_MS/1000 + " seconds");
+
+ mSimulationRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (!mIsRunning) return;
+
+ // Generate realistic sensor data
+ String simulatedData = generateSensorData();
+
+ // Send data to callback
+ if (mDataCallback != null) {
+ mDataCallback.onDataReceived(simulatedData);
+ }
+
+ // Schedule next reading
+ mHandler.postDelayed(this, SIMULATION_INTERVAL_MS);
+ }
+ };
+
+ // Start immediately
+ mHandler.post(mSimulationRunnable);
+ }
+
+ public void stopSimulation() {
+ if (!mIsRunning) {
+ Log.w(TAG, "Simulation not running");
+ return;
+ }
+
+ mIsRunning = false;
+ Log.i(TAG, "Stopping Bluetooth data simulation");
+
+ if (mHandler != null && mSimulationRunnable != null) {
+ mHandler.removeCallbacks(mSimulationRunnable);
+ }
+ }
+
+ public boolean isRunning() {
+ return mIsRunning;
+ }
+
+ /**
+ * Generates realistic sensor data in the format expected by the DeviceDashboard
+ * Format: "timestamp tempValue tempStdDev humValue humStdDev coValue coStdDev no2Value no2StdDev"
+ */
+ private String generateSensorData() {
+ // Generate gradual changes to make data more realistic
+ mCurrentTemp = generateGradualChange(mCurrentTemp, TEMP_MIN, TEMP_MAX, 2.0f);
+ mCurrentHumidity = generateGradualChange(mCurrentHumidity, HUMIDITY_MIN, HUMIDITY_MAX, 5.0f);
+ mCurrentCO = generateGradualChange(mCurrentCO, CO_MIN, CO_MAX, 3.0f);
+ mCurrentNO2 = generateGradualChange(mCurrentNO2, NO2_MIN, NO2_MAX, 0.2f);
+
+ // Add some random noise
+ float tempReading = mCurrentTemp + (mRandom.nextFloat() - 0.5f) * 1.0f;
+ float humidityReading = mCurrentHumidity + (mRandom.nextFloat() - 0.5f) * 2.0f;
+ float coReading = mCurrentCO + (mRandom.nextFloat() - 0.5f) * 1.0f;
+ float no2Reading = mCurrentNO2 + (mRandom.nextFloat() - 0.5f) * 0.1f;
+
+ // Ensure values stay within realistic bounds
+ tempReading = Math.max(TEMP_MIN, Math.min(TEMP_MAX, tempReading));
+ humidityReading = Math.max(HUMIDITY_MIN, Math.min(HUMIDITY_MAX, humidityReading));
+ coReading = Math.max(CO_MIN, Math.min(CO_MAX, coReading));
+ no2Reading = Math.max(NO2_MIN, Math.min(NO2_MAX, no2Reading));
+
+ // Format data string (matching expected format from real Bluetooth device)
+ // Format: "timestamp tempValue tempStdDev humValue humStdDev coValue coStdDev no2Value no2StdDev"
+ String simulatedData = String.format("SimBT %.1f 0.1 %.1f 0.1 %.1f 0.1 %.2f 0.01",
+ tempReading, humidityReading, coReading, no2Reading);
+
+ Log.d(TAG, "Generated sensor data: " + simulatedData);
+ return simulatedData;
+ }
+
+ /**
+ * Generates gradual changes to sensor values for more realistic simulation
+ */
+ private float generateGradualChange(float currentValue, float min, float max, float maxChange) {
+ // Random walk with bounds
+ float change = (mRandom.nextFloat() - 0.5f) * maxChange;
+ float newValue = currentValue + change;
+
+ // Keep within bounds
+ newValue = Math.max(min, Math.min(max, newValue));
+
+ return newValue;
+ }
+
+ public void cleanup() {
+ stopSimulation();
+ mHandler = null;
+ mDataCallback = null;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/pyrrha_platform/simulation/SensorDataSimulator.java b/app/src/main/java/org/pyrrha_platform/simulation/SensorDataSimulator.java
new file mode 100644
index 0000000..6becc32
--- /dev/null
+++ b/app/src/main/java/org/pyrrha_platform/simulation/SensorDataSimulator.java
@@ -0,0 +1,249 @@
+package org.pyrrha_platform.simulation;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.pyrrha_platform.DeviceDashboard;
+import org.pyrrha_platform.PyrrhaApplication;
+import org.pyrrha_platform.ble.BluetoothLeService;
+import org.pyrrha_platform.iot.IoTClient;
+import org.pyrrha_platform.utils.Constants;
+import org.pyrrha_platform.utils.MessageFactory;
+import org.pyrrha_platform.utils.MyIoTActionListener;
+import org.pyrrha_platform.utils.PyrrhaEvent;
+import org.pyrrha_platform.galaxy.ProviderService;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Random;
+import java.util.TimeZone;
+
+/**
+ * Simulates Prometeo device sensor readings and publishes them to MQTT
+ * This class generates realistic sensor data every 2 seconds:
+ * - Temperature: 20-40°C (realistic firefighter environment)
+ * - Humidity: 30-90% (varying conditions)
+ * - CO: 0-50 ppm (normal to elevated levels)
+ * - NO2: 0-2 ppm (normal to slightly elevated)
+ */
+public class SensorDataSimulator {
+ private static final String TAG = SensorDataSimulator.class.getSimpleName();
+ private static final int UPDATE_INTERVAL_MS = 2000; // 2 seconds
+
+ private final Context context;
+ private final String deviceName;
+ private final String firefighterId;
+ private final Handler handler;
+ private final Random random;
+ private final SimpleDateFormat dateFormatter;
+
+ private boolean isRunning = false;
+ private Runnable simulationRunnable;
+
+ // Sensor reading ranges for realistic Prometeo device simulation
+ private static final float TEMP_MIN = 20.0f;
+ private static final float TEMP_MAX = 40.0f;
+ private static final float HUMIDITY_MIN = 30.0f;
+ private static final float HUMIDITY_MAX = 90.0f;
+ private static final float CO_MIN = 0.0f;
+ private static final float CO_MAX = 50.0f;
+ private static final float NO2_MIN = 0.0f;
+ private static final float NO2_MAX = 2.0f;
+
+ // Callbacks for UI updates
+ public interface SensorDataCallback {
+ void onSensorDataReceived(String sensorData);
+ void onMqttDataSent(PyrrhaEvent event);
+ void onGalaxyWatchDataSent(float temperature, float humidity, float co, float no2);
+ }
+
+ private SensorDataCallback callback;
+
+ public SensorDataSimulator(Context context, String deviceName, String firefighterId) {
+ this.context = context;
+ this.deviceName = deviceName;
+ this.firefighterId = firefighterId;
+ this.handler = new Handler(Looper.getMainLooper());
+ this.random = new Random();
+
+ // Initialize date formatter for UTC timestamps
+ this.dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+ this.dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ public void setCallback(SensorDataCallback callback) {
+ this.callback = callback;
+ }
+
+ /**
+ * Start generating sensor data every 2 seconds
+ */
+ public void startSimulation() {
+ if (isRunning) {
+ Log.w(TAG, "Simulation already running");
+ return;
+ }
+
+ isRunning = true;
+ Log.i(TAG, "Starting sensor simulation for device: " + deviceName);
+
+ simulationRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (isRunning) {
+ generateAndPublishSensorData();
+ handler.postDelayed(this, UPDATE_INTERVAL_MS);
+ }
+ }
+ };
+
+ handler.post(simulationRunnable);
+ }
+
+ /**
+ * Stop the sensor data simulation
+ */
+ public void stopSimulation() {
+ isRunning = false;
+ if (simulationRunnable != null) {
+ handler.removeCallbacks(simulationRunnable);
+ simulationRunnable = null;
+ }
+ Log.i(TAG, "Stopped sensor simulation for device: " + deviceName);
+ }
+
+ /**
+ * Generate realistic sensor readings and publish to all endpoints
+ */
+ private void generateAndPublishSensorData() {
+ try {
+ // Generate realistic sensor readings
+ float temperature = generateReading(TEMP_MIN, TEMP_MAX, 1); // 1 decimal place
+ float humidity = generateReading(HUMIDITY_MIN, HUMIDITY_MAX, 1);
+ float co = generateReading(CO_MIN, CO_MAX, 2); // 2 decimal places for gas concentration
+ float no2 = generateReading(NO2_MIN, NO2_MAX, 2);
+
+ // Create formatted sensor data string matching Prometeo device format
+ String sensorData = String.format("TEMP %.1f STDEV 0.1 HUM %.1f STDEV 0.1 CO %.2f STDEV 0.01 NO2 %.2f STDEV 0.01",
+ temperature, humidity, co, no2);
+
+ Log.d(TAG, "Generated sensor data: " + sensorData);
+
+ // 1. Update UI through callback (simulates Bluetooth data reception)
+ if (callback != null) {
+ callback.onSensorDataReceived(sensorData);
+ }
+
+ // 2. Send data directly to MQTT (same as DeviceDashboard does)
+ publishToMqtt(temperature, humidity, co, no2);
+
+ // 3. Send data to Galaxy Watch
+ sendToGalaxyWatch(temperature, humidity, co, no2);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error generating sensor data: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Generate a random reading within specified range with given precision
+ */
+ private float generateReading(float min, float max, int decimalPlaces) {
+ float value = min + random.nextFloat() * (max - min);
+ float multiplier = (float) Math.pow(10, decimalPlaces);
+ return Math.round(value * multiplier) / multiplier;
+ }
+
+ /**
+ * Publish sensor data to MQTT using the same logic as DeviceDashboard
+ */
+ private void publishToMqtt(float temperature, float humidity, float co, float no2) {
+ try {
+ PyrrhaApplication app = (PyrrhaApplication) context.getApplicationContext();
+ IoTClient iotClient = IoTClient.getInstance(context);
+
+ Date timestamp = new Date();
+
+ // Create PyrrhaEvent with sensor data
+ PyrrhaEvent event = new PyrrhaEvent();
+ event.setFirefighter_id(firefighterId);
+ event.setDevice_id(deviceName);
+ event.setDevice_battery_level("85"); // Simulated battery level
+ event.setTemperature(temperature);
+ event.setHumidity(humidity);
+ event.setCarbon_monoxide(co);
+ event.setNitrogen_dioxide(no2);
+ event.setFormaldehyde(0.0f);
+ event.setAcrolein(0.0f);
+ event.setBenzene(0.0f);
+ event.setDevice_timestamp(dateFormatter.format(timestamp));
+
+ // Create message and publish
+ String messageData = MessageFactory.getPyrrhaDeviceMessage(event);
+ Log.d(TAG, "Publishing to MQTT: " + messageData);
+
+ MyIoTActionListener listener = new MyIoTActionListener(context, Constants.ActionStateStatus.PUBLISH);
+ iotClient.publishEvent(Constants.TEXT_EVENT, "json", messageData, 0, false, listener);
+
+ // Update publish count
+ int count = app.getPublishCount();
+ app.setPublishCount(++count);
+
+ // Notify callback
+ if (callback != null) {
+ callback.onMqttDataSent(event);
+ }
+
+ Log.d(TAG, "Successfully published sensor data to MQTT");
+
+ } catch (MqttException e) {
+ Log.e(TAG, "Failed to publish to MQTT: " + e.getMessage(), e);
+ } catch (Exception e) {
+ Log.e(TAG, "Unexpected error publishing to MQTT: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Send sensor data to Galaxy Watch via ProviderService
+ */
+ private void sendToGalaxyWatch(float temperature, float humidity, float co, float no2) {
+ try {
+ // This would typically be called through the DeviceDashboard's ProviderService
+ // For simulation, we'll just log and notify callback
+ Log.d(TAG, "Sending to Galaxy Watch: T=" + temperature + "°C, H=" + humidity + "%, CO=" + co + "ppm, NO2=" + no2 + "ppm");
+
+ if (callback != null) {
+ callback.onGalaxyWatchDataSent(temperature, humidity, co, no2);
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error sending to Galaxy Watch: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Simulate a Bluetooth data broadcast intent (for integration with existing code)
+ */
+ private void sendBluetoothSimulationIntent(String sensorData) {
+ Intent intent = new Intent(BluetoothLeService.ACTION_DATA_AVAILABLE);
+ intent.putExtra(BluetoothLeService.EXTRA_DATA, sensorData);
+ context.sendBroadcast(intent);
+ Log.d(TAG, "Sent Bluetooth simulation broadcast: " + sensorData);
+ }
+
+ public boolean isRunning() {
+ return isRunning;
+ }
+
+ public String getDeviceName() {
+ return deviceName;
+ }
+
+ public String getFirefighterId() {
+ return firefighterId;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/pyrrha_platform/simulation/SimulationTestActivity.java b/app/src/main/java/org/pyrrha_platform/simulation/SimulationTestActivity.java
new file mode 100644
index 0000000..764f2ce
--- /dev/null
+++ b/app/src/main/java/org/pyrrha_platform/simulation/SimulationTestActivity.java
@@ -0,0 +1,167 @@
+package org.pyrrha_platform.simulation;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import org.pyrrha_platform.R;
+import org.pyrrha_platform.utils.PyrrhaEvent;
+
+/**
+ * Test activity to demonstrate the SensorDataSimulator functionality
+ * This simulates a Prometeo device connected via Bluetooth and shows:
+ * 1. Sensor readings every 2 seconds
+ * 2. Data display on mobile app
+ * 3. MQTT publishing to IoT platform
+ * 4. Samsung Galaxy Watch integration
+ */
+public class SimulationTestActivity extends AppCompatActivity implements SensorDataSimulator.SensorDataCallback {
+ private static final String TAG = SimulationTestActivity.class.getSimpleName();
+
+ private SensorDataSimulator simulator;
+ private TextView statusText;
+ private TextView sensorDataText;
+ private TextView mqttStatusText;
+ private TextView watchStatusText;
+ private Button startButton;
+ private Button stopButton;
+
+ private int dataUpdateCount = 0;
+ private int mqttPublishCount = 0;
+ private int watchUpdateCount = 0;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_simulation_test);
+
+ initializeViews();
+ initializeSimulator();
+ updateUI();
+ }
+
+ private void initializeViews() {
+ statusText = findViewById(R.id.status_text);
+ sensorDataText = findViewById(R.id.sensor_data_text);
+ mqttStatusText = findViewById(R.id.mqtt_status_text);
+ watchStatusText = findViewById(R.id.watch_status_text);
+ startButton = findViewById(R.id.start_button);
+ stopButton = findViewById(R.id.stop_button);
+
+ startButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startSimulation();
+ }
+ });
+
+ stopButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ stopSimulation();
+ }
+ });
+ }
+
+ private void initializeSimulator() {
+ // Initialize simulator with realistic Prometeo device settings
+ String deviceName = "Prometeo:00:00:00:00:00:01";
+ String firefighterId = "firefighter_1";
+
+ simulator = new SensorDataSimulator(this, deviceName, firefighterId);
+ simulator.setCallback(this);
+
+ Log.i(TAG, "Initialized SensorDataSimulator for device: " + deviceName);
+ }
+
+ private void startSimulation() {
+ if (!simulator.isRunning()) {
+ simulator.startSimulation();
+ Toast.makeText(this, "Started Prometeo device simulation", Toast.LENGTH_SHORT).show();
+ Log.i(TAG, "Started sensor simulation");
+ } else {
+ Toast.makeText(this, "Simulation already running", Toast.LENGTH_SHORT).show();
+ }
+ updateUI();
+ }
+
+ private void stopSimulation() {
+ if (simulator.isRunning()) {
+ simulator.stopSimulation();
+ Toast.makeText(this, "Stopped Prometeo device simulation", Toast.LENGTH_SHORT).show();
+ Log.i(TAG, "Stopped sensor simulation");
+ } else {
+ Toast.makeText(this, "Simulation not running", Toast.LENGTH_SHORT).show();
+ }
+ updateUI();
+ }
+
+ private void updateUI() {
+ boolean isRunning = simulator != null && simulator.isRunning();
+
+ statusText.setText(isRunning ? "Status: RUNNING" : "Status: STOPPED");
+ startButton.setEnabled(!isRunning);
+ stopButton.setEnabled(isRunning);
+
+ if (!isRunning) {
+ sensorDataText.setText("Sensor Data: Waiting...");
+ mqttStatusText.setText("MQTT: Ready");
+ watchStatusText.setText("Galaxy Watch: Ready");
+ }
+ }
+
+ // SensorDataCallback implementation
+
+ @Override
+ public void onSensorDataReceived(String sensorData) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ dataUpdateCount++;
+ sensorDataText.setText("Sensor Data (#" + dataUpdateCount + "):\n" + sensorData);
+ Log.d(TAG, "Received sensor data: " + sensorData);
+ }
+ });
+ }
+
+ @Override
+ public void onMqttDataSent(PyrrhaEvent event) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mqttPublishCount++;
+ mqttStatusText.setText("MQTT: Published #" + mqttPublishCount +
+ "\nLast: " + event.getDevice_timestamp() +
+ "\nCO: " + event.getCarbon_monoxide() + "ppm, NO2: " + event.getNitrogen_dioxide() + "ppm");
+ Log.d(TAG, "Published to MQTT: " + event.getFirefighter_id());
+ }
+ });
+ }
+
+ @Override
+ public void onGalaxyWatchDataSent(float temperature, float humidity, float co, float no2) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ watchUpdateCount++;
+ watchStatusText.setText("Galaxy Watch: Update #" + watchUpdateCount +
+ "\nT: " + temperature + "°C, H: " + humidity + "%" +
+ "\nCO: " + co + "ppm, NO2: " + no2 + "ppm");
+ Log.d(TAG, "Sent to Galaxy Watch: T=" + temperature + ", CO=" + co);
+ }
+ });
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (simulator != null && simulator.isRunning()) {
+ simulator.stopSimulation();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_test.xml b/app/src/main/res/drawable/ic_test.xml
new file mode 100644
index 0000000..7c14d75
--- /dev/null
+++ b/app/src/main/res/drawable/ic_test.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_test_active.xml b/app/src/main/res/drawable/ic_test_active.xml
new file mode 100644
index 0000000..dd77b4c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_test_active.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_device_dashboard.xml b/app/src/main/res/layout/activity_device_dashboard.xml
index 2222893..8396ceb 100644
--- a/app/src/main/res/layout/activity_device_dashboard.xml
+++ b/app/src/main/res/layout/activity_device_dashboard.xml
@@ -200,6 +200,18 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider2" />
+
+
-
+ app:layout_constraintStart_toStartOf="parent">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pyrrha.properties.template b/pyrrha.properties.template
index 0a41557..9495322 100644
--- a/pyrrha.properties.template
+++ b/pyrrha.properties.template
@@ -1,6 +1,6 @@
FLAVOR_APP_ID=org.pyrrha.platform
-FLAVOR_IOT_ORGANIZATION=
-FLAVOR_IOT_TOKEN=
+FLAVOR_IOT_ORGANIZATION=local
+FLAVOR_IOT_TOKEN=password
FLAVOR_DEVICE_TYPE=PyrrhaDevice
FLAVOR_DEVICE_UUID_SERVICE=
FLAVOR_DEVICE_UUID_CHARACTERISTIC=
@@ -8,7 +8,7 @@ FLAVOR_DEVICE_UUID_DATE_TIME=
FLAVOR_DEVICE_UUID_STATUS_CLOUD=
FLAVOR_SENSORS_MEASUREMENT=
FLAVOR_SENSORS_MEASUREMENT_LABEL=Pyrrha Device Service
-FLAVOR_DEVICE_SERVICE=
+FLAVOR_DEVICE_SERVICE=http://localhost:5000
FLAVOR_DEVICE_SERVICE_LABEL=Pyrrha Device Measurement
-FLAVOR_RULES_DECISION_SERVICE=
+FLAVOR_RULES_DECISION_SERVICE=http://localhost:8080
FLAVOR_APP_ID_SERVICE_TENANT=
\ No newline at end of file
diff --git a/screenshot.png b/screenshot.png
new file mode 100644
index 0000000..fbe64fe
Binary files /dev/null and b/screenshot.png differ