commit 9bfa1e11fe46a72b8a2a6ba8f9d9016ac113c7d7 Author: TheUnrealDragon Date: Wed Apr 1 18:33:19 2026 +0530 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa3808c --- /dev/null +++ b/.gitignore @@ -0,0 +1,82 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* +*.qbs.user* +CMakeLists.txt.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + +# Directories with generated files +.moc/ +.obj/ +.pch/ +.rcc/ +.uic/ +/build*/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c116d03 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,97 @@ +cmake_minimum_required(VERSION 3.16) + +project(waterLevelSensor VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt6 REQUIRED COMPONENTS Quick Network) + +qt_standard_project_setup(REQUIRES 6.9) + +qt_add_executable(appwaterLevelSensor + main.cpp + images.qrc +) + + +if(ANDROID) +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 + ${CMAKE_CURRENT_SOURCE_DIR}/android) + + +target_link_libraries(service + PRIVATE + Qt::Core + Qt::CorePrivate + Qt::Network + Qt::Quick +) +qt_add_qml_module(appwaterLevelSensor + URI waterLevelSensor + QML_FILES + Main.qml + RESOURCES + RESOURCES images.qrc + SOURCES moving_container.h + SOURCES configinterface.h configinterface.cpp + SOURCES udplistener.h udplistener.cpp + SOURCES service.h +) +target_link_libraries(appwaterLevelSensor + PRIVATE Qt6::Quick Qt6::Network service +) + +qt_finalize_executable(appwaterLevelSensor) +add_dependencies(appwaterLevelSensor service) + +qt_generate_deploy_app_script( + TARGET appwaterLevelSensor + OUTPUT_SCRIPT deploy_script + NO_UNSUPPORTED_PLATFORM_ERROR +) +install(SCRIPT ${deploy_script}) + +endif() +if(NOT ANDROID) +qt_add_qml_module(appwaterLevelSensor + URI waterLevelSensor + QML_FILES + Main.qml + RESOURCES + RESOURCES images.qrc + SOURCES udplistener.h udplistener.cpp + SOURCES moving_container.h + SOURCES configinterface.h configinterface.cpp +) + +# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. +# If you are developing for iOS or macOS you should consider setting an +# explicit, fixed bundle identifier manu ally though. +set_target_properties(appwaterLevelSensor PROPERTIES +# MACOSX_BUNDLE_GUI_IDENTIFIER com.example.appwaterLevelSensor + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE +) + +target_link_libraries(appwaterLevelSensor + PRIVATE Qt6::Quick Qt6::Network +) + +qt_finalize_executable(appwaterLevelSensor) + +include(GNUInstallDirs) +install(TARGETS appwaterLevelSensor + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) +endif() diff --git a/Main.qml b/Main.qml new file mode 100644 index 0000000..66f221e --- /dev/null +++ b/Main.qml @@ -0,0 +1,532 @@ +import QtQuick +import waterLevelSensor +import QtQuick.Effects +import QtQuick.Controls +import QtQuick.Controls.Material + +Window { + width: 640 + height: 480 + visible: true + title: qsTr("Hello World") + id: root + property color accent_color: "#F05FAF5F" + 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 double distance: 0 + property int distance_count: 0 + // visibility: Window.FullScreen + Rectangle { + anchors.fill: parent + id: background + color: "#DF0F0F0F" + Rectangle { + id: top_bar + height: Math.min(root.height / 12, 100) + width: root.width + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + topMargin: 25 + } + color: "#0A0F0F0F" + + Rectangle { + id: settings_rect + height: top_bar.height - 20 + width: top_bar.width / 12 + anchors { + top: parent.top + left: parent.left + bottomMargin: 5 + topMargin: 5 + leftMargin: 5 + } + Behavior on color { + ColorAnimation { + duration: 200 + } + } + + color: "#0A0F0F0F" + border.color: "#AF121212" + border.width: 0.5 + radius: 2 + Text { + id: settings_text + text: "⚙" + anchors.centerIn: parent + color: root.accent_color + font.pointSize: root.font_enlarged_text_size + } + + MouseArea { + id: settings_button_area + anchors.fill: parent + hoverEnabled: true + property color settings_rect_pressed_prev: "#00000000" + property color settings_rect_entered_prev: "#00000000" + onEntered: { + settings_rect_entered_prev = settings_rect.color + settings_rect.color = "#5A1F1F1F" + } + onExited: { + settings_rect.color = settings_rect_entered_prev + } + onPressed: { + settings_rect_pressed_prev = settings_rect.color + settings_rect.color = "#5A1F4F1F" + settings_popup.open() + } + onReleased: { + settings_rect.color = settings_rect_pressed_prev + } + } + } + + + + Rectangle { + property color old_color: "#0F0F0F0F" + id: status_indicator + width: 20 + height: 20 + radius: 20 + anchors { + top: parent.top + right: parent.right + topMargin: 20 + bottomMargin: 5 + leftMargin: 5 + rightMargin: 20 + } + color: "#F0AF2F1F" + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + } + } + Text { + id: status_label + anchors { + top: parent.top + topMargin: 20 + right: status_indicator.left + rightMargin: 10 + } + horizontalAlignment: Text.AlignRight + width: parent.width - status_indicator.width + height: parent.height + color: root.accent_color + font.pointSize: root.font_normal_text_size + text: "Disconnected" + } + } + + Rectangle { + id: base + color: "#0A0F0F0F" + width: parent.width + height: parent.height - top_bar.height + anchors { + top: top_bar.bottom + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + + Popup { + id: settings_popup + background: Rectangle { + color: "#F0121212" + radius: 5 + border.color: root.accent_color + border.width: 1 + } + height: parent.height / 1.5 + width: parent.width / 1.5 + anchors.centerIn: Overlay.overlay + + + Rectangle { + id: settings_base + anchors.fill: parent + color: "#F0121212" + + Text { + id: settings_label + text: "Settings" + width: parent.width + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + } + horizontalAlignment: Text.AlignHCenter + color: root.accent_color + font.pointSize: root.font_enlarged_text_size + } + + + Text { + id: max_thres_label + text: "Max Threshold: " + anchors { + topMargin: 20 + top: settings_label.bottom + left: parent.left + } + height: max_thres_input.height + verticalAlignment: Text.AlignVCenter + color: root.accent_color + font.pixelSize: root.font_normal_text_size + } + + TextField { + id: max_thres_input + text: config.max + anchors { + left: max_thres_label.right + leftMargin: 5 + top: settings_label.bottom + topMargin: 20 + } + color: root.accent_color + font.pixelSize: root.font_normal_text_size + } + + + Text { + id: min_thres_label + text: "Min Threshold: " + anchors { + topMargin: 20 + top: max_thres_label.bottom + left: parent.left + } + height: min_thres_input.height + verticalAlignment: Text.AlignVCenter + color: root.accent_color + font.pixelSize: root.font_normal_text_size + } + + TextField { + id: min_thres_input + text: config.min + anchors { + left: min_thres_label.right + leftMargin: 5 + top: max_thres_label.bottom + topMargin: 20 + } + color: root.accent_color + font.pixelSize: root.font_normal_text_size + } + + + Text { + id: alert_thres_label + text: "Alert Percentage: " + anchors { + topMargin: 20 + top: min_thres_label.bottom + left: parent.left + } + height: alert_thres_input.height + verticalAlignment: Text.AlignVCenter + color: root.accent_color + font.pixelSize: root.font_normal_text_size + } + + TextField { + id: alert_thres_input + text: config.alert + anchors { + left: alert_thres_label.right + leftMargin: 5 + top: min_thres_label.bottom + topMargin: 20 + } + color: root.accent_color + font.pixelSize: root.font_normal_text_size + } + + Text { + id: settings_error_label + property string settings_error_text: "" + text: "Error: " + settings_error_text + visible: settings_error_text === "" ? false : true + anchors { + topMargin: 20 + top: alert_thres_label.bottom + left: parent.left + horizontalCenter: parent.horizontalCenter + } + horizontalAlignment: Text.AlignHCenter + color: root.danger_color + font.pixelSize: root.font_normal_text_size + } + + + + Rectangle { + id: save_config + anchors { + topMargin: 10 + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + color: "#F0121212" + width: parent.width / 2.5 + height: 60 + border { + width: 1 + color: root.accent_color + } + radius: 3 + + Text { + anchors.centerIn: parent + text: "Save" + 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 = save_config.color + save_config.color = "#5A1F1F1F" + } + onExited: { + save_config.color = settings_rect_entered_prev + } + onPressed: { + settings_rect_pressed_prev = save_config.color + save_config.color = "#5A1F4F1F" + 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); + if(max < min) { + settings_error_label.settings_error_text = "Max cannot be less than min"; + return; + } + if (alert > 100 || alert < 0) { + 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); + settings_popup.close(); + } + onReleased: { + save_config.color = settings_rect_pressed_prev + } + } + } + } + } + + Rectangle { + id: tank_lid + color: "#F0121212" + anchors { + bottom: tank_base.top + horizontalCenter: tank_base.horizontalCenter + bottomMargin: -5 + } + radius: 10 + width: parent.width / 4 + height: parent.height / 15 + } + + Rectangle { + id: tank_base + color: "#0FFFFFFF" + anchors.centerIn: parent + width: parent.width / 2.5 + height: parent.height / 2 + border.color: "#F0121212" + border.width: 5 + radius: 4 + } + + Rectangle { + id: water_level + color: root.water_color + property double height_max: tank_base.height - 10 + property double current_level: 0 + property double percentage_filled: 0 + height: water_level.current_level + width: tank_base.width - 10 + anchors { + bottom: tank_base.bottom + horizontalCenter: tank_base.horizontalCenter + bottomMargin: 5 + } + Behavior on color { + ColorAnimation { + duration: 200 + } + } + Behavior on height { + NumberAnimation { + duration: 200 + } + } + } + Text { + id: info_text + text: "Current Reading: " + root.distance.toFixed(2) + " cm" + anchors { + top: tank_base.bottom + topMargin: 20 + horizontalCenter: parent.horizontalCenter + } + horizontalAlignment: Text.AlignHCenter + color: root.accent_color + font.pixelSize: root.font_normal_text_size + } + + Text { + id: info_percent_text + text: "Percentage filled: " + water_level.percentage_filled.toFixed(2) + " %" + anchors { + top: info_text.bottom + topMargin: 10 + horizontalCenter: parent.horizontalCenter + } + horizontalAlignment: Text.AlignHCenter + color: root.accent_color + font.pixelSize: root.font_normal_text_size + } + + Text { + id: error_text + property string error_string: "" + text: "Error: " + error_string + visible: error_string === "" ? false : true; + anchors { + top: info_percent_text.bottom + topMargin: 10 + horizontalCenter: parent.horizontalCenter + } + horizontalAlignment: Text.AlignHCenter + color: root.danger_color + font.pixelSize: root.font_normal_text_size + } + + + } + } + + + Timer { + id: status_indicator_timer + running: false + repeat: false + triggeredOnStart: false + interval: 300 + onTriggered: { + status_indicator.color = status_indicator.old_color; + } + } + + ConfigInterface { + id: config + onConfigData: (max, min, alert) => { + console.log("Got config data as: ", max, min, alert); + } + } + + + UDPListener { + id: listener + + onShowError: (err) => { + error_text.error_string = err; + } + + 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) { + 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); + let current_reading = Math.max(0, root.distance - current_min_reading); + let height_max_possible = water_level.height_max; + let percentage_filled = Math.max(0, ((effective_max_reading - current_reading) / effective_max_reading * 100)) + let normalized_height = percentage_filled * height_max_possible / 100; + let alert = parseFloat(alert_thres_input.text); + if(percentage_filled <= alert) { + console.log("Water Level is lower than alert"); + water_level.color = root.danger_color; + } + else { + water_level.color = root.water_color; + } + + console.log("Current reading is: " + current_reading) + console.log("Normalized height is: " + normalized_height) + water_level.current_level = normalized_height; + water_level.percentage_filled = percentage_filled; + } + } + onConnected: { + error_text.error_string = ""; + console.log("Connected"); + status_indicator.color = "#F05F5F0F" + status_label.text = "Waiting for more values"; + + } + onDisconnected: { + error_text.error_string = "Disconnected"; + console.log("Disconnected"); + status_indicator.color = "#F05F5F0F" + status_label.text = "Disconnected"; + } + onDistanceReceived: (distance, time) => { + if(root.distance_count >= 1) + dataBlink(); + if(root.distance_count == 0) { + 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("Alert effective reading is: " + alert_effective_reading + " for value: " + alert); + listener.setAlert(alert_effective_reading); + } + + distance_count += 1; + } + } + + onClosing:(close)=> { + if(Qt.platform.os === "android"){ + close.accepted = false + } else { + close.accepted = true + } + } + + function dataBlink() { + let old_color = status_indicator.color; + let new_color = "#F04FAF5F"; + status_indicator.color = new_color; + status_indicator.old_color = old_color; + status_indicator_timer.start(); + } +} diff --git a/NotificationManager.java b/NotificationManager.java new file mode 100644 index 0000000..5656db5 --- /dev/null +++ b/NotificationManager.java @@ -0,0 +1,14 @@ +package org.qtproject.example.androidnotifier; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.BitmapFactory; +import android.app.NotificationChannel; + + +public class NotificationManager { + +} diff --git a/WaterTank1.png b/WaterTank1.png new file mode 100644 index 0000000..24ef3c1 Binary files /dev/null and b/WaterTank1.png differ diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml new file mode 100644 index 0000000..d31ff4c --- /dev/null +++ b/android/AndroidManifest.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..ea6c193 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,83 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + //noinspection AndroidGradlePluginVersion + classpath 'com.android.tools.build:gradle:8.8.0' + } +} + +repositories { + google() + mavenCentral() +} + +apply plugin: qtGradlePluginType + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) + //noinspection GradleDependency + implementation 'androidx.core:core:1.16.0' +} + +android { + /******************************************************* + * The following variables: + * - androidBuildToolsVersion, + * - androidCompileSdkVersion + * - qtAndroidDir - holds the path to qt android files + * needed to build any Qt application + * on Android. + * - qtGradlePluginType - whether to build an app or a library + * + * are defined in gradle.properties file. This file is + * updated by QtCreator and androiddeployqt tools. + * Changing them manually might break the compilation! + *******************************************************/ + + namespace = androidPackageName + compileSdkVersion androidCompileSdkVersion + buildToolsVersion androidBuildToolsVersion + ndkVersion = androidNdkVersion + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = [qtAndroidDir + '/src', 'src', 'java'] + aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl'] + res.srcDirs = [qtAndroidDir + '/res', 'res'] + resources.srcDirs = ['resources'] + renderscript.srcDirs = ['src'] + assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['libs'] + } + } + + tasks.withType(JavaCompile) { + options.incremental = true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + lintOptions { + abortOnError = false + } + + aaptOptions { + // Do not compress Qt binary resources file + noCompress 'rcc' + } + + defaultConfig { + resConfig "en" + minSdkVersion qtMinSdkVersion + targetSdkVersion qtTargetSdkVersion + ndk.abiFilters = qtTargetAbiList.split(",") + } +} diff --git a/android/res/drawable-hdpi/ic_launcher.png b/android/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..7d0f630 Binary files /dev/null and b/android/res/drawable-hdpi/ic_launcher.png differ diff --git a/android/res/drawable-mdpi/ic_launcher.png b/android/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..9260e4e Binary files /dev/null and b/android/res/drawable-mdpi/ic_launcher.png differ diff --git a/android/res/drawable-xhdpi/ic_launcher.png b/android/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..c3e5ba9 Binary files /dev/null and b/android/res/drawable-xhdpi/ic_launcher.png differ diff --git a/android/res/drawable-xxhdpi/ic_launcher.png b/android/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..1257bb6 Binary files /dev/null and b/android/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/android/res/drawable-xxxhdpi/ic_launcher.png b/android/res/drawable-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4ed8a75 Binary files /dev/null and b/android/res/drawable-xxxhdpi/ic_launcher.png differ diff --git a/android/src/org/qtproject/example/appwaterLevelSensor/.WaterLevelService.java.kate-swp b/android/src/org/qtproject/example/appwaterLevelSensor/.WaterLevelService.java.kate-swp new file mode 100644 index 0000000..aac66dc Binary files /dev/null and b/android/src/org/qtproject/example/appwaterLevelSensor/.WaterLevelService.java.kate-swp differ diff --git a/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelReceiver.java b/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelReceiver.java new file mode 100644 index 0000000..60b004c --- /dev/null +++ b/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelReceiver.java @@ -0,0 +1,17 @@ +package org.qtproject.example.appwaterLevelSensor; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class WaterLevelReceiver extends BroadcastReceiver { + public native static void updateWaterLevel(byte[] data); + + @Override + public void onReceive(Context context, Intent intent) { + if ("com.example.WATER_LEVEL_UPDATE".equals(intent.getAction())) { + byte[] data = intent.getByteArrayExtra("level_data"); + updateWaterLevel(data); + } + } +} diff --git a/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelService.java b/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelService.java new file mode 100644 index 0000000..c868acc --- /dev/null +++ b/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelService.java @@ -0,0 +1,210 @@ +package org.qtproject.example.appwaterLevelSensor; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.BitmapFactory; +import android.app.NotificationChannel; +import org.qtproject.qt.android.bindings.QtService; +import android.util.Log; +import android.content.Intent; +import java.lang.Runnable; +import java.io.IOException; +import java.net.*; +import java.nio.charset.StandardCharsets; +import java.nio.*; +import org.qtproject.qt.android.bindings.QtActivity; +import android.app.Service; +import java.time.Instant; +import java.time.Duration; + +public class WaterLevelService extends Service { + + private static float alert_thres_distance = 1000000; + + public class UDPCommandClient implements Runnable { + private DatagramSocket socket_cmd_recv; + private DatagramSocket socket_cmd_send; + @Override + public void run() { + Log.d("UDP Service", "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"); + while (!Thread.currentThread().isInterrupted()) { + byte[] buffer = new byte[1024]; + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + socket_cmd_recv.receive(packet); + String str = new String(buffer, StandardCharsets.UTF_8); + if(str.startsWith("", ""); + alert_thres_distance = Float.parseFloat(str); + } + Log.d("UDP Service ", "Received command packet " + str); + + } + } + catch (IOException e) { + Log.e("UDP Service", "Socket Error", e); + } + } + + } + + public class UDPClient implements Runnable { + private DatagramSocket socket_recv; + private DatagramSocket socket_send; + private Instant prevAlertTimestamp; + private boolean started = false; + + @Override + public void run() { + Log.d("UDP Service", "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.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"); + + + NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + String alertChannelId = "water_alert_channel"; + NotificationChannel channel = new NotificationChannel( + alertChannelId, + "Water Level Alerts", + NotificationManager.IMPORTANCE_HIGH + ); + manager.createNotificationChannel(channel); + + try { + multicastLock.acquire(); + wakeLock.acquire(); + socket_recv = new DatagramSocket(8176); + socket_send = new DatagramSocket(8177); + socket_recv.setBroadcast(true); + Log.d("UDP Service", "Created Sockets"); + 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); + boolean duration_passed = false; + if(!started) { + duration_passed = true; + prevAlertTimestamp = Instant.now(); + } + else { + Instant currentTimestamp = Instant.now(); + Duration duration = Duration.between(prevAlertTimestamp, currentTimestamp); + if(duration.toMinutes() > 30) { + duration_passed = true; + } + prevAlertTimestamp = currentTimestamp; + } + if(f > alert_thres_distance && alert_thres_distance < 999999 && duration_passed) { + Notification.Builder builder = new Notification.Builder(WaterLevelService.this, alertChannelId) + .setSmallIcon(android.R.drawable.ic_dialog_alert) + .setContentTitle("Water Level Sensor") + .setContentText("Water Level is below alert threshold") + .setPriority(Notification.PRIORITY_HIGH); + manager.notify(2, builder.build()); + Log.d("UDP Service", "Notification sent"); + } + DatagramPacket send_packet = new DatagramPacket(buffer, buffer.length, InetAddress.getByName("localhost"), 8180); + socket_send.send(send_packet); + Log.d("UDP Service ", "Sent packet"); + } + } + catch (IOException e) { + Log.e("UDP Service", "Socket Error", e); + } finally { + if (multicastLock.isHeld()) + multicastLock.release(); + if (wakeLock.isHeld()) + wakeLock.release(); + if (socket_recv != null) + socket_recv.close(); + } + } + } + + public void startWaterLevelService() { + Log.d("UDP Service", "Starting the service from Activity"); + + Intent intent = new Intent(this, WaterLevelService.class); + + startForegroundService(intent); + } + public WaterLevelService() { + super(); + Log.i("UDP Service", "Class is created"); + } + + @Override + public android.os.IBinder onBind(android.content.Intent intent) { return null; } + + @Override + public void onCreate() { + super.onCreate(); + Notification notification = createNotification(); + startForeground(1, notification); + Log.i("UDP Service", "Creating Service"); + + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.i("UDP Service", "Destroying Service"); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Notification notification = createNotification(); + startForeground(1, notification); + + Log.d("UDP Service", "Actually starting the thread"); + + int ret = super.onStartCommand(intent, flags, startId); + + (new Thread(new UDPClient())).start(); + (new Thread(new UDPCommandClient())).start(); + + return START_STICKY; + } + + + private Notification createNotification() { + String channelId = "water_level_channel"; + + NotificationManager manager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + NotificationChannel channel = new NotificationChannel( + channelId, + "Water Level Service", + NotificationManager.IMPORTANCE_LOW + ); + manager.createNotificationChannel(channel); + + return new Notification.Builder(this, channelId) + .setContentTitle("Water Level Sensor Running") + .setContentText("Monitoring water level...") + .setSmallIcon(android.R.drawable.ic_menu_info_details) + .build(); + } + + public static void startQtAndroidService(QtActivity activity) { + Log.d("UDP Service", "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 new file mode 100644 index 0000000..98b114c --- /dev/null +++ b/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelServiceStarter.java @@ -0,0 +1,19 @@ +package org.qtproject.example.appwaterLevelSensor; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.util.Log; +import org.qtproject.qt.android.bindings.QtActivity; + +public class WaterLevelServiceStarter { + + public static void startService(Context context) { + Log.d("UDP Service", "Starting service via QtActivity instance"); + + Intent intent = new Intent(context, WaterLevelService.class); + + context.startForegroundService(intent); + } +} diff --git a/configinterface.cpp b/configinterface.cpp new file mode 100644 index 0000000..563cf68 --- /dev/null +++ b/configinterface.cpp @@ -0,0 +1,108 @@ +#include "configinterface.h" + +ConfigInterface::ConfigInterface(QObject *parent) + : QObject{parent} +{ + auto paths = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + qDebug() << "Got writable path as: " << paths; + std::filesystem::path dir = paths.toStdString(); + if(!QDir(dir).exists()) { + QDir().mkpath(paths); + } + std::filesystem::path path = dir / "config.json"; + QFile config(path); + if(config.exists()) { + qDebug() << "Loading config file...."; + bool ok = config.open(QFile::ReadOnly); + if(!ok) { + qDebug() << config.errorString(); + return; + } + 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(); + setMax(max); + setMin(min); + setAlert(alert); + } + else { + qDebug() << "File does not exist creating one..."; + QJsonObject top; + top["max_threshold"] = "125"; + top["min_threshold"] = "25"; + top["alert_threshold"] = "100"; + QByteArray data = QJsonDocument(top).toJson(); + bool ok = config.open(QFile::WriteOnly); + if(!ok) { + qDebug() << config.errorString(); + return; + } + config.write(data); + config.flush(); + config.close(); + } +} + +void ConfigInterface::saveConfig(QString max, QString min, QString alert) +{ + auto paths = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + qDebug() << "Got writable path as: " << paths; + std::filesystem::path path = paths.toStdString(); + QFile config(path / "config.json"); + qDebug() << "Writing to config file..." << max << min << alert; + QJsonObject top; + top["max_threshold"] = max; + top["min_threshold"] = min; + top["alert_threshold"] = alert; + setMax(max); + setMin(min); + setAlert(alert); + QByteArray data = QJsonDocument(top).toJson(); + config.open(QIODevice::WriteOnly); + config.write(data); + config.flush(); + config.close(); +} + +QString ConfigInterface::alert() const +{ + return m_alert; +} + +void ConfigInterface::setAlert(const QString &newAlert) +{ + if (m_alert == newAlert) + return; + m_alert = newAlert; + emit alertChanged(); +} + +QString ConfigInterface::max() const +{ + return m_max; +} + +void ConfigInterface::setMax(const QString &newMax) +{ + if (m_max == newMax) + return; + m_max = newMax; + emit maxChanged(); +} + +QString ConfigInterface::min() const +{ + return m_min; +} + +void ConfigInterface::setMin(const QString &newMin) +{ + if (m_min == newMin) + return; + m_min = newMin; + emit minChanged(); +} diff --git a/configinterface.h b/configinterface.h new file mode 100644 index 0000000..edf60ee --- /dev/null +++ b/configinterface.h @@ -0,0 +1,45 @@ +#ifndef CONFIGINTERFACE_H +#define CONFIGINTERFACE_H + +#include +#include +#include +#include +#include +#include +#include +#include + +class ConfigInterface : public QObject +{ + Q_OBJECT + QML_ELEMENT + 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) +public: + explicit ConfigInterface(QObject *parent = nullptr); + QString alert() const; + void setAlert(const QString &newAlert); + + QString max() const; + void setMax(const QString &newMax); + + QString min() const; + void setMin(const QString &newMin); + +signals: + void configData(QString max, QString min, QString alert); + void alertChanged(); + void maxChanged(); + void minChanged(); + +public slots: + void saveConfig(QString max, QString min, QString alert); +private: + QString m_alert; + QString m_max; + QString m_min; +}; + +#endif // CONFIGINTERFACE_H diff --git a/images.qrc b/images.qrc new file mode 100644 index 0000000..2d11999 --- /dev/null +++ b/images.qrc @@ -0,0 +1,5 @@ + + + WaterTank1.png + + diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..ffff36f --- /dev/null +++ b/main.cpp @@ -0,0 +1,18 @@ +#include +#include + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + QObject::connect( + &engine, + &QQmlApplicationEngine::objectCreationFailed, + &app, + []() { QCoreApplication::exit(-1); }, + Qt::QueuedConnection); + engine.loadFromModule("waterLevelSensor", "Main"); + + return app.exec(); +} diff --git a/moving_container.h b/moving_container.h new file mode 100644 index 0000000..89a1cf0 --- /dev/null +++ b/moving_container.h @@ -0,0 +1,89 @@ +#ifndef MOVING_CONTAINER +#define MOVING_CONTAINER + + +using ld = long double; + +template class average { + struct info { + info *next = nullptr; + T data; + }; + info *head = nullptr, *tail = nullptr, *curr = nullptr; + ld avg = 0.0; + unsigned int count = 0; + unsigned int max_size = 10; + +public: + average(int max_size = 10) { + this->max_size = max_size; + } + + ~average() { + curr = head; + while(curr != nullptr) { + info *now = curr; + curr = curr->next; + delete now; + } + } + + unsigned int size() { + return this->count; + } + + double get_average() { + return this->avg; + } + + void recalculate_average() { + curr = head; + this->avg = 0; + this->count = 0; + while(curr != nullptr) { + info *now = curr; + auto t = now->data; + curr = curr->next; + count++; + avg = ((avg) * (ld) (count - 1) / (ld )(count)) + ((ld)(t) / (ld) count); + } + } + + void push_back(T t) { + if(head == nullptr) { + auto *i = new info(); + i->next = nullptr; + i->data = t; + head = i; + tail = i; + } + else { + info *i = new info(); + if(this->count == max_size) { + T old_value; + if(head->next) { + auto *temp = head; + head = head->next; + old_value = temp->data; + delete temp; + } + recalculate_average(); + tail->next = i; + i->next = nullptr; + i->data = t; + tail = i; + } + tail->next = i; + i->next = nullptr; + i->data = t; + tail = i; + } + count++; + avg = ((avg) * (ld) (count - 1) / (ld )(count)) + ((ld)(t) / (ld) count); + } +}; + + + + +#endif diff --git a/service.cpp b/service.cpp new file mode 100644 index 0000000..6b819b0 --- /dev/null +++ b/service.cpp @@ -0,0 +1,115 @@ +#include "service.h" + +#include +#include +#include +#include +#include +#include + +// Q_DECLARE_JNI_CLASS(Intent, "android/content/Intent") +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) { + jbyte *bytes = env->GetByteArrayElements(data, nullptr); + jsize len = env->GetArrayLength(data); + + // Process your data here + qDebug() << "Received data from Java broadcast! Length:" << len; + + + level = *((float *) (bytes)); + + qDebug() << "Level received as: " << level; + + env->ReleaseByteArrayElements(data, bytes, JNI_ABORT); +} + + +service::service(QObject *parent) + : QObject{parent} +{ + if(QSysInfo::productType() == "android") { + qDebug() << "Running android service"; + + 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); + } +} diff --git a/service.h b/service.h new file mode 100644 index 0000000..5b64765 --- /dev/null +++ b/service.h @@ -0,0 +1,15 @@ +#ifndef SERVICE_H +#define SERVICE_H + +#include + +class service : public QObject +{ + Q_OBJECT +public: + explicit service(QObject *parent = nullptr); + +signals: +}; + +#endif // SERVICE_H diff --git a/servicemain.cpp b/servicemain.cpp new file mode 100644 index 0000000..e0a684e --- /dev/null +++ b/servicemain.cpp @@ -0,0 +1,12 @@ +#include +#include +#include "service.h" + +int main(int argc, char *argv[]) +{ + qWarning() << "Service starting from a separate .so file"; + QAndroidService app(argc, argv); + service service; + + return app.exec(); +} \ No newline at end of file diff --git a/udplistener.cpp b/udplistener.cpp new file mode 100644 index 0000000..4978794 --- /dev/null +++ b/udplistener.cpp @@ -0,0 +1,161 @@ +#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->moveToThread(thread); + connect(worker, &Worker::datagramReceived, this, &UDPListener::datagramRecieved); + connect(worker, &Worker::socketError, this, &UDPListener::socketError); + connect(this, &UDPListener::startWorker, worker, &Worker::run); + thread->start(); + emit startWorker(); + timer = new QTimer(this); + timer->setInterval(10000); + 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 +{ + return m_distance; +} + +void UDPListener::setDistance(double newDistance) +{ + m_distance = newDistance; + emit distanceChanged(); +} + +void UDPListener::setAlert(float distance) +{ + if(!command_socket) { + command_socket = new QUdpSocket(this); + bool bind = command_socket->bind(8181); + if(bind) { + QNetworkDatagram data; + data.setDestination(QHostAddress::LocalHost, 8178); + QString s_data = ""; + while(s_data.toUtf8().size() != 1024) { + s_data += "="; + } + data.setData(s_data.toUtf8()); + command_socket->writeDatagram(data); + command_socket->flush(); + command_socket->close(); + } + } +} + +void UDPListener::socketError(QString err) +{ + qDebug() << "Socket error received: " << err; + if(err == "The bound address is already in use") { + err = "Port already in use."; + } + emit showError(err); +} + +void UDPListener::startService() +{ + +} + +void UDPListener::datagramRecieved(float distance) +{ + qDebug() << "Data received in main class: " << distance; + averages.push_back(distance); + prev_time_point = std::chrono::high_resolution_clock::now(); + emit distanceReceived(distance, QDateTime::currentDateTime().toString("dd/mm/yyyy hh:mm:ss.z ap")); + if(averages.size() >= 5) { + this->m_distance = averages.get_average(); + emit distanceChanged(); + } + if(averages.size() == 1) + emit connected(); + +} + +void UDPListener::destroy() +{ + qDebug() << "Destroying UDP Listener"; + timer->stop(); + thread->exit(0); + thread->deleteLater(); + worker->deleteLater(); + timer->deleteLater(); + qDebug() << "Deinitialized UDP Listener"; +} + +void UDPListener::connectionTimerTimedOut() +{ + using namespace std::chrono_literals; + auto current_time_point = std::chrono::high_resolution_clock::now(); + if((current_time_point - prev_time_point) > 10s) { + qDebug() << "Disconnected::" << current_time_point.time_since_epoch() << " " << prev_time_point.time_since_epoch(); + } + else { + qDebug() << "All fine"; + } +} + +Worker::Worker(QObject *parent) +{ + +} + +void Worker::run() +{ + using namespace std::chrono_literals; + socket = new QUdpSocket(this); +#ifdef Q_OS_ANDROID + bool bind = socket->bind(QHostAddress::Any, 8180); +#else + bool bind = socket->bind(QHostAddress("192.168.1.255"), 8176); +#endif + if(!bind) + { + qDebug() << "UI Worker socket bind failed" << bind << socket->errorString(); + emit socketError(socket->errorString()); + } + + 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]; + } + 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); + } +} diff --git a/udplistener.h b/udplistener.h new file mode 100644 index 0000000..0eefa61 --- /dev/null +++ b/udplistener.h @@ -0,0 +1,61 @@ +#ifndef UDPLISTENER_H +#define UDPLISTENER_H + +#include +#include +#include +#include +#include +#include +#include +#include "moving_container.h" + +class Worker: public QObject { + Q_OBJECT + QUdpSocket *socket = nullptr; +public: + explicit Worker(QObject *parent = nullptr); +signals: + void socketError(QString error); + void datagramReceived(float distance); +public slots: + void run(); +private slots: + void socketDataReceived(); +}; + +class UDPListener : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(double distance READ distance WRITE setDistance NOTIFY distanceChanged FINAL) +public: + explicit UDPListener(QObject *parent = nullptr); + double distance() const; + void setDistance(double newDistance); +signals: + void showError(QString err); + void distanceChanged(); + void startWorker(); + void connected(); + void disconnected(); + void distanceReceived(double dist, QString timestamp); +public slots: + void setAlert(float distance); + void socketError(QString err); + void startService(); + void datagramRecieved(float distance); +private slots: + void destroy(); + void connectionTimerTimedOut(); +private: + QUdpSocket *command_socket = nullptr; + std::chrono::time_point prev_time_point; + average averages; + double m_distance; + Worker *worker = nullptr; + QThread *thread = nullptr; + QTimer *timer = nullptr; +}; + +#endif // UDPLISTENER_H