diff --git a/CMakeLists.txt b/CMakeLists.txt index 45891ca..2b80b0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,9 +5,9 @@ project(waterLevelSensor VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTORCC ON) -find_package(Qt6 REQUIRED COMPONENTS Quick Network) +find_package(Qt6 REQUIRED COMPONENTS Quick Network CorePrivate) -qt_standard_project_setup(REQUIRES 6.9) +qt_standard_project_setup(REQUIRES 6.11) qt_add_executable(appwaterLevelSensor main.cpp @@ -16,10 +16,22 @@ qt_add_executable(appwaterLevelSensor if(ANDROID) + +include(FetchContent) +FetchContent_Declare( + android_openssl + DOWNLOAD_EXTRACT_TIMESTAMP true + URL https://github.com/KDAB/android_openssl/archive/refs/heads/master.zip +) +FetchContent_MakeAvailable(android_openssl) +include(${android_openssl_SOURCE_DIR}/android_openssl.cmake) + qt_add_library(service SHARED servicemain.cpp service.h service.cpp ) + + set_property(TARGET appwaterLevelSensor APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android) set_property(TARGET service APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR @@ -29,8 +41,8 @@ set_property(TARGET service APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR target_link_libraries(service PRIVATE Qt::Core - Qt::CorePrivate Qt::Network + Qt::CorePrivate Qt::Quick ) qt_add_qml_module(appwaterLevelSensor @@ -45,6 +57,7 @@ qt_add_qml_module(appwaterLevelSensor SOURCES service.h RESOURCES README.md ) +add_android_openssl_libraries(service appwaterLevelSensor) target_link_libraries(appwaterLevelSensor PRIVATE Qt6::Quick Qt6::Network service ) diff --git a/Main.qml b/Main.qml index 66f221e..e7844b9 100644 --- a/Main.qml +++ b/Main.qml @@ -6,19 +6,62 @@ import QtQuick.Controls.Material Window { width: 640 - height: 480 + height: 1080 visible: true title: qsTr("Hello World") id: root property color accent_color: "#F05FAF5F" + property color dark_accent_color: "#FF5F7F5F" property color danger_color: "#F0AF2F1F" property color water_color: "#F00F7FAF" Material.accent: root.accent_color property int font_enlarged_text_size: 20 - property int font_normal_text_size: 18 + property int font_normal_text_size: 16 property double distance: 0 property int distance_count: 0 + property int distance_count_thres: 3 + property string mode: "http" // visibility: Window.FullScreen + Component.onCompleted: { + loading_popup.open(); + } + + + Popup { + id: loading_popup + background: Rectangle { + color: "#B0121212" + radius: 5 + border.color: root.accent_color + border.width: 1 + } + height: parent.height / 10 + width: parent.width / 1.1 + anchors.centerIn: Overlay.overlay + closePolicy: Popup.NoAutoClose + BusyIndicator { + id: loading_indicator + running: true + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + } + } + Text { + id: loading_text + text: "Loading..." + anchors { + leftMargin: 20 + left: loading_indicator.right + verticalCenter: parent.verticalCenter + } + verticalAlignment: Text.AlignVCenter + color: root.accent_color + font.pointSize: root.font_enlarged_text_size + } + + } + Rectangle { anchors.fill: parent id: background @@ -145,8 +188,8 @@ Window { border.color: root.accent_color border.width: 1 } - height: parent.height / 1.5 - width: parent.width / 1.5 + height: Math.max(parent.height / 1.2, contentHeight) + width: parent.width / 1.1 anchors.centerIn: Overlay.overlay @@ -176,24 +219,26 @@ Window { topMargin: 20 top: settings_label.bottom left: parent.left + verticalCenter: max_thres_input.verticalCenter } height: max_thres_input.height verticalAlignment: Text.AlignVCenter color: root.accent_color - font.pixelSize: root.font_normal_text_size + font.pointSize: root.font_normal_text_size } TextField { id: max_thres_input text: config.max anchors { - left: max_thres_label.right - leftMargin: 5 + right: parent.right + rightMargin: 5 top: settings_label.bottom topMargin: 20 } + width: parent.width / 2 color: root.accent_color - font.pixelSize: root.font_normal_text_size + font.pointSize: root.font_normal_text_size } @@ -204,24 +249,26 @@ Window { topMargin: 20 top: max_thres_label.bottom left: parent.left + verticalCenter: min_thres_input.verticalCenter } height: min_thres_input.height verticalAlignment: Text.AlignVCenter color: root.accent_color - font.pixelSize: root.font_normal_text_size + font.pointSize: root.font_normal_text_size } TextField { id: min_thres_input text: config.min anchors { - left: min_thres_label.right - leftMargin: 5 + right: parent.right + rightMargin: 5 top: max_thres_label.bottom topMargin: 20 } + width: parent.width / 2 color: root.accent_color - font.pixelSize: root.font_normal_text_size + font.pointSize: root.font_normal_text_size } @@ -232,24 +279,132 @@ Window { topMargin: 20 top: min_thres_label.bottom left: parent.left + verticalCenter: alert_thres_input.verticalCenter } height: alert_thres_input.height verticalAlignment: Text.AlignVCenter color: root.accent_color - font.pixelSize: root.font_normal_text_size + font.pointSize: root.font_normal_text_size } TextField { id: alert_thres_input text: config.alert anchors { - left: alert_thres_label.right - leftMargin: 5 + right: parent.right + rightMargin: 5 top: min_thres_label.bottom topMargin: 20 } + width: parent.width / 2 color: root.accent_color - font.pixelSize: root.font_normal_text_size + font.pointSize: root.font_normal_text_size + } + CheckBox { + id: flip_bits_box + text: "Flip Bits" + checked: config.flipBits + anchors { + topMargin: 20 + top: alert_thres_label.bottom + horizontalCenter: parent.horizontalCenter + } + contentItem: Text { + text: flip_bits_box.text + font.pointSize: root.font_normal_text_size + color: root.accent_color + leftPadding: flip_bits_box.indicator.width + flip_bits_box.spacing + } + } + + Text { + id: settings_mode_text + anchors { + topMargin: 20 + top: flip_bits_box.bottom + left: parent.left + } + font.pointSize: root.font_normal_text_size + color: root.accent_color + text: "Mode: " + } + Row { + id: mode_row + anchors { + left: settings_mode_text.right + right: parent.right + verticalCenter: settings_mode_text.verticalCenter + leftMargin: 10 + } + RadioButton { + id: udp_radio_button + checked: config.mode === "udp" ? true : false + text: "UDP" + font.pointSize: root.font_normal_text_size + contentItem: Text { + text: udp_radio_button.text + font: udp_radio_button.font + opacity: enabled ? 1.0 : 0.3 + color: udp_radio_button.down ? root.dark_accent_color : root.accent_color + verticalAlignment: Text.AlignVCenter + leftPadding: udp_radio_button.indicator.width + udp_radio_button.spacing + } + onCheckedChanged: { + if(checked) { + root.mode = "udp"; + } + } + } + RadioButton { + id: http_radio_button + checked: config.mode === "http" ? true : false + text: "HTTP" + font.pointSize: root.font_normal_text_size + contentItem: Text { + text: http_radio_button.text + font: http_radio_button.font + opacity: enabled ? 1.0 : 0.3 + color: http_radio_button.down ? root.dark_accent_color : root.accent_color + verticalAlignment: Text.AlignVCenter + leftPadding: http_radio_button.indicator.width + http_radio_button.spacing + } + onCheckedChanged: { + if(checked) { + root.mode = "http"; + } + } + } + } + Text { + id: http_url_label + text: "Url: " + anchors { + topMargin: 20 + top: settings_mode_text.bottom + left: parent.left + verticalCenter: http_url_input.verticalCenter + } + height: http_url_input.height + verticalAlignment: Text.AlignVCenter + color: root.accent_color + font.pointSize: root.font_normal_text_size + } + + TextField { + id: http_url_input + text: config.url + anchors { + right: parent.right + rightMargin: 20 + leftMargin: 20 + top: mode_row.bottom + topMargin: 20 + } + enabled: root.mode === "http" ? true : false + width: Math.max(parent.width / 2, parent.width - http_url_label.width - anchors.leftMargin) + color: root.accent_color + font.pointSize: root.font_normal_text_size + cursorPosition: 0 } Text { @@ -259,7 +414,9 @@ Window { visible: settings_error_text === "" ? false : true anchors { topMargin: 20 - top: alert_thres_label.bottom + top: http_url_label.bottom + bottom: reset_averages.top + bottomMargin: 10 left: parent.left horizontalCenter: parent.horizontalCenter } @@ -268,7 +425,52 @@ Window { font.pixelSize: root.font_normal_text_size } + Rectangle { + id: reset_averages + anchors { + topMargin: 10 + bottom: save_config.top + bottomMargin: 10 + horizontalCenter: parent.horizontalCenter + } + color: "#F0121212" + width: parent.width / 1.3 + height: 60 + border { + width: 1 + color: root.accent_color + } + radius: 3 + Text { + anchors.centerIn: parent + text: "Reset Averages" + color: root.accent_color + font.pointSize: root.font_enlarged_text_size + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + property color settings_rect_pressed_prev: "#00000000" + property color settings_rect_entered_prev: "#00000000" + onEntered: { + settings_rect_entered_prev = reset_averages.color + reset_averages.color = "#5A1F1F1F" + } + onExited: { + reset_averages.color = settings_rect_entered_prev + } + onPressed: { + settings_rect_pressed_prev = reset_averages.color + reset_averages.color = "#5A1F4F1F" + listener.resetAverages(); + } + onReleased: { + reset_averages.color = settings_rect_pressed_prev + } + } + + } Rectangle { id: save_config @@ -278,7 +480,7 @@ Window { horizontalCenter: parent.horizontalCenter } color: "#F0121212" - width: parent.width / 2.5 + width: parent.width / 1.3 height: 60 border { width: 1 @@ -310,7 +512,10 @@ Window { let max = parseFloat(min_thres_input.text); let min = parseFloat(min_thres_input.text); let alert = parseFloat(alert_thres_input.text); - console.log("Parsed values:", max, min, alert); + let flipBits = flip_bits_box.checked; + let mode = root.mode; + let url = http_url_input.text; + console.log("Parsed values:", max, min, alert, flip_bits_box.checked); if(max < min) { settings_error_label.settings_error_text = "Max cannot be less than min"; return; @@ -319,20 +524,19 @@ Window { settings_error_label.settings_error_text = "Alert must be between 0 to 100"; return; } - - let current_min_reading = Math.max(0, (parseFloat(min_thres_input.text))); let effective_max_reading = Math.max(0, (parseFloat(max_thres_input.text)) - current_min_reading); let alert_effective_reading = (effective_max_reading * (100 - alert) / 100) + current_min_reading; console.log("Alert effective reading is: " + alert_effective_reading + " for value: " + alert); - listener.setAlert(alert_effective_reading); - config.saveConfig(max_thres_input.text, min_thres_input.text, alert_thres_input.text); + listener.setConfig(alert_effective_reading, flipBits, mode, url); + config.saveConfig(max_thres_input.text, min_thres_input.text, alert_thres_input.text, flip_bits_box.checked, mode, url); settings_popup.close(); } onReleased: { save_config.color = settings_rect_pressed_prev } } + } } } @@ -354,7 +558,7 @@ Window { id: tank_base color: "#0FFFFFFF" anchors.centerIn: parent - width: parent.width / 2.5 + width: parent.width / 2 height: parent.height / 2 border.color: "#F0121212" border.width: 5 @@ -410,6 +614,20 @@ Window { color: root.accent_color font.pixelSize: root.font_normal_text_size } + Text { + id: last_updated_text + property string last_updated: "" + text: "Last Updated On Server On: " + last_updated + anchors { + top: info_percent_text.bottom + topMargin: 10 + horizontalCenter: parent.horizontalCenter + } + horizontalAlignment: Text.AlignHCenter + color: root.accent_color + font.pixelSize: root.font_normal_text_size + visible: config.mode === "http" ? true : false + } Text { id: error_text @@ -417,7 +635,7 @@ Window { text: "Error: " + error_string visible: error_string === "" ? false : true; anchors { - top: info_percent_text.bottom + top: last_updated_text.bottom topMargin: 10 horizontalCenter: parent.horizontalCenter } @@ -444,11 +662,40 @@ Window { ConfigInterface { id: config - onConfigData: (max, min, alert) => { - console.log("Got config data as: ", max, min, alert); + onConfigData: (max, min, alert, mode, url) => { + console.log("Got config data as: ", max, min, alert, mode, url); + } + onModeChanged: { + root.mode = mode; } } + Timer { + property int count: 0 + id: init_timer + running: true + repeat: true + interval: 1000 + onTriggered: { + if(count === 0) { + listener.startService(); + count++; + return; + } + else { + console.log("Timer triggered saving config, hope everything is loaded by now"); + let current_min_reading = Math.max(0, (parseFloat(min_thres_input.text))); + let effective_max_reading = Math.max(0, (parseFloat(max_thres_input.text)) - current_min_reading); + let alert = parseFloat(alert_thres_input.text); + let alert_effective_reading = (effective_max_reading * (100 - alert) / 100) + current_min_reading; + console.log("Setting initial configs: ", alert_effective_reading, config.flipBits , config.mode, config.url); + listener.setConfig(alert_effective_reading, config.flipBits , config.mode, config.url); + http_url_input.cursorPosition = 0; + init_timer.repeat = false; + init_timer.stop(); + } + } + } UDPListener { id: listener @@ -457,12 +704,29 @@ Window { error_text.error_string = err; } + onLastUpdatedChanged: (update, warn) => { + console.log("QML Last Updated: ", update); + if(!warn) + { + last_updated_text.last_updated = update; + last_updated_text.color = root.accent_color; + } + else + { + last_updated_text.last_updated = update; + last_updated_text.color = root.danger_color; + } + } + onDistanceChanged: { console.log("Distance changed"); root.distance = listener.distance; error_text.error_string = ""; console.log("Distance count: ", root.distance_count); - if(root.distance_count >= 5) { + if(root.distance_count >= root.distance_count_thres) { + if(loading_popup.opened) { + loading_popup.close(); + } status_label.text = "Recieving values"; let current_min_reading = Math.max(0, (parseFloat(min_thres_input.text))); let effective_max_reading = Math.max(0, (parseFloat(max_thres_input.text)) - current_min_reading); @@ -478,7 +742,9 @@ Window { else { water_level.color = root.water_color; } - + if(root.distance_count > 1000) { + root.distance_count = root.distance_count_thres; + } console.log("Current reading is: " + current_reading) console.log("Normalized height is: " + normalized_height) water_level.current_level = normalized_height; @@ -507,23 +773,29 @@ Window { let alert = parseFloat(alert_thres_input.text); let alert_effective_reading = (effective_max_reading * (100 - alert) / 100) + current_min_reading; console.log("Alert effective reading is: " + alert_effective_reading + " for value: " + alert); - listener.setAlert(alert_effective_reading); + listener.setConfig(alert_effective_reading, flip_bits_box.checked, root.mode, http_url_input.text); } distance_count += 1; } } - onClosing:(close)=> { + onClosing: (close)=> { if(Qt.platform.os === "android"){ - close.accepted = false + // close.accepted = false + // listener.moveAppToBackground(); + // console.log("Moving to backgound"); + close.accepted = true; } else { close.accepted = true } } function dataBlink() { - let old_color = status_indicator.color; + if(status_indicator_timer.running) { + status_indicator_timer.stop(); + } + let old_color = "#F05F5F0F"; let new_color = "#F04FAF5F"; status_indicator.color = new_color; status_indicator.old_color = old_color; diff --git a/README.md b/README.md index 96a9151..7198d51 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,13 @@ Builds for android and desktop.
On Android Creates a service from service.cpp and android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelService.java.
Changes in AndroidManifest.xml are important.
On Desktop just waits for values from a UDP Broadcast on local network with port 8176 and shows those values.
-On Android the service does that, and creates an alert notification if the value in broadcast is less than alert threshold.
\ No newline at end of file +On Android the service does that, and creates an alert notification if the value in broadcast is less than alert threshold.
+ + +
+Create a new socket in Worker that will instantly start listening.
+In the service launch the threads and immediately send client hello to the worker socket.
+On client hello send command data to the android service.
+On quit send server bye to android service.
+On server bye deinit the sockets that send data to cpp server.
+On next launch, the command and client threads can be reinitialized from the StartService java class.
\ No newline at end of file diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index d31ff4c..ead6548 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -11,7 +11,7 @@ - + - + android:foregroundServiceType="connectedDevice" + android:exported="true" + android:process=":water_service" + android:stopWithTask="false"> + diff --git a/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelService.java b/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelService.java index c868acc..cc765d0 100644 --- a/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelService.java +++ b/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelService.java @@ -19,39 +19,190 @@ import org.qtproject.qt.android.bindings.QtActivity; import android.app.Service; import java.time.Instant; import java.time.Duration; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONArray; +import android.widget.Toast; +import android.os.IBinder; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; public class WaterLevelService extends Service { private static float alert_thres_distance = 1000000; + private static boolean bits_flipped = false; + private static String mode = ""; + private static String url = ""; + private static Thread udpThread; + private static Thread cmdThread; + private static boolean resetBinds = false; + private final IBinder binder = new LocalBinder(); + final Messenger mMessenger = new Messenger(new IncomingHandler()); + public static final int MSG_RESTART_THREADS = 1; + + + class IncomingHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_RESTART_THREADS: + Log.d("UDP Service MainThread", "Received IPC request to restart threads"); + restartThreads(); + break; + default: + super.handleMessage(msg); + } + } + } + + + public synchronized void restartThreads() { + Log.d("UDP Service MainThread", "In synchronized thread stopper."); + udpThread.interrupt(); + cmdThread.interrupt(); + try { + udpThread.join(); + cmdThread.join(); + } catch (InterruptedException e) { + Log.d("UDP Service MainThread", "Thread joining was interrupted because of: " + e); + } + + new Thread(new + Runnable() { + @Override + public void run() { + try { + // Move your socket and thread starting logic here + if (udpThread == null || !udpThread.isAlive()) { + udpThread = new Thread(new UDPClient()); + udpThread.start(); + } + if (cmdThread == null || !cmdThread.isAlive()) { + cmdThread = new Thread(new UDPCommandClient()); + cmdThread.start(); + } + } catch (Exception e) { + Log.e("UDP Service MainThread", "Restarting the threads", e); + } + } + }).start(); + Log.d("UDP Service MainThread", "Actually restarting the thread"); + } + + public static synchronized boolean checkBound() { + return resetBinds; + } + + public static synchronized void setBound(boolean value) { + resetBinds = value; + } + + public class LocalBinder extends android.os.Binder { + WaterLevelService getService() { + return WaterLevelService.this; + } + } public class UDPCommandClient implements Runnable { private DatagramSocket socket_cmd_recv; private DatagramSocket socket_cmd_send; + private boolean received_cmd_first = false; + @Override public void run() { - Log.d("UDP Service", "Command thread initialising"); + Log.d("UDP Service CommandThread", "Command thread initialising"); try { - socket_cmd_recv = new DatagramSocket(8178, InetAddress.getByName("localhost")); - socket_cmd_send = new DatagramSocket(8179); - - Log.d("UDP Service", "Created command thread sockets"); + socket_cmd_recv = new DatagramSocket(null); + socket_cmd_send = new DatagramSocket(null); + socket_cmd_recv.setSoTimeout(4000); + socket_cmd_recv.setReuseAddress(true); + socket_cmd_send.setReuseAddress(true); + socket_cmd_recv.bind(new InetSocketAddress(8178)); + socket_cmd_send.bind(new InetSocketAddress(8187)); + boolean not_send_set = true; + Log.d("UDP Service CommandThread", "Created command thread sockets"); while (!Thread.currentThread().isInterrupted()) { + if(socket_cmd_recv.isClosed()) { + socket_cmd_recv.bind(new InetSocketAddress(8178)); + } + if(socket_cmd_send.isClosed()) { + socket_cmd_send.bind(new InetSocketAddress(8187)); + } + + if(not_send_set) { + String s_data = new String(""); + byte[] buff_s = s_data.getBytes(StandardCharsets.UTF_8); + DatagramPacket packet = new DatagramPacket(buff_s, buff_s.length, InetAddress.getByName("127.0.0.1"), 8186); + try { + socket_cmd_send.connect(InetAddress.getByName("127.0.0.1"), 8186); + socket_cmd_send.send(packet); + } catch(Exception e) { + Log.d("UDP Service CommandThread", "Failed to send data to client due to: " + e); + } + Log.d("UDP Service CommandThread", "Sent set message to client"); + } + byte[] buffer = new byte[1024]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); - socket_cmd_recv.receive(packet); + try { + socket_cmd_recv.receive(packet); + } catch(java.net.SocketTimeoutException e) { + continue; + } String str = new String(buffer, StandardCharsets.UTF_8); + Log.d("UDP Service CommandThread", "Received command packet " + str); + not_send_set = false; if(str.startsWith("", ""); alert_thres_distance = Float.parseFloat(str); - } - Log.d("UDP Service ", "Received command packet " + str); + { + if(!checkBound()) + { + setBound(true); + Log.d("UDP Service CommandThread", "Set Bound to True"); + } + else { + Log.d("UDP Service CommandThread", "Set Bound is False"); + } + } + } + else if(str.startsWith("", ""); + Log.d("UDP Service CommandThread", "Flipping " + str); + if(str.toLowerCase() == "false") { + bits_flipped = false; + } + else if(str.toLowerCase() == "true") { + bits_flipped = true; + } + } + else if(str.startsWith("", ""); + Log.d("UDP Service CommandThread", "Mode " + str); + mode = str.toLowerCase(); + } + else if(str.startsWith("", ""); + Log.d("UDP Service CommandThread", "Url " + str); + url = str.toLowerCase(); + } } } catch (IOException e) { - Log.e("UDP Service", "Socket Error", e); + // showToast("IOException: " + e); + Log.e("UDP Service CommandThread", "IOException Error" + e); } } @@ -61,13 +212,15 @@ public class WaterLevelService extends Service { private DatagramSocket socket_recv; private DatagramSocket socket_send; private Instant prevAlertTimestamp; + private InetAddress address; private boolean started = false; @Override public void run() { - Log.d("UDP Service", "Thread initialising"); + Log.d("UDP Service ClientThread", "Client Thread initialising"); android.net.wifi.WifiManager wifi = (android.net.wifi.WifiManager) getSystemService(Context.WIFI_SERVICE); android.net.wifi.WifiManager.MulticastLock multicastLock = wifi.createMulticastLock("water_level_lock"); + android.net.wifi.WifiManager.WifiLock wifiLock = wifi.createWifiLock(android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF, "WaterLevel:WifiLock"); android.os.PowerManager pm = (android.os.PowerManager) getSystemService(Context.POWER_SERVICE); android.os.PowerManager.WakeLock wakeLock = pm.newWakeLock(android.os.PowerManager.PARTIAL_WAKE_LOCK, "WaterLevel:WakeLock"); @@ -84,17 +237,144 @@ public class WaterLevelService extends Service { try { multicastLock.acquire(); wakeLock.acquire(); - socket_recv = new DatagramSocket(8176); - socket_send = new DatagramSocket(8177); + wifiLock.acquire(); + socket_recv = new DatagramSocket(null); + socket_send = new DatagramSocket(null); socket_recv.setBroadcast(true); - Log.d("UDP Service", "Created Sockets"); + socket_recv.setSoTimeout(4000); + socket_recv.setReuseAddress(true); + socket_send.setReuseAddress(true); + socket_send.bind(new InetSocketAddress(8177)); + socket_recv.bind(new InetSocketAddress(8176)); + Log.d("UDP Service ClientThread", "Created Sockets"); + int count = 0; while (!Thread.currentThread().isInterrupted()) { - byte[] buffer = new byte[4]; - DatagramPacket packet = new DatagramPacket(buffer, buffer.length); - socket_recv.receive(packet); - float f = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getFloat(); - Log.d("UDP Service ", "Received packet: " + f); - Log.d("UDP Service ", "Threshold is: " + alert_thres_distance); + count++; + if(count == 3) { + Log.i("UDP Service ClientThread", "Thread is running with mode: " + mode); + count = 0; + } + if(checkBound()) { + Log.i("UDP Service ClientThread", "Reconnecting sockets"); + this.address = InetAddress.getByName("127.0.0.1"); + socket_recv.disconnect(); + socket_send.disconnect(); + socket_recv.close(); + socket_send.close(); + socket_recv = null; + socket_send = null; + socket_recv = new DatagramSocket(null); + socket_send = new DatagramSocket(null); + socket_recv.setBroadcast(true); + socket_recv.setSoTimeout(4000); + socket_recv.setReuseAddress(true); + socket_send.setReuseAddress(true); + socket_send.bind(new InetSocketAddress(8177)); + socket_recv.bind(new InetSocketAddress(8176)); + setBound(false); + Log.i("UDP Service ClientThread", "Reconnected sockets"); + } + // if(checkBound()) { + // Log.i("UDP Service", "Reset socket binds: " + mode); + // socket_recv.close(); + // socket_send.close(); + // socket_send.bind(new InetSocketAddress(8177)); + // socket_recv.bind(new InetSocketAddress(8176)); + // setBound(false); + // if(socket_send.isBound()) { + // Log.i("UDP Service", "Rebound socket: " + 8177); + // } + // else { + // Log.i("UDP Service", "Failed to rebound socket: " + 8177); + // } + // if(socket_recv.isBound()) { + // Log.i("UDP Service", "Rebound socket: " + 8176); + // } + // else { + // Log.i("UDP Service", "Failed to rebound socket: " + 8176); + // } + + // } + // if(checkBound()) { + // Log.i("UDP Service", "Reset socket binds: " + mode); + // } + // else { + // Log.i("UDP Service", "Reset socket binds: " + mode); + // } + byte[] buffer = null; + String updated = ""; + if(mode.equals("udp")) { + try { + if(socket_recv.isClosed()) { + socket_recv.bind(new InetSocketAddress(8176)); + } + buffer = new byte[4]; + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + socket_recv.receive(packet); + } catch(java.net.SocketTimeoutException e) { + // showToast("Socket Timeout: " + e); + continue; + } + } + else if(mode.equals("http")) { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Log.d("UDP Service ClientThread", "Thread exiting due to interrupt."); + return; + } + Log.d("UDP Service ClientThread", "Http String Url: " + url); + URL t_url = new URL(url); + HttpURLConnection con = (HttpURLConnection) t_url.openConnection(); + con.setRequestMethod("GET"); + con.setConnectTimeout(5000); + con.setReadTimeout(5000); + con.setInstanceFollowRedirects(true); + int status = con.getResponseCode(); + Log.d("UDP Service ClientThread", "Got status from http: " + status); + if(status == 200) { + BufferedReader streamReader = new BufferedReader(new InputStreamReader(con.getInputStream())); + String data = streamReader.readLine(); + try { + JSONObject obj = new JSONObject(data); + JSONArray arr = obj.getJSONArray("water_level"); + float f = (float ) arr.getDouble(0); + updated = obj.getString("updated_on"); + buffer = ByteBuffer.allocate(4).putFloat(f).array(); + Log.d("UDP Service ClientThread", "Got value from http: " + f); + Log.d("UDP Service ClientThread", "Last updated on: " + updated); + } catch (JSONException e) { + Log.e("UDP Service ClientThread", "Invalid Json"); + // showToast("Invalid Json" + e); + continue; + } + } + else { + // showToast("Bad Response From Server: " + status); + InputStreamReader streamReader = new InputStreamReader(con.getErrorStream()); + continue; + } + } + else { + Log.d("UDP Service ClientThread", "Invalid mode: " + mode); + // showToast("Invalid mode: " + mode); + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + Log.d("UDP Service ClientThread", "Thread exiting due to interrupt."); + return; + } + continue; + } + float f; + if(bits_flipped) { + f = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getFloat(); + } + else { + f = ByteBuffer.wrap(buffer).order(ByteOrder.BIG_ENDIAN).getFloat(); + } + Log.d("UDP Service ClientThread", "Received packet: " + f); + Log.d("UDP Service ClientThread", "Threshold is: " + alert_thres_distance); boolean duration_passed = false; if(!started) { duration_passed = true; @@ -103,7 +383,7 @@ public class WaterLevelService extends Service { else { Instant currentTimestamp = Instant.now(); Duration duration = Duration.between(prevAlertTimestamp, currentTimestamp); - if(duration.toMinutes() > 30) { + if(duration.toMinutes() > 60) { duration_passed = true; } prevAlertTimestamp = currentTimestamp; @@ -113,18 +393,33 @@ public class WaterLevelService extends Service { .setSmallIcon(android.R.drawable.ic_dialog_alert) .setContentTitle("Water Level Sensor") .setContentText("Water Level is below alert threshold") - .setPriority(Notification.PRIORITY_HIGH); + .setPriority(Notification.PRIORITY_LOW); manager.notify(2, builder.build()); - Log.d("UDP Service", "Notification sent"); + Log.d("UDP Service ClientThread", "Notification sent"); + if(!started) { + started = true; + } } - DatagramPacket send_packet = new DatagramPacket(buffer, buffer.length, InetAddress.getByName("localhost"), 8180); + if(socket_send.isClosed()) { + socket_send.bind(new InetSocketAddress(8177)); + } + DatagramPacket send_packet = new DatagramPacket(buffer, buffer.length, InetAddress.getByName("127.0.0.1"), 8180); socket_send.send(send_packet); - Log.d("UDP Service ", "Sent packet"); - } + Log.d("UDP Service ClientThread", "Sent packet"); + if(mode.equals("http")) { + byte[] buff2 = updated.getBytes(StandardCharsets.UTF_8); + DatagramPacket send_packet_2 = new DatagramPacket(buff2, buff2.length, InetAddress.getByName("127.0.0.1"), 8185); + socket_send.send(send_packet_2); + Log.d("UDP Service ClientThread", "Sent last updated packet"); + } + } } catch (IOException e) { - Log.e("UDP Service", "Socket Error", e); + // showToast("IOException: " + e); + Log.e("UDP Service ClientThread", "IOException Error" + e); } finally { + if (wifiLock.isHeld()) + wifiLock.release(); if (multicastLock.isHeld()) multicastLock.release(); if (wakeLock.isHeld()) @@ -136,7 +431,7 @@ public class WaterLevelService extends Service { } public void startWaterLevelService() { - Log.d("UDP Service", "Starting the service from Activity"); + Log.d("UDP Service MainThread", "Starting the service from Activity"); Intent intent = new Intent(this, WaterLevelService.class); @@ -144,25 +439,25 @@ public class WaterLevelService extends Service { } public WaterLevelService() { super(); - Log.i("UDP Service", "Class is created"); + Log.i("UDP Service MainThread", "Class is created"); } @Override - public android.os.IBinder onBind(android.content.Intent intent) { return null; } + public android.os.IBinder onBind(android.content.Intent intent) { return mMessenger.getBinder(); } @Override public void onCreate() { super.onCreate(); Notification notification = createNotification(); startForeground(1, notification); - Log.i("UDP Service", "Creating Service"); + Log.i("UDP Service MainThread", "Creating Service"); } @Override public void onDestroy() { super.onDestroy(); - Log.i("UDP Service", "Destroying Service"); + Log.i("UDP Service MainThread", "Destroying Service"); } @Override @@ -170,16 +465,47 @@ public class WaterLevelService extends Service { Notification notification = createNotification(); startForeground(1, notification); - Log.d("UDP Service", "Actually starting the thread"); + + if (intent == null) { + Log.w("UDP Service MainThread", "Service restarted with null intent. Stopping to avoid crash."); + stopSelf(); + return START_NOT_STICKY; + } + new Thread(new + Runnable() { + @Override + public void run() { + try { + // Move your socket and thread starting logic here + if (udpThread == null || !udpThread.isAlive()) { + udpThread = new Thread(new UDPClient()); + udpThread.start(); + } + if (cmdThread == null || !cmdThread.isAlive()) { + cmdThread = new Thread(new UDPCommandClient()); + cmdThread.start(); + } + } catch (Exception e) { + Log.e("UDP Service MainThread", "Error starting threads", e); + } + } + }).start(); + + Log.d("UDP Service MainThread", "Actually starting the thread"); int ret = super.onStartCommand(intent, flags, startId); - (new Thread(new UDPClient())).start(); - (new Thread(new UDPCommandClient())).start(); + // (new Thread(new UDPClient())).start(); + // (new Thread(new UDPCommandClient())).start(); - return START_STICKY; + return START_NOT_STICKY; } + public void showToast(String str) { + int duration = Toast.LENGTH_SHORT; + Toast toast = Toast.makeText(this, str, duration); + toast.show(); + } private Notification createNotification() { String channelId = "water_level_channel"; @@ -202,7 +528,7 @@ public class WaterLevelService extends Service { } public static void startQtAndroidService(QtActivity activity) { - Log.d("UDP Service", "Starting the service"); + Log.d("UDP Service MainThread", "Starting the service"); Intent intent = new Intent(activity, WaterLevelService.class); activity.startForegroundService(intent); diff --git a/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelServiceStarter.java b/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelServiceStarter.java index 98b114c..9dccce9 100644 --- a/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelServiceStarter.java +++ b/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelServiceStarter.java @@ -5,15 +5,114 @@ import android.content.Context; import android.content.Intent; import android.os.Build; import android.util.Log; +import android.app.ActivityManager; +import android.app.ActivityManager.RunningServiceInfo; import org.qtproject.qt.android.bindings.QtActivity; +import android.os.Handler; +import android.os.Looper; +import android.content.ServiceConnection; +import android.content.ComponentName; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; public class WaterLevelServiceStarter { + private static Messenger mServiceMessenger = null; + private static boolean mBound = false; + private static boolean restart_bind = false; + + private static ServiceConnection connection = new ServiceConnection() { + + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + mServiceMessenger = new Messenger(service); + mBound = true; + Log.d("UDP Service StarterThread", "Connected to Service object reference"); + if(restart_bind) + { + sendRestartMessage(); + Log.d("UDP Service StarterThread", "Restart Threads in Service."); + } + restart_bind = false; + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + mServiceMessenger = null; + mBound = false; + } + }; + + public static void startAndBindService(Context context) { + Intent intent = new Intent(context, WaterLevelService.class); + context.startForegroundService(intent); + context.bindService(intent, connection, Context.BIND_AUTO_CREATE); + } + + public static void bindService(Context context) { + restart_bind = true; + Intent intent = new Intent(context, WaterLevelService.class); + context.bindService(intent, connection, Context.BIND_AUTO_CREATE); + } + + private static boolean isMyServiceRunning(Class serviceClass, Context context) { + ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { + if (serviceClass.getName().equals(service.service.getClassName())) { + return true; + } + } + return false; + } + + + private static int getServicePid(Class serviceClass, Context context) { + ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { + if (serviceClass.getName().equals(service.service.getClassName())) { + return service.pid; + } + } + return 0; + } + + + public static void sendRestartMessage() { + if (!mBound || mServiceMessenger == null) { + Log.d("UDP Service StarterThread", "Service is not running."); + } + try { + Message msg = Message.obtain(null, WaterLevelService.MSG_RESTART_THREADS); + mServiceMessenger.send(msg); + Log.d("UDP Service StarterThread", "Sent restart signal to remote process"); + } catch (RemoteException e) { + Log.e("UDP Service StarterThread", "Failed to send message", e); + } + } public static void startService(Context context) { - Log.d("UDP Service", "Starting service via QtActivity instance"); + // Log.d("UDP Service", "Starting service via QtActivity instance"); - Intent intent = new Intent(context, WaterLevelService.class); + // if(!isMyServiceRunning(WaterLevelService.class, context)) { + // Intent intent = new Intent(context, WaterLevelService.class); + // context.startForegroundService(intent); + // } - context.startForegroundService(intent); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + + if (!isMyServiceRunning(WaterLevelService.class, context)) { + Log.d("UDP Service StarterThread", "Starting service safely on Main Looper"); + startAndBindService(context); + } + else { + Log.d("UDP Service StarterThread", "Service already running, so restarting the threads."); + bindService(context); + } + Log.d("UDP Service StarterThread", "Background service running with pid: " + getServicePid(WaterLevelService.class, context)); + } + }); } } diff --git a/configinterface.cpp b/configinterface.cpp index 563cf68..73ae20a 100644 --- a/configinterface.cpp +++ b/configinterface.cpp @@ -21,20 +21,30 @@ ConfigInterface::ConfigInterface(QObject *parent) auto data = config.readAll(); config.close(); QJsonObject top = QJsonDocument::fromJson(data).object(); - QString max, min, alert; - max = top["max_threshold"].toString(); - min = top["min_threshold"].toString(); - alert = top["alert_threshold"].toString(); + QString max, min, alert, mode, url; + bool flipbits; + max = top["max_threshold"].toString("110"); + min = top["min_threshold"].toString("10"); + alert = top["alert_threshold"].toString("30"); + flipbits = top["flip_bits"].toBool(true); + mode = top["mode"].toString("http"); + url = top["url"].toString("https://unrealdragon.in/server/waterlevel"); setMax(max); setMin(min); setAlert(alert); + setFlipBits(flipbits); + setMode(mode); + setUrl(url); } else { qDebug() << "File does not exist creating one..."; QJsonObject top; - top["max_threshold"] = "125"; - top["min_threshold"] = "25"; - top["alert_threshold"] = "100"; + top["max_threshold"] = "110"; + top["min_threshold"] = "10"; + top["alert_threshold"] = "30"; + top["flip_bits"] = true; + top["mode"] = "http"; + top["url"] = "https://unrealdragon.in/server/waterlevel"; QByteArray data = QJsonDocument(top).toJson(); bool ok = config.open(QFile::WriteOnly); if(!ok) { @@ -44,10 +54,17 @@ ConfigInterface::ConfigInterface(QObject *parent) config.write(data); config.flush(); config.close(); + setMax("110"); + setMin("10"); + setAlert("30"); + setFlipBits(true); + setMode("http"); + setUrl("https://unrealdragon.in/server/waterlevel"); + qDebug() << "Config set initial values completed"; } } -void ConfigInterface::saveConfig(QString max, QString min, QString alert) +void ConfigInterface::saveConfig(QString max, QString min, QString alert, bool flipbits, QString mode, QString url) { auto paths = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); qDebug() << "Got writable path as: " << paths; @@ -58,9 +75,13 @@ void ConfigInterface::saveConfig(QString max, QString min, QString alert) top["max_threshold"] = max; top["min_threshold"] = min; top["alert_threshold"] = alert; + top["flip_bits"] = flipbits; + top["mode"] = mode; + top["url"] = url; setMax(max); setMin(min); setAlert(alert); + setMode(mode); QByteArray data = QJsonDocument(top).toJson(); config.open(QIODevice::WriteOnly); config.write(data); @@ -106,3 +127,42 @@ void ConfigInterface::setMin(const QString &newMin) m_min = newMin; emit minChanged(); } + +bool ConfigInterface::flipBits() const +{ + return m_flipBits; +} + +void ConfigInterface::setFlipBits(bool newFlipBits) +{ + if (m_flipBits == newFlipBits) + return; + m_flipBits = newFlipBits; + emit flipBitsChanged(); +} + +QString ConfigInterface::mode() const +{ + return m_mode; +} + +void ConfigInterface::setMode(const QString &newMode) +{ + if (m_mode == newMode) + return; + m_mode = newMode; + emit modeChanged(); +} + +QString ConfigInterface::url() const +{ + return m_url; +} + +void ConfigInterface::setUrl(const QString &newUrl) +{ + if (m_url == newUrl) + return; + m_url = newUrl; + emit urlChanged(); +} diff --git a/configinterface.h b/configinterface.h index edf60ee..840cc2c 100644 --- a/configinterface.h +++ b/configinterface.h @@ -17,6 +17,9 @@ class ConfigInterface : public QObject Q_PROPERTY(QString min READ min WRITE setMin NOTIFY minChanged FINAL) Q_PROPERTY(QString max READ max WRITE setMax NOTIFY maxChanged FINAL) Q_PROPERTY(QString alert READ alert WRITE setAlert NOTIFY alertChanged FINAL) + Q_PROPERTY(QString mode READ mode WRITE setMode NOTIFY modeChanged FINAL) + Q_PROPERTY(QString url READ url WRITE setUrl NOTIFY urlChanged FINAL) + Q_PROPERTY(bool flipBits READ flipBits WRITE setFlipBits NOTIFY flipBitsChanged FINAL) public: explicit ConfigInterface(QObject *parent = nullptr); QString alert() const; @@ -28,18 +31,33 @@ public: QString min() const; void setMin(const QString &newMin); + bool flipBits() const; + void setFlipBits(bool newFlipBits); + + QString mode() const; + void setMode(const QString &newMode); + + QString url() const; + void setUrl(const QString &newUrl); + signals: - void configData(QString max, QString min, QString alert); + void configData(QString max, QString min, QString alert, bool flipbits, QString mode, QString url); void alertChanged(); void maxChanged(); void minChanged(); + void flipBitsChanged(); + void modeChanged(); + void urlChanged(); public slots: - void saveConfig(QString max, QString min, QString alert); + void saveConfig(QString max, QString min, QString alert, bool flipbits, QString mode, QString url); private: QString m_alert; QString m_max; QString m_min; + bool m_flipBits; + QString m_mode; + QString m_url; }; #endif // CONFIGINTERFACE_H diff --git a/moving_container.h b/moving_container.h index 89a1cf0..966bce8 100644 --- a/moving_container.h +++ b/moving_container.h @@ -1,7 +1,7 @@ #ifndef MOVING_CONTAINER #define MOVING_CONTAINER - +#include using ld = long double; template class average { @@ -21,11 +21,16 @@ public: ~average() { curr = head; + if(!curr) + return; while(curr != nullptr) { info *now = curr; curr = curr->next; delete now; } + curr = nullptr; + head = nullptr; + tail = nullptr; } unsigned int size() { @@ -36,6 +41,21 @@ public: return this->avg; } + void clear() { + curr = head; + if(!curr) + return; + while(curr != nullptr) { + info *now = curr; + curr = curr->next; + delete now; + } + curr = nullptr; + head = nullptr; + tail = nullptr; + count = 0; + } + void recalculate_average() { curr = head; this->avg = 0; diff --git a/service.cpp b/service.cpp index 6b819b0..f48e455 100644 --- a/service.cpp +++ b/service.cpp @@ -11,7 +11,6 @@ Q_DECLARE_JNI_CLASS(ComponentName, "android/content/ComponentName") Q_DECLARE_JNI_CLASS(WaterLevelService, "org/qtproject/example/appwaterLevelSensor/WaterLevelService") - static volatile float level = -1; static void handleWaterLevelUpdate(JNIEnv *env, jobject /*obj*/, jbyteArray data) { @@ -29,87 +28,95 @@ static void handleWaterLevelUpdate(JNIEnv *env, jobject /*obj*/, jbyteArray data env->ReleaseByteArrayElements(data, bytes, JNI_ABORT); } +extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + + // Register your native method ONCE when the library loads + jclass javaClass = env->FindClass("org/qtproject/example/appwaterLevelSensor/WaterLevelReceiver"); + if (javaClass) { + static JNINativeMethod methods[] = { + {"updateWaterLevel", "([B)V", (void *)handleWaterLevelUpdate} + }; + env->RegisterNatives(javaClass, methods, 1); + } + + return JNI_VERSION_1_6; +} + + service::service(QObject *parent) : QObject{parent} { if(QSysInfo::productType() == "android") { qDebug() << "Running android service"; + // QJniEnvironment env; + // // if (env->ExceptionCheck()) env->ExceptionClear(); + // // static bool nativesRegistered = false; - using namespace QtJniTypes; - using namespace QNativeInterface; + // // if(!nativesRegistered) { + // // // jclass javaClass = env.findClass("org/qtproject/example/appwaterLevelSensor/WaterLevelReceiver"); + // // // if (javaClass) { + // // // JNINativeMethod methods[] = {{"updateWaterLevel", "([B)V", (void *)handleWaterLevelUpdate}}; + // // // if (env->RegisterNatives(javaClass, methods, 1) < 0) { + // // // qDebug() << "RegisterNatives failed!"; + // // // } else { + // // // nativesRegistered = true; + // // // } + // // // } else { + // // // qDebug() << "Could not find WaterLevelReceiver class"; + // // // } + // // // if (env->ExceptionCheck()) env->ExceptionClear(); + // // } + // using namespace QtJniTypes; + // using namespace QNativeInterface; - JNINativeMethod methods[] = { - {"updateWaterLevel", "([B)V", (void *)handleWaterLevelUpdate} - }; - // auto *androidApp = qGuiApp->nativeInterface(); - // Context context = androidApp->context(); - QJniObject context = QNativeInterface::QAndroidApplication::context(); - - QJniEnvironment env; - auto serviceClass = env.findClass(Traits::className()); - if (!serviceClass) { - qDebug() << "FAILED to find WaterLevelService class!"; - env->ExceptionDescribe(); // VERY important - env->ExceptionClear(); - return; - } - - jclass javaClass = env.findClass("org/qtproject/example/appwaterLevelSensor/WaterLevelReceiver"); - env->RegisterNatives(javaClass, methods, 1); - - - QJniObject::callStaticMethod( - "org/qtproject/example/appwaterLevelSensor/WaterLevelServiceStarter", - "startService", - "(Landroid/content/Context;)V", - context.object()); // pass the QtActivity instance ); - - // QJniObject activity = QNativeInterface::QAndroidApplication::context(); - // activity.callMethod("startWaterLevelService", "()V"); - // Intent serviceIntent(activity, serviceClass); - // activity.callMethod("startWaterLevelService"); - - // activity.callMethod<() - // QJniObject::callStaticMethod( - // "org/qtproject/example/appwaterLevelSensor/WaterLevelService", - // "startQtAndroidService", - // "(Landroid/content/Context;)V", - // activity.object() - // ); - - // activity.callMethod("startWaterLevelService", "()V"); // Intent serviceIntent(context, serviceClass); - // context.callMethod("startService", serviceIntent); - - // context.callMethod( - // "startForegroundService", - // serviceIntent - // ); - // QJniObject className = QJniObject::fromString( - // "org.qtproject.example.appwaterLevelSensor.WaterLevelService" - // ); - - // QJniObject serviceClassObj = QJniObject::callStaticObjectMethod( - // "java/lang/Class", - // "forName", - // "(Ljava/lang/String;)Ljava/lang/Class;", - // className.object() - // ); - - // QJniObject intent( - // "android/content/Intent", - // "(Landroid/content/Context;Ljava/lang/Class;)V", - // context.object(), - // serviceClassObj.object() - // ); - - // context.callObjectMethod( - // "startForegroundService", - // "(Landroid/content/Intent;)Landroid/content/ComponentName;", - // intent.object() - // ); // WaterLevelService::callStaticMethod("startQtAndroidService", context); + // // Context context = androidApp->context(); + // QJniObject context = QNativeInterface::QAndroidApplication::context(); + // auto serviceClass = env.findClass(Traits::className()); + // if (!serviceClass) { + // qDebug() << "FAILED to find WaterLevelService class!"; + // env->ExceptionDescribe(); // VERY important + // env->ExceptionClear(); + // return; + // } + // if (context.isValid()) { + // QJniObject::callStaticMethod< + // void>( + // "org/qtproject/example/appwaterLevelSensor/WaterLevelServiceStarter", + // "startService", + // "(Landroid/content/Context;)V", + // context.object()); + // } } } + +void service::moveAppToBackground() +{ + QJniObject activity = QNativeInterface::QAndroidApplication::context(); + if (activity.isValid()) { + qDebug() << "Moved app to background"; + activity.callMethod("moveTaskToBack", "(Z)Z", true); + } + qDebug() << "Closing"; +} + +void service::begin() { + + + // Only call this once the UI is visible + QJniObject context = QNativeInterface::QAndroidApplication::context(); + QJniObject::callStaticMethod( + "org/qtproject/example/appwaterLevelSensor/WaterLevelServiceStarter", + "startService", + "(Landroid/content/Context;)V", + context.object() + ); +} + diff --git a/service.h b/service.h index 5b64765..1ba18a1 100644 --- a/service.h +++ b/service.h @@ -8,6 +8,8 @@ class service : public QObject Q_OBJECT public: explicit service(QObject *parent = nullptr); + void moveAppToBackground(); + void begin(); signals: }; diff --git a/udplistener.cpp b/udplistener.cpp index 4978794..155df4e 100644 --- a/udplistener.cpp +++ b/udplistener.cpp @@ -1,19 +1,19 @@ #include "udplistener.h" #include -#ifdef Q_OS_ANDROID -#include "service.h" -#endif UDPListener::UDPListener(QObject *parent) : QObject{parent} { - worker = new Worker(this); - thread = new QThread(this); + worker = new Worker(); + thread = new QThread(); worker->moveToThread(thread); + connect(worker, &Worker::lastUpdateddatagramReceived, this, &UDPListener::lastUpdateddatagramReceived); connect(worker, &Worker::datagramReceived, this, &UDPListener::datagramRecieved); connect(worker, &Worker::socketError, this, &UDPListener::socketError); + connect(worker, &Worker::sendConfig, this, &UDPListener::sendConfigOnSet); connect(this, &UDPListener::startWorker, worker, &Worker::run); + connect(this, &UDPListener::changeWorkerMode, worker, &Worker::changeMode); thread->start(); emit startWorker(); timer = new QTimer(this); @@ -21,11 +21,6 @@ UDPListener::UDPListener(QObject *parent) timer->setSingleShot(false); connect(timer, &QTimer::timeout, this, &UDPListener::connectionTimerTimedOut); connect(qApp, &QCoreApplication::aboutToQuit, this, &UDPListener::destroy); -#ifdef Q_OS_ANDROID - qDebug() << "Running on Android"; - service service; - qDebug() << "Ran service"; -#endif } double UDPListener::distance() const @@ -39,26 +34,118 @@ void UDPListener::setDistance(double newDistance) emit distanceChanged(); } -void UDPListener::setAlert(float distance) +void UDPListener::sendConfigOnSet() { + bool flip_bits = m_flip_bits; + QString url = m_url; + QString mode = m_mode; + float distance = m_distance; if(!command_socket) { command_socket = new QUdpSocket(this); - bool bind = command_socket->bind(8181); + bool bind = command_socket->bind(QHostAddress::Any, 0); if(bind) { - QNetworkDatagram data; - data.setDestination(QHostAddress::LocalHost, 8178); + QNetworkDatagram data1, data2, data3, data4; + data1.setDestination(QHostAddress::LocalHost, 8178); + data2.setDestination(QHostAddress::LocalHost, 8178); + data3.setDestination(QHostAddress::LocalHost, 8178); + data4.setDestination(QHostAddress::LocalHost, 8178); + QString s_data = ""; while(s_data.toUtf8().size() != 1024) { s_data += "="; } - data.setData(s_data.toUtf8()); - command_socket->writeDatagram(data); + data1.setData(s_data.toUtf8()); + command_socket->writeDatagram(data1); command_socket->flush(); + + QString b_data = ""; + while(b_data.toUtf8().size() != 1024) { + b_data += "="; + } + data2.setData(b_data.toUtf8()); + command_socket->writeDatagram(data2); + command_socket->flush(); + + QString m_data = ""; + while(m_data.toUtf8().size() != 1024) { + m_data += "="; + } + data3.setData(m_data.toUtf8()); + command_socket->writeDatagram(data3); + command_socket->flush(); + + QString u_data = ""; + while(u_data.toUtf8().size() != 1024) { + u_data += "="; + } + data4.setData(u_data.toUtf8()); + command_socket->writeDatagram(data4); + command_socket->flush(); + command_socket->close(); + command_socket->deleteLater(); + command_socket = nullptr; + + qDebug() << "Sent all Command Statements."; + emit changeWorkerMode(mode, url); + } + else { + showError("Command Socket Bind Failed"); } } } +void UDPListener::moveAppToBackground() +{ +#ifdef Q_OS_ANDROID + service->moveAppToBackground(); +#endif +} + +void UDPListener::lastUpdateddatagramReceived(QByteArray data) +{ + QString s_data = QString::fromUtf8(data); + qDebug() << "Main Class last updated on: " << s_data; + QDateTime time = QDateTime::fromString(s_data, "dd/MM/yy hh:mm:ss").addYears(100); + QDateTime thres_time = QDateTime::currentDateTime().addSecs(-1800); + if(time.isValid() && thres_time < time) + { + emit lastUpdatedChanged(s_data, false); + } + else if(thres_time > time && time.isValid()) { + emit lastUpdatedChanged(s_data, true); + } + else if(!time.isValid()) { + emit lastUpdatedChanged("Invalid Date", true); + } +} + +void UDPListener::continueDestruction() +{ + destroyed = true; +} + +void UDPListener::resetAverages() +{ + averages.clear(); +} + +void UDPListener::setFlipBits(bool flip_bits) +{ + m_flip_bits = flip_bits; +} + +void UDPListener::setConfig(float distance, bool flip_bits, QString mode, QString url) +{ + setFlipBits(flip_bits); + setDistance(distance); + setMode(mode); + setUrl(url); + emit changeWorkerMode(mode, url); + qDebug() << "Setting config from UI completed." << flip_bits << distance << mode << url; + averages.clear(); +} + void UDPListener::socketError(QString err) { qDebug() << "Socket error received: " << err; @@ -70,15 +157,34 @@ void UDPListener::socketError(QString err) void UDPListener::startService() { +#ifdef Q_OS_ANDROID + qDebug() << "Running on Android"; + service = new class service(); + qDebug() << "Ran service"; + service->begin(); +#endif } -void UDPListener::datagramRecieved(float distance) +void UDPListener::datagramRecieved(QByteArray data1, QByteArray data2) { - qDebug() << "Data received in main class: " << distance; - averages.push_back(distance); + std::string correct_order = ""; +#ifdef Q_OS_ANDROID + if(!m_flip_bits) + correct_order = "0x" + data2.toHex().toStdString(); + else + correct_order = "0x" + data1.toHex().toStdString(); +#else + if(!m_flip_bits) + correct_order = "0x" + data1.toHex().toStdString(); + else + correct_order = "0x" + data2.toHex().toStdString(); +#endif + float data = convertToFloat(correct_order); + qDebug() << "Data received in main class: " << data; + averages.push_back(data); prev_time_point = std::chrono::high_resolution_clock::now(); - emit distanceReceived(distance, QDateTime::currentDateTime().toString("dd/mm/yyyy hh:mm:ss.z ap")); + emit distanceReceived(data, QDateTime::currentDateTime().toString("dd/mm/yyyy hh:mm:ss.z ap")); if(averages.size() >= 5) { this->m_distance = averages.get_average(); emit distanceChanged(); @@ -91,11 +197,31 @@ void UDPListener::datagramRecieved(float distance) void UDPListener::destroy() { qDebug() << "Destroying UDP Listener"; + +#ifdef Q_OS_ANDROID + delete service; +#endif + disconnect(timer, nullptr, nullptr, nullptr); timer->stop(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 2000); + connect(this, &UDPListener::stopWorker, worker, &Worker::stop); + connect(worker, &Worker::stopped, this, &UDPListener::continueDestruction); + emit stopWorker(); + while(!destroyed) { + QCoreApplication::processEvents(QEventLoop::AllEvents, 2000); + } thread->exit(0); - thread->deleteLater(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 2000); worker->deleteLater(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 2000); + while(thread->isRunning()) { + qDebug() << "The thread is still running."; + QCoreApplication::processEvents(QEventLoop::AllEvents, 2000); + } + thread->deleteLater(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 2000); timer->deleteLater(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 2000); qDebug() << "Deinitialized UDP Listener"; } @@ -111,51 +237,239 @@ void UDPListener::connectionTimerTimedOut() } } +float UDPListener::convertToFloat(std::string hex) +{ + uint32_t num; + sscanf(hex.c_str(), "%x", &num); + float dist = *((float*)&num); + return dist; +} + Worker::Worker(QObject *parent) { } +void Worker::gotHttpReply(QNetworkReply *rep) +{ + QByteArray m_data = rep->readAll(); + rep->deleteLater(); + qDebug() << "Got reply from http server"; + QJsonObject top = QJsonDocument::fromJson(m_data).object(); + float distance = (float ) top["water_level"].toArray()[0].toDouble(); + QByteArray data, data2; + qDebug() << "Updated on: " << top["updated_on"].toString(); + data = QByteArray::fromRawData((char *)&distance, sizeof(float)); + char *buff = new char[4]; + char *reader = (char *) &distance; + for(int i=0;i< sizeof(float) ;i++) { + qDebug() << i << "vs" << sizeof(float) - 1 - i; + buff[i] = reader[sizeof(float) - 1 - i]; + } + delete[] buff; + data2 = QByteArray::fromRawData(buff, sizeof(float)); + emit datagramReceived(data, data2); + emit lastUpdateddatagramReceived(top["updated_on"].toString().toUtf8()); +} + +void Worker::httpTimerTimeout() +{ + qDebug() << "Worker httpTimer Timeout"; + QNetworkRequest req; + req.setUrl(QUrl("https://unrealdragon.in/server/waterlevel")); + disconnect(manager, nullptr, nullptr, nullptr); + connect(manager, &QNetworkAccessManager::finished, this, &Worker::gotHttpReply); + manager->get(req); +} + +void Worker::stop() +{ + if(socket_2) { + disconnect(socket_2, nullptr, nullptr, nullptr); + socket_2->close(); + delete socket_2; + } + if(socket) { + disconnect(socket, nullptr, nullptr, nullptr); + socket->close(); + delete socket; + } + if(socket_3) { + disconnect(socket_3, nullptr, nullptr, nullptr); + socket_3->close(); + delete socket_3; + } + if(httpTimer) { + disconnect(httpTimer, nullptr, nullptr, nullptr); + delete httpTimer; + } + if(manager) { + disconnect(manager, nullptr, nullptr, nullptr); + delete manager; + } + emit stopped(); +} + void Worker::run() { using namespace std::chrono_literals; socket = new QUdpSocket(this); + socket_2 = new QUdpSocket(this); + socket_3 = new QUdpSocket(this); + httpTimer = new QTimer(this); + manager = new QNetworkAccessManager(this); #ifdef Q_OS_ANDROID - bool bind = socket->bind(QHostAddress::Any, 8180); + // If it is android use it as a command socket; + bool bind = socket->bind(QHostAddress::LocalHost, 8180, QUdpSocket::ReuseAddressHint | QUdpSocket::ShareAddress); + bool bind2 = socket_2->bind(QHostAddress::LocalHost, 8185, QUdpSocket::ReuseAddressHint | QUdpSocket::ShareAddress); + bool bind3 = socket_3->bind(QHostAddress::LocalHost, 8186, QUdpSocket::ReuseAddressHint | QUdpSocket::ShareAddress); #else + // Else use it as a receiver; bool bind = socket->bind(QHostAddress("192.168.1.255"), 8176); + bool bind2 = true; #endif - if(!bind) + if(!bind || !bind2 || !bind3) { qDebug() << "UI Worker socket bind failed" << bind << socket->errorString(); + qDebug() << "UI Worker socket bind failed" << bind2 << socket_2->errorString(); + qDebug() << "UI Worker socket bind failed" << bind3 << socket_3->errorString(); emit socketError(socket->errorString()); + emit socketError(socket_2->errorString()); + emit socketError(socket_3->errorString()); + qApp->exit(-1); + } + connect(httpTimer, &QTimer::timeout, this, &Worker::httpTimerTimeout); + connect(socket, &QUdpSocket::readyRead, this, &Worker::socketDataReceived); + connect(socket_2, &QUdpSocket::readyRead, this, &Worker::socket2DataReceived); + connect(socket_3, &QUdpSocket::readyRead, this, &Worker::socket3DataReceived); +#ifdef Q_OS_ANDROID + qDebug() << "Waiting for message from service."; +#endif + +} + +void Worker::changeMode(QString mode, QString url) +{ + qDebug() << "Worker changing mode: " << mode << url; + this->mode = mode; + this->url = url; + if(mode == "http" && QSysInfo::productType() != "android") { + httpTimer->setInterval(2000); + httpTimer->setSingleShot(false); + httpTimer->start(); + } + else if(mode == "udp") { + httpTimer->stop(); + socketDataReceived(); + } + if(socket) { + if(socket->hasPendingDatagrams()) { + socketDataReceived(); + } + } + if(socket_2) { + if(socket_2->hasPendingDatagrams()) { + socket2DataReceived(); + } + } + if(socket_3) { + if(socket_3->hasPendingDatagrams()) { + socket3DataReceived(); + } + } + +} + +void Worker::socket3DataReceived() +{ + if(socket_3->hasPendingDatagrams()) { + while(this->mode == "" && this->url == "") { + QCoreApplication::processEvents(); + // qDebug() << "Worker is waiting for mode to be set."; + } + size_t data_size = socket_3->pendingDatagramSize(); + qDebug() << "Socket 3 has pending data: " << data_size; + if(data_size < 100000) { + char buff[data_size]; + socket_3->readDatagram(buff, data_size); + QByteArray data = QByteArray::fromRawData(buff, data_size); + if(QString::fromUtf8(data) == "") { + qDebug() << "Data Received from Service Threads."; + emit sendConfig(); + } + } + } +} + +void Worker::socket2DataReceived() +{ + if(socket_2->hasPendingDatagrams()) { + if(mode == "http") { + size_t data_size = socket_2->pendingDatagramSize(); + qDebug() << "Socket 2 has pending data: " << data_size; + char buff[data_size]; + socket_2->readDatagram(buff, data_size); + QByteArray data = QByteArray::fromRawData(buff, data_size); + emit lastUpdateddatagramReceived(data); + } + else if(mode == "udp") { + qDebug() << "Last Updated on Socket data received in udp mode ignoring it."; + } + else { + qDebug() << "Mode is not set in socket 2"; + } } - connect(socket, &QUdpSocket::readyRead, this, &Worker::socketDataReceived); } void Worker::socketDataReceived() { if(socket->hasPendingDatagrams()) { - qDebug() << "Socket has pending data: " << socket->pendingDatagramSize(); - char buff[4] = {0x00, 0x00, 0x00, 0x00}; - socket->readDatagram(buff, 4); - auto data= QByteArray(buff,4); - char ch[4]; - for(int i=0;i<4;i++) { - ch[i] = buff[3-i]; + if(mode == "udp" || (mode == "http" && QSysInfo::productType() == "android")) { + qDebug() << "Socket has pending data: " << socket->pendingDatagramSize(); + char buff[4] = {0x00, 0x00, 0x00, 0x00}; + socket->readDatagram(buff, 4); + auto data= QByteArray(buff,4); + char ch[4]; + for(int i=0;i<4;i++) { + ch[i] = buff[3-i]; + } + auto data2 = QByteArray(ch,4); + std::string correct_order = ""; + qDebug() << "Distance received: " << correct_order; + emit datagramReceived(data, data2); + } + else if(mode == "http") { + qDebug() << "Socket data received in http mode ignoring it."; + } + else { + qDebug() << "Mode is not set in socket."; } - auto data2 = QByteArray(ch,4); -#ifdef Q_OS_ANDROID - std::string correct_order = "0x" + data2.toHex().toStdString(); -#else - std::string correct_order = "0x" + data2.toHex().toStdString(); -#endif - qDebug() << "Distance received: " << correct_order; - uint32_t num; - sscanf(correct_order.c_str(), "%x", &num); - float dist = *((float*)&num); - qDebug() << "Float is: " << dist; - emit datagramReceived(dist); } } + +QString UDPListener::mode() const +{ + return m_mode; +} + +void UDPListener::setMode(const QString &newMode) +{ + if (m_mode == newMode) + return; + m_mode = newMode; + emit modeChanged(); +} + +QString UDPListener::url() const +{ + return m_url; +} + +void UDPListener::setUrl(const QString &newUrl) +{ + if (m_url == newUrl) + return; + m_url = newUrl; + emit urlChanged(); +} diff --git a/udplistener.h b/udplistener.h index 0eefa61..079dd35 100644 --- a/udplistener.h +++ b/udplistener.h @@ -8,19 +8,45 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include "moving_container.h" +#ifdef Q_OS_ANDROID +#include "service.h" +#endif + + class Worker: public QObject { Q_OBJECT QUdpSocket *socket = nullptr; + QUdpSocket *socket_2 = nullptr; + QUdpSocket *socket_3 = nullptr; + QString mode = ""; + QString url = ""; + QNetworkAccessManager *manager = nullptr; + QTimer *httpTimer = nullptr; public: explicit Worker(QObject *parent = nullptr); signals: + void sendConfig(); + void stopped(); void socketError(QString error); - void datagramReceived(float distance); + void lastUpdateddatagramReceived(QByteArray data); + void datagramReceived(QByteArray data, QByteArray data2); public slots: + void gotHttpReply(QNetworkReply *rep); + void httpTimerTimeout(); + void stop(); void run(); + void changeMode(QString mode, QString url); private slots: + void socket3DataReceived(); + void socket2DataReceived(); void socketDataReceived(); }; @@ -29,25 +55,51 @@ class UDPListener : public QObject Q_OBJECT QML_ELEMENT Q_PROPERTY(double distance READ distance WRITE setDistance NOTIFY distanceChanged FINAL) + Q_PROPERTY(QString mode READ mode WRITE setMode NOTIFY modeChanged FINAL) + Q_PROPERTY(QString url READ url WRITE setUrl NOTIFY urlChanged FINAL) + bool destroyed = false; +#ifdef Q_OS_ANDROID + service *service = nullptr; +#endif public: explicit UDPListener(QObject *parent = nullptr); double distance() const; void setDistance(double newDistance); + QString mode() const; + void setMode(const QString &newMode); + + QString url() const; + void setUrl(const QString &newUrl); + signals: + void lastUpdatedChanged(QString last_updated, bool warn); + void stopWorker(); void showError(QString err); void distanceChanged(); void startWorker(); void connected(); void disconnected(); void distanceReceived(double dist, QString timestamp); + void changeWorkerMode(QString mode, QString url); + void modeChanged(); + void initLoadFinished(); + void urlChanged(); + public slots: - void setAlert(float distance); + void sendConfigOnSet(); + void moveAppToBackground(); + void lastUpdateddatagramReceived(QByteArray data); + void continueDestruction(); + void resetAverages(); + void setFlipBits(bool flip_bits); + void setConfig(float distance, bool flip_bits, QString mode, QString url); void socketError(QString err); void startService(); - void datagramRecieved(float distance); + void datagramRecieved(QByteArray data, QByteArray data2); private slots: void destroy(); void connectionTimerTimedOut(); + float convertToFloat(std::string hex); private: QUdpSocket *command_socket = nullptr; std::chrono::time_point prev_time_point; @@ -56,6 +108,9 @@ private: Worker *worker = nullptr; QThread *thread = nullptr; QTimer *timer = nullptr; + bool m_flip_bits; + QString m_mode; + QString m_url; }; #endif // UDPLISTENER_H