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" /> +