diff --git a/.gitignore b/.gitignore index c9e89eb..aa3808c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,82 @@ -build/* -.qtcreator/* +# 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 index 957c9c2..2b80b0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,69 +1,111 @@ cmake_minimum_required(VERSION 3.16) -project(WaterLevelRewrite VERSION 0.1 LANGUAGES CXX) +project(waterLevelSensor VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTORCC ON) -find_package(Qt6 REQUIRED COMPONENTS Gui Core Quick Multimedia) +find_package(Qt6 REQUIRED COMPONENTS Quick Network CorePrivate) -qt_standard_project_setup(REQUIRES 6.10) +qt_standard_project_setup(REQUIRES 6.11) -if (EXISTS "${CMAKE_CURRENT_LIST_DIR}/importedcontent/CMakeLists.txt") - add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/importedcontent) -endif() - -qt_add_executable(appWaterLevelRewrite +qt_add_executable(appwaterLevelSensor main.cpp + images.qrc ) -qt_add_qml_module(appWaterLevelRewrite - URI WaterLevelRewrite + +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 + ${CMAKE_CURRENT_SOURCE_DIR}/android) + + +target_link_libraries(service + PRIVATE + Qt::Core + Qt::Network + Qt::CorePrivate + Qt::Quick +) +qt_add_qml_module(appwaterLevelSensor + URI waterLevelSensor QML_FILES Main.qml - WaterTank.qml - SettingsBarIcon.qml - SettingsPopup.qml - ConnectButton.qml - SOURCES - configinterface.h - configinterface.cpp - requestworker.h - requestworker.cpp - wifihelper.h - wifihelper.cpp - RESOURCES - media.qrc - QML_FILES CustomCheckBox.qml - QML_FILES RequestResponsePopup.qml - QML_FILES ColorInput.qml + RESOURCES + RESOURCES images.qrc + SOURCES moving_container.h + SOURCES configinterface.h configinterface.cpp + SOURCES udplistener.h udplistener.cpp + SOURCES service.h + RESOURCES README.md +) +add_android_openssl_libraries(service appwaterLevelSensor) +target_link_libraries(appwaterLevelSensor + PRIVATE Qt6::Quick Qt6::Network service ) -if(APPLE) - set_target_properties(appWaterLevelRewrite PROPERTIES - # MACOSX_BUNDLE_GUI_IDENTIFIER com.example.appWaterLevelRewrite - MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} - MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} - MACOSX_BUNDLE TRUE - ) +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(WIN32) - set_target_properties(appWaterLevelRewrite PROPERTIES - WIN32_EXECUTABLE TRUE - ) -endif() - -set_target_properties(appWaterLevelRewrite PROPERTIES - QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android" +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 ) -target_link_libraries(appWaterLevelRewrite - PRIVATE Qt6::Quick Qt6::Multimedia Qt6::Gui Qt6::Core +# 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 ) -install(TARGETS appWaterLevelRewrite +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/ColorInput.qml b/ColorInput.qml deleted file mode 100644 index a4cebe7..0000000 --- a/ColorInput.qml +++ /dev/null @@ -1,21 +0,0 @@ -import QtQuick -import QtQuick.Effects -import QtQuick.Controls -import QtQuick.Controls.Material - -TextField { - id: color_text - text: "F05FAF5F" - property alias color_text: base.color - Rectangle { - id: base - anchors { - right: parent.right - rightMargin: 10 - verticalCenter: parent.verticalCenter - } - width: 10 - height: parent.height / 2 - color: "#" + color_text.text - } -} diff --git a/ConnectButton.qml b/ConnectButton.qml deleted file mode 100644 index dd058bc..0000000 --- a/ConnectButton.qml +++ /dev/null @@ -1,63 +0,0 @@ -import QtQuick -import QtQuick.Effects -import QtQuick.Controls -import QtQuick.Controls.Material - -Rectangle { - id: root - property color accent_color: "#00000000" - property color font_color: "#00000000" - property int font_enlarged_text_size: 20 - property int font_normal_text_size: 16 - property string text: "Connect" - property string text2: "Stop" - property int mode: 0 - signal connect; - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - } - - Text { - anchors.centerIn: parent - font.pointSize: root.font_enlarged_text_size - color: root.font_color - text: mode ? root.text2 : root.text - horizontalAlignment: Text.AlignHCenter - } - - radius: 5 - border { - color: root.accent_color - width: 0.5 - } - 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 = root.color - root.color = "#5A1F1F1F" - } - onExited: { - root.color = settings_rect_entered_prev - } - onPressed: { - settings_rect_pressed_prev = root.color - root.color = "#5A1F4F1F" - root.connect(); - switchMode(); - } - onReleased: { - root.color = settings_rect_pressed_prev - } - } - function switchMode() { - if(root.mode === 0) - root.mode = 1 - else - root.mode = 0; - } -} diff --git a/CustomCheckBox.qml b/CustomCheckBox.qml deleted file mode 100644 index 0cdbb38..0000000 --- a/CustomCheckBox.qml +++ /dev/null @@ -1,17 +0,0 @@ -import QtQuick -import QtQuick.Controls.Basic - -CheckBox { - id: control - property color font_color: "#0000FF00" - property int font_point_size: 22 - font.pointSize: font_point_size - checked: true - contentItem: Text { - text: control.text - font: control.font - color: control.font_color - verticalAlignment: Text.AlignVCenter - leftPadding: control.indicator.width + control.spacing - } -} diff --git a/Main.qml b/Main.qml index 28693c5..e7844b9 100644 --- a/Main.qml +++ b/Main.qml @@ -1,59 +1,71 @@ import QtQuick -import WaterLevelRewrite +import waterLevelSensor import QtQuick.Effects import QtQuick.Controls import QtQuick.Controls.Material Window { - width: 960 - height: 1400 + width: 640 + height: 1080 visible: true - title: qsTr("Water Level Rewrite") + 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" - property color water_color_above_alert: "#F00FAF0F" - property color background_color: "#1F1F1F" Material.accent: root.accent_color property int font_enlarged_text_size: 20 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" - property string status: "Initializing" - property string connection_text_string: "Reading WiFi Status..." - property string alarm_status_text: "" - property double water_tank_height_factor: 1.3 - property double water_tank_width_factor: 2 - property var root_config: ({}) - property var base_config: { - "max": 0, - "min": 0, - "alert": 0, - "interval": 0, - "url": "none", - "water_tank_width": 2, - "water_tank_height": 1.3, - "rate_of_fill_enable": true, - "current_reading_enable": true, - "percentage_enable": true, - "filling_status_enable": true, - "expected_to_fill_enable": true, - "accent_color": "F05FAF5F", - "dark_accent_color": "FF5F7F5F", - "danger_color": "F0AF2F1F", - "water_color": "F00F7FAF", - "water_color_above_alert": "F00FAF0F", - "background_color": "1F1F1F", - "font_normal_text_size": 16, - "font_enlarged_text_size": 20 + // 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 - color: root.background_color - id: root_rect + id: background + color: "#DF0F0F0F" Rectangle { id: top_bar height: Math.min(root.height / 12, 100) @@ -64,321 +76,729 @@ Window { topMargin: 25 } color: "#0A0F0F0F" - SettingsBarIcon { - id: settings_icon + + Rectangle { + id: settings_rect height: top_bar.height - 20 - width: height + width: top_bar.width / 12 anchors { top: parent.top - right: parent.right + left: parent.left bottomMargin: 5 topMargin: 5 leftMargin: 5 } + Behavior on color { + ColorAnimation { + duration: 200 + } + } + color: "#0A0F0F0F" - radius: 100 - accent_color: root.accent_color - onOpen: { - settings_popup.open() + 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 + } } } - SettingsBarIcon { - height: top_bar.height - 20 - width: height + + + Rectangle { + property color old_color: "#0F0F0F0F" + id: status_indicator + width: 20 + height: 20 + radius: 20 anchors { top: parent.top - left: parent.left + right: parent.right + topMargin: 20 bottomMargin: 5 - topMargin: 10 leftMargin: 5 + rightMargin: 20 } - text: "⇅" - color: "#0A0F0F0F" - radius: 100 - accent_color: root.accent_color - onOpen: { - request_response_popup.open() + color: "#F0AF2F1F" + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true } } - } - - RequestResponsePopup { - id: request_response_popup - accent_color: root.accent_color - font_enlarged_text_size: 20 - - } - - SettingsPopup { - id: settings_popup - accent_color: root.accent_color - background_color: root.background_color - font_enlarged_text_size: root.font_enlarged_text_size - font_normal_text_size: root.font_normal_text_size - function setConfig(new_config: var) { - settings_popup.config.max = new_config.max - settings_popup.config.min = new_config.min - settings_popup.config.alert = new_config.alert - settings_popup.config.url = new_config.url + 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" } - onSaveConfig: (config) => { - console.log("Saving config: ", config) - config_interface.saveConfig(config); - } - onDeleteConfig: () => { - config_interface.deleteConfig(); - } - } - - Text { - id: connection_text - anchors { - top: top_bar.bottom - topMargin: 20 - horizontalCenter: parent.horizontalCenter - } - text: root.connection_text_string - color: root.accent_color - font.pointSize: root.font_normal_text_size - } - - WaterTank { - id: tank - anchors { - top: connection_text.bottom - horizontalCenter: parent.horizontalCenter - topMargin: 40 - } - height: (root_rect.height) / root.water_tank_height_factor - width: root_rect.width / root.water_tank_width_factor - base_color: root.background_color - distance: 0 - tank_lid_color: "#F0121212" - tank_base_color: "#0FFFFFFF" - water_level_color: root.water_color - tank_base_border_color: "#F0121212" - water_color_above_alert: root.water_color_above_alert - min_thres_input: 0 - max_thres_input: 0 - text_color: root.accent_color - font_normal_text_size: root.font_normal_text_size - font_enlarged_text_size: root.font_enlarged_text_size - } - - - Text { - id: alarm_status - text: root.alarm_status_text - visible: (root.alarm_status_text.length > 0 ) ? true : false - anchors { - bottom: status.top - bottomMargin: 10 - horizontalCenter: parent.horizontalCenter - } - horizontalAlignment: Text.AlignHCenter - color: root.accent_color - font.pointSize: root.font_normal_text_size - } - - Text { - id: status - text: root.status - anchors { - bottom: button_base.top - bottomMargin: 20 - horizontalCenter: parent.horizontalCenter - } - horizontalAlignment: Text.AlignHCenter - color: root.accent_color - font.pointSize: root.font_normal_text_size } Rectangle { - id: button_base - color: "#00000000" - width: parent.width / 1.5 - height: parent.height / 20 + id: base + color: "#0A0F0F0F" + width: parent.width + height: parent.height - top_bar.height anchors { - horizontalCenter: parent.horizontalCenter + top: top_bar.bottom bottom: parent.bottom - bottomMargin: 30 + horizontalCenter: parent.horizontalCenter } - ConnectButton { - id: connect_button - width: parent.width / 2.1 - height: parent.height - font_color: root.accent_color - font_enlarged_text_size: root.font_enlarged_text_size - font_normal_text_size: root.font_normal_text_size - color: root.background_color - accent_color: root.accent_color - anchors { - left: parent.left - leftMargin: 5 - bottom: parent.bottom - bottomMargin: 5 + + Popup { + id: settings_popup + background: Rectangle { + color: "#F0121212" + radius: 5 + border.color: root.accent_color + border.width: 1 } - onConnect: { - if(connect_button.mode === 0) - { - requester.start(); - root.status = "Waiting to Send."; + height: Math.max(parent.height / 1.2, contentHeight) + width: parent.width / 1.1 + 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 } - else { - root.status = "Stopped."; - requester.stop(); + + + Text { + id: max_thres_label + text: "Max Threshold: " + anchors { + 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.pointSize: root.font_normal_text_size + } + + TextField { + id: max_thres_input + text: config.max + anchors { + right: parent.right + rightMargin: 5 + top: settings_label.bottom + topMargin: 20 + } + width: parent.width / 2 + color: root.accent_color + font.pointSize: root.font_normal_text_size + } + + + Text { + id: min_thres_label + text: "Min Threshold: " + anchors { + 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.pointSize: root.font_normal_text_size + } + + TextField { + id: min_thres_input + text: config.min + anchors { + right: parent.right + rightMargin: 5 + top: max_thres_label.bottom + topMargin: 20 + } + width: parent.width / 2 + color: root.accent_color + font.pointSize: root.font_normal_text_size + } + + + Text { + id: alert_thres_label + text: "Alert Percentage: " + anchors { + 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.pointSize: root.font_normal_text_size + } + + TextField { + id: alert_thres_input + text: config.alert + anchors { + right: parent.right + rightMargin: 5 + top: min_thres_label.bottom + topMargin: 20 + } + width: parent.width / 2 + color: root.accent_color + 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 { + id: settings_error_label + property string settings_error_text: "" + text: "Error: " + settings_error_text + visible: settings_error_text === "" ? false : true + anchors { + topMargin: 20 + top: http_url_label.bottom + bottom: reset_averages.top + bottomMargin: 10 + left: parent.left + horizontalCenter: parent.horizontalCenter + } + horizontalAlignment: Text.AlignHCenter + color: root.danger_color + 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 + anchors { + topMargin: 10 + bottom: parent.bottom + 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: "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); + 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; + } + 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.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 + } + } + } } } - ConnectButton { - id: start_alarm - width: parent.width / 2.1 - height: parent.height - font_color: root.accent_color - font_enlarged_text_size: root.font_enlarged_text_size - font_normal_text_size: root.font_normal_text_size - color: root.background_color - accent_color: root.accent_color - text: "Start Alarm" - text2: "Stop Alarm" + Rectangle { + id: tank_lid + color: "#F0121212" anchors { - left: connect_button.right - leftMargin: 10 - bottom: parent.bottom + 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 + 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 } - onConnect: { - if(start_alarm.mode === 0) - { - requester.startAlarm(); - root.alarm_status_text = "Started Alarm; Waiting for Trigger: " + root.root_config.alert + Behavior on color { + ColorAnimation { + duration: 200 } - else { - requester.stopAlarm(); - root.alarm_status_text = ""; + } + 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: 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 + property string error_string: "" + text: "Error: " + error_string + visible: error_string === "" ? false : true; + anchors { + top: last_updated_text.bottom + topMargin: 10 + horizontalCenter: parent.horizontalCenter + } + horizontalAlignment: Text.AlignHCenter + color: root.danger_color + font.pixelSize: root.font_normal_text_size + } + } - - } - RequestWorker { - id: requester - onStatusChanged: () => { - root.status = requester.status; - } - onNetworkReply: (water_level, fill_rate, expected, data) => { - tank.setDistance(water_level); - tank.rate_of_fill = fill_rate; - tank.expected_to_fill = expected; - status.color = root.accent_color; - request_response_popup.request_response_string = data; - } - onRequestFailed: (data) => { - status.color = root.danger_color; - request_response_popup.request_response_string = data; - } - onConnectionText: (text, error) => { - root.connection_text_string = text - if(!error) { - connection_text.color = root.accent_color - } - else { - connection_text.color = root.danger_color - } + Timer { + id: status_indicator_timer + running: false + repeat: false + triggeredOnStart: false + interval: 300 + onTriggered: { + status_indicator.color = status_indicator.old_color; } } ConfigInterface { - id: config_interface - fileName: "config.json" - onConfig: (config) => { - settings_popup.config = config; - console.log("Loading initial config"); - setConfig(config); - if(root.status === "Initializing") { - root.status = "Press Connect."; - } + id: config + onConfigData: (max, min, alert, mode, url) => { + console.log("Got config data as: ", max, min, alert, mode, url); } - onConfigDeleted: () => { - let config = base_config; - settings_popup.config = config; - console.log("Loading initial config"); - setConfig(config); - if(root.status === "Initializing") { - root.status = "Press Connect."; - } - } - - function setConfig(config) { - tank.setConfig(config); - requester.setConfig(config); - root.root_config = config; - if(config.water_tank_height === undefined) { - config.water_tank_height = 2 - } - if(config.water_tank_width === undefined) { - config.water_tank_width = 2.2 - } - if(config.rate_of_fill_enable === undefined) { - config.rate_of_fill_enable = true - } - if(config.current_reading_enable === undefined) { - config.current_reading_enable = true - } - if(config.percentage_enable === undefined) { - config.percentage_enable = true - } - if(config.filling_status_enable === undefined) { - config.filling_status_enable = true - } - if(config.expected_to_fill_enable === undefined) { - config.expected_to_fill_enable = true - } - if(config.font_enlarged_text_size === undefined) { - config.font_enlarged_text_size = 20 - } - if(config.font_normal_text_size === undefined) { - config.font_normal_text_size = 16 - } - if(config.accent_color === undefined) { - config.accent_color = "F05FAF5F" - } - if(config.danger_color === undefined) { - config.danger_color = "F0AF2F1F" - } - if(config.dark_accent_color === undefined) { - config.dark_accent_color = "FF5F7F5F" - } - if(config.water_color === undefined) { - config.water_color = "F00F7FAF" - } - if(config.water_color_above_alert === undefined) { - config.water_color_above_alert = "F00FAF0F" - } - if(config.background_color === undefined) { - config.background_color = "1F1F1F" - } - root.water_tank_height_factor = config.water_tank_height - root.water_tank_width_factor = config.water_tank_width - root.accent_color = "#" + config.accent_color - root.danger_color = "#" + config.danger_color - root.dark_accent_color = "#" + config.dark_accent_color - root.water_color = "#" + config.water_color - root.water_color_above_alert = "#" + config.water_color_above_alert - root.background_color = "#" + config.background_color - root.font_enlarged_text_size = config.font_enlarged_text_size - root.font_normal_text_size = config.font_normal_text_size - + onModeChanged: { + root.mode = mode; } } - onClosing: { - requester.stopAlarm(); + 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 + + onShowError: (err) => { + 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 >= 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); + 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; + } + 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; + 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.setConfig(alert_effective_reading, flip_bits_box.checked, root.mode, http_url_input.text); + } + + distance_count += 1; + } + } + + onClosing: (close)=> { + if(Qt.platform.os === "android"){ + // close.accepted = false + // listener.moveAppToBackground(); + // console.log("Moving to backgound"); + close.accepted = true; + } else { + close.accepted = true + } + } + + function dataBlink() { + 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; + 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/README.md b/README.md new file mode 100644 index 0000000..7198d51 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Water Level Sensor + +Builds using QtCreator, use Qt Version 6.9.
+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.
+ + +
+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/RequestResponsePopup.qml b/RequestResponsePopup.qml deleted file mode 100644 index 468bcda..0000000 --- a/RequestResponsePopup.qml +++ /dev/null @@ -1,30 +0,0 @@ -import QtQuick -import QtQuick.Effects -import QtQuick.Controls -import QtQuick.Controls.Material - -Popup { - id: request_response_popup - property color accent_color: "#00000000" - property int font_enlarged_text_size: 20 - property int font_normal_text_size: 16 - property string request_response_string: "" - background: Rectangle { - color: "#F0121212" - radius: 5 - border.color: request_response_popup.accent_color - border.width: 1 - } - height: Math.max(parent.height / 1.6, contentHeight) - width: parent.width / 1.2 - anchors.centerIn: Overlay.overlay - ScrollView { - id: view - anchors.fill: parent - TextArea { - text: request_response_string - font.pointSize: font_normal_text_size - color: request_response_popup.accent_color - } - } -} diff --git a/SettingsBarIcon.qml b/SettingsBarIcon.qml deleted file mode 100644 index 35072b7..0000000 --- a/SettingsBarIcon.qml +++ /dev/null @@ -1,49 +0,0 @@ -import QtQuick -import QtQuick.Effects -import QtQuick.Controls -import QtQuick.Controls.Material - -Rectangle { - id: settings_rect - signal open; - property color accent_color: "#00000000" - property int font_enlarged_text_size: 20 - property string text: "⚙" - Behavior on color { - ColorAnimation { - duration: 200 - } - } - Text { - id: settings_text - text: settings_rect.text - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: settings_rect.accent_color - font.pointSize: settings_rect.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_rect.open(); - } - onReleased: { - settings_rect.color = settings_rect_pressed_prev - } - } -} diff --git a/SettingsPopup.qml b/SettingsPopup.qml deleted file mode 100644 index 6a97c2b..0000000 --- a/SettingsPopup.qml +++ /dev/null @@ -1,594 +0,0 @@ -import QtQuick -import QtQuick.Effects -import QtQuick.Controls -import QtQuick.Controls.Material - -Popup { - id: settings_popup - signal saveConfig(config: var) - signal deleteConfig() - property color accent_color: "#00000000" - property int font_enlarged_text_size: 20 - property int font_normal_text_size: 16 - property color background_color: "#00000000" - property var config: { - "max": 0, - "min": 0, - "alert": 0, - "interval": 0, - "url": "none", - "water_tank_width": 2, - "water_tank_height": 1.3, - "rate_of_fill_enable": true, - "current_reading_enable": true, - "percentage_enable": true, - "filling_status_enable": true, - "expected_to_fill_enable": true, - "accent_color": "F05FAF5F", - "dark_accent_color": "FF5F7F5F", - "danger_color": "F0AF2F1F", - "water_color": "F00F7FAF", - "water_color_above_alert": "F00FAF0F", - "background_color": "1F1F1F", - "font_normal_text_size": 16, - "font_enlarged_text_size": 20 - } - - background: Rectangle { - color: "#F0121212" - radius: 5 - border.color: settings_popup.accent_color - border.width: 1 - } - height: Math.max(parent.height / 1.6, contentHeight) - width: parent.width / 1.2 - anchors.centerIn: Overlay.overlay - Rectangle { - id: settings_base - anchors.fill: parent - color: "#F0121212" - - ScrollView { - id: rect_base_scroll_view - anchors { - top: parent.top - horizontalCenter: parent.horizontalCenter - bottom: save_config.top - bottomMargin: 10 - } - width: parent.width - clip: true - - // Use contentWidth and contentHeight to define the scrollable area - contentWidth: availableWidth - contentHeight: settings_column.height - - Column { - id: settings_column - width: rect_base_scroll_view.width - spacing: 20 - - Text { - id: settings_label - text: "Settings" - width: parent.width - horizontalAlignment: Text.AlignHCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_enlarged_text_size - } - - // Max Threshold - Row { - width: parent.width - spacing: 10 - Text { - text: "Max Threshold: " - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: max_thres_input.verticalCenter - } - - } - TextField { - id: max_thres_input - text: config.max - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - } - } - - // Min Threshold - Row { - width: parent.width - spacing: 10 - Text { - text: "Min Threshold: " - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: min_thres_input.verticalCenter - } - - } - TextField { - id: min_thres_input - text: config.min - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - } - } - - // Alert Threshold - Row { - width: parent.width - spacing: 10 - Text { - text: "Alert Threshold: " - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: alert_thres_input.verticalCenter - } - - } - TextField { - id: alert_thres_input - text: config.alert - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - } - } - - // Update Interval - Row { - width: parent.width - spacing: 10 - Text { - text: "Update Interval: " - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: interval_input.verticalCenter - } - } - TextField { - id: interval_input - text: config.interval - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - } - } - - // URL - Row { - width: parent.width - spacing: 10 - Text { - text: "Url: " - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: http_url_input.verticalCenter - } - - } - TextField { - id: http_url_input - text: config.url - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - cursorPosition: 0 - } - } - - // Tank Image Width - Row { - width: parent.width - spacing: 10 - Text { - text: "Tank Image Width" - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: water_tank_width_input.verticalCenter - } - - } - TextField { - id: water_tank_width_input - text: String(config.water_tank_width) - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - cursorPosition: 0 - } - } - - // Tank Image Height - Row { - width: parent.width - spacing: 10 - Text { - text: "Tank Image Height" - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: water_tank_height_input.verticalCenter - } - } - TextField { - id: water_tank_height_input - text: String(config.water_tank_height) - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - cursorPosition: 0 - } - } - - // Accent Color - Row { - width: parent.width - spacing: 10 - Text { - text: "Accent Color" - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: accent_color_input.verticalCenter - } - } - ColorInput { - id: accent_color_input - text: config.accent_color - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - } - } - - // Dark Accent Color - Row { - width: parent.width - spacing: 10 - Text { - text: "Dark Accent Color" - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: dark_accent_color_input.verticalCenter - } - } - ColorInput { - id: dark_accent_color_input - text: config.dark_accent_color - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - } - } - - // Danger Color - Row { - width: parent.width - spacing: 10 - Text { - text: "Danger Text Color" - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: danger_color_input.verticalCenter - } - } - ColorInput { - id: danger_color_input - text: config.danger_color - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - } - } - - // Water Color - Row { - width: parent.width - spacing: 10 - Text { - text: "Water Color" - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: water_color_input.verticalCenter - } - } - ColorInput { - id: water_color_input - text: config.water_color - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - } - } - - // Alert Water Color - Row { - width: parent.width - spacing: 10 - Text { - text: "Alert Water Color" - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: water_color_above_alert_input.verticalCenter - } - } - ColorInput { - id: water_color_above_alert_input - text: config.water_color_above_alert - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - } - } - - // Background Color - Row { - width: parent.width - spacing: 10 - Text { - text: "Backgound Color" - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: background_color_input.verticalCenter - } - } - ColorInput { - id: background_color_input - text: config.background_color - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - } - } - - // Normal Font Size - Row { - width: parent.width - spacing: 10 - Text { - text: "Font Size" - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: font_normal_text_size_input.verticalCenter - } - } - TextField { - id: font_normal_text_size_input - text: String(config.font_normal_text_size) - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - cursorPosition: 0 - } - } - - // Header Font Size - Row { - width: parent.width - spacing: 10 - Text { - text: "Header Font Size" - width: parent.width * 0.4 - verticalAlignment: Text.AlignVCenter - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - anchors { - verticalCenter: font_enlarged_text_size_input.verticalCenter - } - } - TextField { - id: font_enlarged_text_size_input - text: String(config.font_enlarged_text_size) - width: parent.width * 0.55 - color: settings_popup.accent_color - font.pointSize: settings_popup.font_normal_text_size - cursorPosition: 0 - } - } - - // CheckBox - CustomCheckBox { - id: current_reading_enable - checked: config.current_reading_enable - font_color: settings_popup.accent_color - font_point_size: settings_popup.font_normal_text_size - text: "Enable Current Reading" - leftPadding: 0 - } - - // CheckBox - CustomCheckBox { - id: percentage_enable - checked: config.percentage_enable - font_color: settings_popup.accent_color - font_point_size: settings_popup.font_normal_text_size - text: "Enable Percentage Reading" - leftPadding: 0 - } - - // CheckBox - CustomCheckBox { - id: filling_status_enable - checked: config.filling_status_enable - font_color: settings_popup.accent_color - font_point_size: settings_popup.font_normal_text_size - text: "Enable Filling Status" - leftPadding: 0 - } - - // CheckBox - CustomCheckBox { - id: rate_of_fill_enable - checked: config.rate_of_fill_enable - font_color: settings_popup.accent_color - font_point_size: settings_popup.font_normal_text_size - text: "Enable Text Rate of Fill" - leftPadding: 0 - } - - // CheckBox - CustomCheckBox { - id: expected_to_fill_enable - checked: config.expected_to_fill_enable - font_color: settings_popup.accent_color - font_point_size: settings_popup.font_normal_text_size - text: "Enable Expected to fill" - leftPadding: 0 - } - - ConnectButton { - id: delete_config_button - width: parent.width / 1.5 - height: 80 - anchors { - horizontalCenter: parent.horizontalCenter - } - - font_color: settings_popup.accent_color - font_enlarged_text_size: settings_popup.font_enlarged_text_size - font_normal_text_size: settings_popup.font_normal_text_size - color: settings_popup.background_color - accent_color: settings_popup.accent_color - text: "Delete/Reset Config" - text2: text - onConnect: { - settings_popup.deleteConfig(); - } - } - } - } - Rectangle { - id: save_config - anchors { - topMargin: 10 - bottom: parent.bottom - horizontalCenter: parent.horizontalCenter - } - color: "#F0121212" - width: parent.width / 1.3 - height: 60 - border { - width: 1 - color: settings_popup.accent_color - } - radius: 3 - - Text { - anchors.centerIn: parent - text: "Save" - color: settings_popup.accent_color - font.pointSize: settings_popup.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(max_thres_input.text); - let min = parseFloat(min_thres_input.text); - let alert = parseFloat(alert_thres_input.text); - let url = http_url_input.text; - let interval = parseInt(interval_input.text); - let image_width = parseFloat(water_tank_width_input.text); - let image_height = parseFloat(water_tank_height_input.text); - - let current_reading = current_reading_enable.checked; - let percentage_reading = percentage_enable.checked - let filling_status = filling_status_enable.checked; - let rate_of_fill = rate_of_fill_enable.checked; - let expected_to_fill = expected_to_fill_enable.checked; - - let normal_font_size = parseInt(font_normal_text_size_input.text); - let enlarged_font_size = parseInt(font_enlarged_text_size_input.text); - - let accent_color = accent_color_input.text; - let dark_accent_color = dark_accent_color_input.text; - let danger_color = danger_color_input.text; - let water_color = water_color_input.text; - let water_color_above_alert = water_color_above_alert_input.text; - let background_color = background_color_input.text; - - var new_config = {"max": max, "min": min, "alert": alert, "url": url, - "interval": interval, - "water_tank_height": image_height, - "water_tank_width": image_width, - "current_reading_enable": current_reading, - "percentage_enable": percentage_reading, - "filling_status_enable": filling_status, - "rate_of_fill_enable": rate_of_fill, - "expected_to_fill_enable": expected_to_fill, - "font_normal_text_size": normal_font_size, - "font_enlarged_text_size": enlarged_font_size, - "accent_color": accent_color, - "dark_accent_color": dark_accent_color, - "danger_color": danger_color, - "water_color": water_color, - "water_color_above_alert": water_color_above_alert, - "background_color": background_color - } - settings_popup.saveConfig(new_config) - settings_popup.close(); - } - onReleased: { - save_config.color = settings_rect_pressed_prev - } - } - } - } -} \ No newline at end of file diff --git a/WaterTank.qml b/WaterTank.qml deleted file mode 100644 index 633fac8..0000000 --- a/WaterTank.qml +++ /dev/null @@ -1,204 +0,0 @@ -import QtQuick -import QtQuick.Effects -import QtQuick.Controls -import QtQuick.Controls.Material - -Item { - id: root - property color base_color: "#000F0F0F" - property color tank_lid_color: "#F0121212" - property color tank_base_color: "#00FFFFFF" - property color water_level_color: "#F00F7FAF" - property color water_color_above_alert: "#F00FAF0F" - property color tank_base_border_color: "#F0121212" - property color text_color: "#F05FAF5F" - property double height_max: tank_base.height - 10 - property int font_normal_text_size: 0 - property int font_enlarged_text_size: 0 - property double current_level: 0 - property double percentage_filled: 0 - property int distance: 0 - property double min_thres_input: 0 - property double max_thres_input: 0 - property double rate_of_fill: 0 - property double expected_to_fill: 0 - property double alert_threshold: 0 - property bool enable_current_reading: true - property bool enable_percentage_reading: true - property bool enable_filling_status: true - property bool enable_rate_of_fill: true - property bool enable_expected_to_fill: true - - Rectangle { - id: base - color: root.base_color - anchors.fill: parent - Rectangle { - id: tank_lid - color: root.tank_lid_color - anchors { - top: parent.top - horizontalCenter: tank_base.horizontalCenter - bottomMargin: -5 - } - radius: 10 - width: tank_base.width / 3 - height: tank_base.height / 15 - } - - Rectangle { - id: tank_base - color: root.tank_base_color - anchors { - top: tank_lid.bottom - horizontalCenter: parent.horizontalCenter - } - width: parent.width - height: parent.height * 0.55 - border.color: root.tank_base_border_color - border.width: 5 - radius: 4 - } - - Rectangle { - id: water_level - color: root.water_level_color - height: root.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 - } - } - } - - ScrollView { - id: rect_base_scroll_view - anchors { - margins: 5 - top: tank_base.bottom - bottom: parent.bottom - horizontalCenter: parent.horizontalCenter - } - width: parent.width - - contentWidth: parent.width - contentHeight: settings_column.implicitHeight - Column { - id: settings_column - spacing: 10 - width: rect_base_scroll_view.width - Text { - id: water_level_reading - text: "Current Reading: " + (root.distance / 10)+ " cm" - font.pointSize: root.font_normal_text_size - color: root.text_color - visible: enable_current_reading - anchors { - horizontalCenter: parent.horizontalCenter - } - horizontalAlignment: Text.AlignHCenter - } - - Text { - id: water_level_percentage - text: "Percentage: " + root.percentage_filled.toFixed(2) + " %" - font.pointSize: root.font_normal_text_size - visible: enable_percentage_reading - color: root.text_color - anchors { - horizontalCenter: parent.horizontalCenter - } - horizontalAlignment: Text.AlignHCenter - } - - Text { - id: fill_status - text: "Filling status: " + (( root.rate_of_fill > 0)? "Filling" : "Discharging") - font.pointSize: root.font_normal_text_size - color: root.text_color - visible: enable_filling_status - anchors { - horizontalCenter: parent.horizontalCenter - } - horizontalAlignment: Text.AlignHCenter - } - Text { - id: rate_of_fill_label - text: "Rate of fill: " + root.rate_of_fill + " cm/s" - font.pointSize: root.font_normal_text_size - color: root.text_color - visible: enable_rate_of_fill - anchors { - horizontalCenter: parent.horizontalCenter - } - horizontalAlignment: Text.AlignHCenter - } - Text { - id: expected_to_fill_label - text: "Expected to fill in: " + root.expected_to_fill + " s" - font.pointSize: root.font_normal_text_size - color: root.text_color - visible: (root.rate_of_fill > 0 && enable_expected_to_fill === true) ? true : false - anchors { - horizontalCenter: parent.horizontalCenter - } - horizontalAlignment: Text.AlignHCenter - } - } - } - - } - function setConfig(config: var) { - min_thres_input = config.min; - max_thres_input = config.max; - alert_threshold = config.alert; - enable_current_reading = config.current_reading_enable; - enable_percentage_reading = config.percentage_enable; - enable_filling_status = config.filling_status_enable; - enable_rate_of_fill = config.rate_of_fill_enable; - enable_expected_to_fill = config.expected_to_fill_enable; - - } - - function setDistance(distance) { - root.distance = distance; - let distance_f = distance / 10; - console.log("Distance changed to: ", distance_f); - let current_min_reading = Math.max(0, min_thres_input); - let effective_max_reading = Math.max(0, max_thres_input) - current_min_reading; - let current_reading = Math.max(0, distance_f - current_min_reading); - console.log("Current reading is: " + current_reading) - console.log("Current min reading is: " + current_min_reading) - console.log("Effective max reading is: " + effective_max_reading) - - let height_max_possible = root.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; - console.log("Current reading is: " + current_reading) - console.log("Normalized height is: " + normalized_height) - console.log("Percentage filled is: " + percentage_filled) - console.log("Alert threshold is: " + root.alert_threshold) - console.log("Is level above alert threshold: " + (distance_f <= root.alert_threshold)) - root.current_level = normalized_height; - root.percentage_filled = percentage_filled; - - if(distance_f <= root.alert_threshold) { - water_level.color = water_color_above_alert - } - else { - water_level.color = water_level_color - } - } -} - 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 index 2598885..ead6548 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -1,18 +1,18 @@ + + + - - - - - - + + + + + + + + + + @@ -38,11 +49,9 @@ - - + android:value="appwaterLevelSensor" /> 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..cc765d0 --- /dev/null +++ b/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelService.java @@ -0,0 +1,536 @@ +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; +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 CommandThread", "Command thread initialising"); + try { + 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); + 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); + { + 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) { + // showToast("IOException: " + e); + Log.e("UDP Service CommandThread", "IOException Error" + e); + } + } + + } + + public class UDPClient implements Runnable { + 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 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"); + + + 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(); + wifiLock.acquire(); + 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)); + Log.d("UDP Service ClientThread", "Created Sockets"); + int count = 0; + while (!Thread.currentThread().isInterrupted()) { + 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; + prevAlertTimestamp = Instant.now(); + } + else { + Instant currentTimestamp = Instant.now(); + Duration duration = Duration.between(prevAlertTimestamp, currentTimestamp); + if(duration.toMinutes() > 60) { + 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_LOW); + manager.notify(2, builder.build()); + Log.d("UDP Service ClientThread", "Notification sent"); + if(!started) { + started = true; + } + } + 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 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) { + // 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()) + wakeLock.release(); + if (socket_recv != null) + socket_recv.close(); + } + } + } + + public void startWaterLevelService() { + Log.d("UDP Service MainThread", "Starting the service from Activity"); + + Intent intent = new Intent(this, WaterLevelService.class); + + startForegroundService(intent); + } + public WaterLevelService() { + super(); + Log.i("UDP Service MainThread", "Class is created"); + } + + @Override + 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 MainThread", "Creating Service"); + + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.i("UDP Service MainThread", "Destroying Service"); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Notification notification = createNotification(); + startForeground(1, notification); + + + 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(); + + 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"; + + 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 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 new file mode 100644 index 0000000..9dccce9 --- /dev/null +++ b/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelServiceStarter.java @@ -0,0 +1,118 @@ +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 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"); + + // if(!isMyServiceRunning(WaterLevelService.class, context)) { + // Intent intent = new Intent(context, WaterLevelService.class); + // 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 8f40d45..73ae20a 100644 --- a/configinterface.cpp +++ b/configinterface.cpp @@ -1,60 +1,168 @@ #include "configinterface.h" -#include -#include -#include - ConfigInterface::ConfigInterface(QObject *parent) : QObject{parent} { -} - -QString ConfigInterface::fileName() const -{ - return m_fileName; -} - -void ConfigInterface::setFileName(const QString &newFileName) -{ - if (m_fileName == newFileName) - return; - m_fileName = newFileName; - std::fstream input(newFileName.toStdString(), std::fstream::in); - if(!input.is_open()) { - return; + 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::string data; - std::string temp; - while(std::getline(input, temp)) { - data += temp; + 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, 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"] = "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) { + qDebug() << config.errorString(); + return; + } + 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"; } - input.flush(); - input.close(); - QByteArray arr_data(data); - QJsonDocument doc = QJsonDocument::fromJson(arr_data); - // The following works even the qml part is not initailzed/connected - // do not change back to just emit config() - QMetaObject::invokeMethod(this, &ConfigInterface::config, Qt::QueuedConnection, doc.object()); - emit fileNameChanged(); } -void ConfigInterface::deleteConfig() +void ConfigInterface::saveConfig(QString max, QString min, QString alert, bool flipbits, QString mode, QString url) { - std::filesystem::remove(std::filesystem::path(m_fileName.toStdString())); - emit configDeleted(); + 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; + 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); + config.flush(); + config.close(); } -void ConfigInterface::saveConfig(QJsonObject config) +QString ConfigInterface::alert() const { - QJsonDocument doc; - doc.setObject(config); - std::string data = doc.toJson().toStdString(); - std::fstream output(m_fileName.toStdString(), std::fstream::out); - if(!output.is_open()) { + return m_alert; +} + +void ConfigInterface::setAlert(const QString &newAlert) +{ + if (m_alert == newAlert) return; - } - output << data; - output.flush(); - output.close(); - QMetaObject::invokeMethod(this, &ConfigInterface::config, Qt::QueuedConnection, doc.object()); + 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(); +} + +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 e3cb75e..840cc2c 100644 --- a/configinterface.h +++ b/configinterface.h @@ -3,28 +3,61 @@ #include #include -#include -#include +#include +#include +#include #include +#include +#include class ConfigInterface : public QObject { Q_OBJECT QML_ELEMENT - Q_PROPERTY(QString fileName READ fileName WRITE setFileName NOTIFY fileNameChanged FINAL) + 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 fileName() const; - void setFileName(const QString &newFileName); + QString alert() const; + void setAlert(const QString &newAlert); + + QString max() const; + void setMax(const QString &newMax); + + 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 config(QJsonObject config); - void fileNameChanged(); - void configDeleted(); + 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 deleteConfig(); - void saveConfig(QJsonObject config); + void saveConfig(QString max, QString min, QString alert, bool flipbits, QString mode, QString url); private: - QString m_fileName; + QString m_alert; + QString m_max; + QString m_min; + bool m_flipBits; + QString m_mode; + QString m_url; }; #endif // CONFIGINTERFACE_H diff --git a/echo.mp3 b/echo.mp3 deleted file mode 100644 index 4373d95..0000000 Binary files a/echo.mp3 and /dev/null differ 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 index ecedbf4..ffff36f 100644 --- a/main.cpp +++ b/main.cpp @@ -3,11 +3,8 @@ int main(int argc, char *argv[]) { - qputenv("QT_SCALE_FACTOR", "1.0"); - qputenv("QT_SCALE_FACTOR_ROUNDING_POLICY", "Round"); QGuiApplication app(argc, argv); - QQmlApplicationEngine engine; QObject::connect( &engine, @@ -15,7 +12,7 @@ int main(int argc, char *argv[]) &app, []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection); - engine.loadFromModule("WaterLevelRewrite", "Main"); + engine.loadFromModule("waterLevelSensor", "Main"); - return QCoreApplication::exec(); + return app.exec(); } diff --git a/media.qrc b/media.qrc deleted file mode 100644 index 2af454f..0000000 --- a/media.qrc +++ /dev/null @@ -1,5 +0,0 @@ - - - echo.mp3 - - diff --git a/moving_container.h b/moving_container.h new file mode 100644 index 0000000..966bce8 --- /dev/null +++ b/moving_container.h @@ -0,0 +1,109 @@ +#ifndef MOVING_CONTAINER +#define MOVING_CONTAINER + +#include +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; + if(!curr) + return; + while(curr != nullptr) { + info *now = curr; + curr = curr->next; + delete now; + } + curr = nullptr; + head = nullptr; + tail = nullptr; + } + + unsigned int size() { + return this->count; + } + + double get_average() { + 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; + 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/requestworker.cpp b/requestworker.cpp deleted file mode 100644 index f0657ef..0000000 --- a/requestworker.cpp +++ /dev/null @@ -1,302 +0,0 @@ -#include "requestworker.h" -#include -#include -#include -#include -#include -#include - -RequestWorker::RequestWorker(QObject *parent) - : QObject{parent} -{ -#ifdef Q_OS_ANDROID - wifi = new WiFiHelper(this); - connect(wifi, &WiFiHelper::ssidRetrieved, this, &RequestWorker::gotWiFiSSID); - poll_wifi = new QTimer(this); - poll_wifi->setInterval(3000); - poll_wifi->setSingleShot(false); - connect(poll_wifi, &QTimer::timeout, this, &RequestWorker::pollWiFi); - wifi->requestPermissionsAndFetch(); - poll_wifi->start(); -#endif -} - -void RequestWorker::setConfig(QJsonObject config) -{ - this->url = config["url"].toString(); - this->interval = config["interval"].toInt(); - this->max_height = config["max"].toInt(); - this->min_height = config["min"].toInt(); - this->alarm_thres = config["alert"].toInt(); - qDebug() << "In cpp config values are: "; - qDebug() << "Url: " << url; - qDebug() << "Interval: " << interval; - qDebug() << "Max Height: " << max_height; - qDebug() << "Min Height: " << min_height; - qDebug() << "Alert: " << alarm_thres; -} - -void RequestWorker::start() -{ - if(manager) { - manager->deleteLater(); - manager = nullptr; - } - manager = new QNetworkAccessManager(); - if(timer) { - timer->deleteLater(); - timer = nullptr; - } - timer = new QTimer(this); - timer->setInterval(interval); - timer->setSingleShot(true); - connect(timer, &QTimer::timeout, this, &RequestWorker::makeRequest); - timer->start(); - stop_timer = false; - setStatus("Waiting to send."); -} - -void RequestWorker::stop() -{ - if(timer->isActive()) - timer->stop(); - stop_timer = true; - qDebug() << "Stopping" << timer->isActive(); -} - -QString RequestWorker::getRequestResponseString(QNetworkReply *rep, QString rep_data) -{ - if(!rep) { - return "No Reply"; - } - QNetworkRequest req = rep->request(); - QString errorString = rep->errorString(); - QString resp = "GET " + req.url().toString() + "\n"; - for(auto &key: req.rawHeaderList()) { - QByteArray value = req.rawHeader(key); - resp += key + ": " + value + "\n"; - } - qDebug() << "Raw header list size: " << req.rawHeaderList().size(); - QString ret; - ret = "Request:\n"; - ret += resp; - ret += "Response:\n"; - if(rep_data == "") - ret += errorString; - else - ret += rep_data; - return ret; -} - -void RequestWorker::pollWiFi() -{ -#ifdef Q_OS_ANDROID - if(wifi) { - wifi->requestPermissionsAndFetch(); - } -#endif -} - -void RequestWorker::gotWiFiSSID(QString ssid) -{ - if(ssid.length() > 0 && ssid != "Permission Denied" && ssid != "") { - emit connectionText("WiFi connected: " + ssid); - } - else if(ssid.length() == 0) { - emit connectionText("WiFi might not be connected", true); - } - else if(ssid != "") { - emit connectionText("Failed to query wifi info", true); - } - else { - emit connectionText("WiFi disconnected", true); - } -} - -void RequestWorker::playAlarm() -{ - if (player_started) - return; - - // Step 1: Copy the qrc resource to a local cache file so Android can read it -#ifdef Q_OS_ANDROID - QString qrcPath = ":/sounds/echo.mp3"; - QString cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/echo.mp3"; - - if (QFile::exists(cachePath)) { - QFile::remove(cachePath); - } - - if (QFile::copy(qrcPath, cachePath)) { - qDebug() << "Successfully copied resource to cache:" << cachePath; - } else { - qDebug() << "Failed to copy resource to cache path!"; - return; - } - - // Step 2: Use Android JNI to initialize and play via android.media.MediaPlayer - QJniEnvironment env; - - // Create new android.media.MediaPlayer() - QJniObject mediaPlayer("android/media/MediaPlayer"); - if (!mediaPlayer.isValid()) { - qDebug() << "Failed to create native Android MediaPlayer!"; - return; - } - - // Set data source using the local cache file path - QJniObject jPath = QJniObject::fromString(cachePath); - mediaPlayer.callMethod("setDataSource", "(Ljava/lang/String;)V", jPath.object()); - - // Enable looping - mediaPlayer.callMethod("setLooping", "(Z)V", true); - - // Prepare and start playback - mediaPlayer.callMethod("prepare"); - mediaPlayer.callMethod("start"); - - // Store a global reference so it doesn't get garbage collected immediately - m_androidMediaPlayer = new QJniObject(mediaPlayer.object()); - - player_started = true; - qDebug() << "Native Android alarm started successfully."; - return; -#endif - if(player) { - player->deleteLater(); - player = nullptr; - } - if(audioOutput) { - audioOutput->deleteLater(); - audioOutput->deleteLater(); - } - qDebug() << "Loading player"; - player = new QMediaPlayer(this); - audioOutput = new QAudioOutput(this); - player->setLoops(QMediaPlayer::Loops::Infinite); - player->setAudioOutput(audioOutput); - if (QFile::exists(":/sounds/echo.mp3")) { - qDebug() << "Resource found successfully!"; - } else { - qDebug() << "Resource not found. Check CMakeLists.txt or .pro file!"; - } - player->setSource(QUrl("qrc:/sounds/echo.mp3")); - audioOutput->setVolume(0.5); - connect(this, &RequestWorker::playMedia, player, &QMediaPlayer::play); - player_started = true; - emit playMedia(); -} - -void RequestWorker::makeRequest() -{ - if(stop_timer) - return; - QNetworkRequest req; - req.setUrl(this->url); - disconnect(manager, nullptr, nullptr, nullptr); - connect(manager, &QNetworkAccessManager::finished, this, &RequestWorker::requestFinished); - manager->setTransferTimeout(15000); - manager->get(req); - setStatus("Request sent; Waiting for Reply."); -} - -void RequestWorker::requestFinished(QNetworkReply *rep) -{ - QByteArray data = rep->readAll(); - auto now = std::chrono::high_resolution_clock::now(); - auto time_diff = std::chrono::duration_cast(now - prev_time); - if(data.size() == 0) { - prev_time = now; - timer->start(); - QString request_response_string = getRequestResponseString(rep); - emit requestFailed(request_response_string); - emit setStatus("Invalid Response; Resending Request"); - rep->deleteLater(); - rep = nullptr; - return; - } - unsigned int time_in_microseconds = time_diff.count(); - QJsonObject jdata = QJsonDocument::fromJson(data).object(); - auto data_water_level = jdata["water_level"].toInt(); - float water_level = (float) ((float) (data_water_level) / 10.0); - float rate_of_fill = ( float )(prev_distance - water_level) /(float) time_in_microseconds; - rate_of_fill = rate_of_fill * 1'000'000; - int expected_to_fill = (water_level - min_height) / rate_of_fill; - int percentage_filled = (float (max_height - water_level) / float (max_height)) * 100; - if(enable_alarm) { - qDebug() << "Alarm enabled"; - if(water_level <= alarm_thres) { - playAlarm(); - } - } - if(reply_number == 0) { - rate_of_fill = 0; - expected_to_fill = 0; - } - reply_number = 1; - qDebug() << "Got water level: " << water_level; - qDebug() << "Got rate of fill: " << rate_of_fill; - qDebug() << "Got expected to fill: " << expected_to_fill; - qDebug() << "Percentage filled: " << percentage_filled; - qDebug() << "Time difference: " << time_in_microseconds; - prev_distance = water_level; - QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss"); - QString request_response_string = getRequestResponseString(rep, data); - request_response_string += "Timestamp:\n" + timestamp; - rep->deleteLater(); - rep = nullptr; - emit networkReply(data_water_level, rate_of_fill, expected_to_fill, request_response_string); - if(stop_timer) { - setStatus("Timer stopped."); - return; - } - prev_time = now; - timer->start(); - QString status = "Got Reply on: " + timestamp + "; Waiting to Send."; - setStatus(status); -} - -QString RequestWorker::status() const -{ - return m_status; -} - -void RequestWorker::setStatus(const QString &newStatus) -{ - if (m_status == newStatus) - return; - m_status = newStatus; - emit statusChanged(); -} - -void RequestWorker::startAlarm() -{ - enable_alarm = true; -} - -void RequestWorker::stopAlarm() -{ - if (!player_started) - return; - - enable_alarm = false; - player_started = false; - if(player) { - player->stop(); - player->deleteLater(); - player = nullptr; - } - if(audioOutput) { - audioOutput->deleteLater(); - audioOutput = nullptr; - } -#ifdef Q_OS_ANDROID - if (m_androidMediaPlayer && m_androidMediaPlayer->isValid()) { - m_androidMediaPlayer->callMethod("stop"); - m_androidMediaPlayer->callMethod("release"); - - delete m_androidMediaPlayer; - m_androidMediaPlayer = nullptr; - } -#endif -} diff --git a/requestworker.h b/requestworker.h deleted file mode 100644 index c759e44..0000000 --- a/requestworker.h +++ /dev/null @@ -1,74 +0,0 @@ -#ifndef REQUESTWORKER_H -#define REQUESTWORKER_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef Q_OS_ANDROID -#include "wifihelper.h" -#include -#endif - -class RequestWorker : public QObject -{ - Q_OBJECT - QML_ELEMENT - Q_PROPERTY(QString status READ status WRITE setStatus NOTIFY statusChanged FINAL) - QNetworkAccessManager *manager = nullptr; - QTimer *timer = nullptr; - QString url; - int interval; - float prev_distance = 0; - int max_height = 0; - int min_height = 0; - bool stop_timer = false; - int reply_number = 0; - int alarm_thres = 0; - bool enable_alarm = false; - bool player_started = false; - QMediaPlayer *player = nullptr; - QAudioOutput *audioOutput = nullptr; -#ifdef Q_OS_ANDROID - WiFiHelper *wifi = nullptr; - QTimer *poll_wifi = nullptr; -#endif - std::chrono::time_point prev_time; -public: - explicit RequestWorker(QObject *parent = nullptr); - QString status() const; - void setStatus(const QString &newStatus); - -signals: - void requestFailed(QString request_response_string); - void networkReply(int distance, float rate_of_fill, int expected_to_fill, QString data); - void statusChanged(); - void playMedia(); - void connectionText(QString text, bool errored = false); -public slots: - void startAlarm(); - void stopAlarm(); - void setConfig(QJsonObject config); - void start(); - void stop(); -private slots: - QString getRequestResponseString(QNetworkReply *rep, QString rep_data = ""); - void pollWiFi(); - void gotWiFiSSID(QString ssid); - void playAlarm(); - void makeRequest(); - void requestFinished(QNetworkReply *rep); -private: - QString m_status; -#ifdef Q_OS_ANDROID - QJniObject *m_androidMediaPlayer = nullptr; -#endif -}; - -#endif // REQUESTWORKER_H diff --git a/service.cpp b/service.cpp new file mode 100644 index 0000000..f48e455 --- /dev/null +++ b/service.cpp @@ -0,0 +1,122 @@ +#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); +} + +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; + + // // 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; + + + + // auto *androidApp = qGuiApp->nativeInterface(); + // // 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 new file mode 100644 index 0000000..1ba18a1 --- /dev/null +++ b/service.h @@ -0,0 +1,17 @@ +#ifndef SERVICE_H +#define SERVICE_H + +#include + +class service : public QObject +{ + Q_OBJECT +public: + explicit service(QObject *parent = nullptr); + void moveAppToBackground(); + void begin(); + +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..155df4e --- /dev/null +++ b/udplistener.cpp @@ -0,0 +1,475 @@ +#include "udplistener.h" +#include + + +UDPListener::UDPListener(QObject *parent) + : QObject{parent} +{ + 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); + timer->setInterval(10000); + timer->setSingleShot(false); + connect(timer, &QTimer::timeout, this, &UDPListener::connectionTimerTimedOut); + connect(qApp, &QCoreApplication::aboutToQuit, this, &UDPListener::destroy); +} + +double UDPListener::distance() const +{ + return m_distance; +} + +void UDPListener::setDistance(double newDistance) +{ + m_distance = newDistance; + emit distanceChanged(); +} + +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(QHostAddress::Any, 0); + if(bind) { + 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 += "="; + } + 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; + if(err == "The bound address is already in use") { + err = "Port already in use."; + } + emit showError(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(QByteArray data1, QByteArray data2) +{ + 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(data, 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"; + +#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); + 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"; +} + +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"; + } +} + +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 + // 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 || !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"; + } + } + +} + +void Worker::socketDataReceived() +{ + if(socket->hasPendingDatagrams()) { + 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."; + } + } +} + +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 new file mode 100644 index 0000000..079dd35 --- /dev/null +++ b/udplistener.h @@ -0,0 +1,116 @@ +#ifndef UDPLISTENER_H +#define UDPLISTENER_H + +#include +#include +#include +#include +#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 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(); +}; + +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 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(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; + average averages; + double m_distance; + Worker *worker = nullptr; + QThread *thread = nullptr; + QTimer *timer = nullptr; + bool m_flip_bits; + QString m_mode; + QString m_url; +}; + +#endif // UDPLISTENER_H diff --git a/wifihelper.cpp b/wifihelper.cpp deleted file mode 100644 index 835cea7..0000000 --- a/wifihelper.cpp +++ /dev/null @@ -1,77 +0,0 @@ -#include "wifihelper.h" -#include -#include -#ifdef Q_OS_ANDROID -#include -#endif -#include - -WiFiHelper::WiFiHelper(QObject *parent) - : QObject{parent} -{ - -} - -QString WiFiHelper::getWifiSsid() { -#ifdef Q_OS_ANDROID - // Get Android application context in Qt 6 - QJniObject context = QNativeInterface::QAndroidApplication::context(); - if (!context.isValid()) return QString(); - - // Get WifiManager service - QJniObject wifiService = QJniObject::fromString("wifi"); - QJniObject wifiManager = context.callObjectMethod( - "getSystemService", - "(Ljava/lang/String;)Ljava/lang/Object;", - wifiService.object() - ); - if (!wifiManager.isValid()) return QString(); - - // Fetch WifiInfo - QJniObject wifiInfo = wifiManager.callObjectMethod( - "getConnectionInfo", - "()Landroid/net/wifi/WifiInfo;" - ); - if (!wifiInfo.isValid()) return QString(); - - // Get SSID string - QJniObject ssidJni = wifiInfo.callObjectMethod( - "getSSID", - "()Ljava/lang/String;" - ); - if (!ssidJni.isValid()) return QString(); - - QString ssid = ssidJni.toString(); - - // Android returns SSIDs wrapped in double quotes (e.g., "\"MyWiFi\"") - if (ssid.startsWith('"') && ssid.endsWith('"') && ssid.length() > 1) { - ssid = ssid.mid(1, ssid.length() - 2); - } - - return ssid; -#endif - return ""; -} - -void WiFiHelper::requestPermissionsAndFetch() { -#ifdef Q_OS_ANDROID - QLocationPermission locationPermission; - locationPermission.setAccuracy(QLocationPermission::Accuracy::Precise); - Qt::PermissionStatus status = qApp->checkPermission(locationPermission); - - if (status == Qt::PermissionStatus::Granted) { - emit ssidRetrieved(getWifiSsid()); - } else { - qDebug() << "Requesting permission."; - qApp->requestPermission(locationPermission, this, [this](const QPermission &permission) { - if (permission.status() == Qt::PermissionStatus::Granted) { - qDebug() << "Requested permission and got ssid"; - emit ssidRetrieved(getWifiSsid()); - } else { - qDebug() << "Failed to get WiFi SSID due to denied permission."; - emit ssidRetrieved("Permission Denied"); - } - }); - } -#endif -} diff --git a/wifihelper.h b/wifihelper.h deleted file mode 100644 index ab10632..0000000 --- a/wifihelper.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef WIFIHELPER_H -#define WIFIHELPER_H - -#include - -class WiFiHelper : public QObject -{ - Q_OBJECT -public: - explicit WiFiHelper(QObject *parent = nullptr); - Q_INVOKABLE QString getWifiSsid(); - Q_INVOKABLE void requestPermissionsAndFetch(); - -signals: - void ssidRetrieved(const QString &ssid); -}; -#endif // WIFIHELPER_H