11 Commits
v1.0 ... v1.1

14 changed files with 594 additions and 182 deletions

View File

@@ -1,8 +1,8 @@
project(qiflora) project(qiflora)
cmake_minimum_required(VERSION 2.8.12) cmake_minimum_required(VERSION 2.8.12)
set(KF5_MIN_VERSION "5.18.0") set(KF5_MIN_VERSION "5.44.0")
set(QT_MIN_VERSION "5.5.0") set(QT_MIN_VERSION "5.12.0")
################# Disallow in-source build ################# ################# Disallow in-source build #################
@@ -28,8 +28,9 @@ include(KDECompilerSettings NO_POLICY_SCOPE)
################# Find dependencies ################# ################# Find dependencies #################
find_package(Qt5 ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS Core Quick Test Gui Svg QuickControls2 Bluetooth) find_package(Qt5 ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS Core Quick Test Gui Svg QuickControls2 Bluetooth Charts)
find_package(KF5Kirigami2 ${KF5_MIN_VERSION}) find_package(KF5Kirigami2 ${KF5_MIN_VERSION} REQUIRED)
find_package(KF5CoreAddons ${KF5_MIN_VERSION} REQUIRED)
################# Enable C++11 features for clang and gcc ################# ################# Enable C++11 features for clang and gcc #################
@@ -38,8 +39,8 @@ set(CMAKE_CXX_STANDARD 11)
################# build and install ################# ################# build and install #################
add_subdirectory(src) add_subdirectory(src)
install(PROGRAMS org.eyecreate.qiflora.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(PROGRAMS packaging/org.eyecreate.qiflora.desktop DESTINATION ${KDE_INSTALL_APPDIR})
install(FILES org.eyecreate.qiflora.svg DESTINATION ${KDE_INSTALL_ICONDIR}) install(FILES packaging/org.eyecreate.qiflora.svg DESTINATION ${KDE_INSTALL_ICONDIR})
install(FILES org.eyecreate.qiflora.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES packaging/org.eyecreate.qiflora.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)

View File

@@ -1,120 +0,0 @@
{
"id": "org.eyecreate.qiflora",
"runtime": "org.kde.Platform",
"runtime-version": "5.12",
"sdk": "org.kde.Sdk",
"command": "qiflora",
"tags": ["kde"],
"finish-args": [
"--share=ipc",
"--allow=bluetooth",
"--system-talk-name=org.bluez",
"--share=network",
"--socket=x11",
"--socket=wayland",
"--device=dri",
"--filesystem=home",
"--talk-name=org.freedesktop.Notifications"
],
"separate-locales": false,
"modules": [
{
"name": "udev",
"rm-configure": true,
"config-opts": [
"--disable-hwdb",
"--disable-logging",
"--disable-introspection",
"--disable-keymap",
"--disable-mtd_probe",
"--with-systemdsystemunitdir=/app/lib/systemd/"
],
"cleanup": [
"/include",
"/etc",
"/libexec",
"/sbin",
"/lib/pkgconfig",
"/lib/systemd",
"/man",
"/share/aclocal",
"/share/doc",
"/share/gtk-doc",
"/share/man",
"/share/pkgconfig",
"*.la",
"*.a"
],
"sources": [
{
"type": "archive",
"url": "https://www.kernel.org/pub/linux/utils/kernel/hotplug/udev-175.tar.bz2",
"sha256": "4c7937fe5a1521316ea571188745b9a00a9fdf314228cffc53a7ba9e5968b7ab"
},
{
"type": "patch",
"path": "sysmacros.patch"
},
{
"type": "script",
"dest-filename": "autogen.sh",
"commands": [
"autoreconf -vfi"
]
}
],
"post-install": [
"sed -i 's|${exec_prefix}|/app|g' /app/share/pkgconfig/udev.pc"
]
},
{
"name": "ical",
"cleanup": [
"/lib/cmake"
],
"buildsystem": "cmake-ninja",
"config-opts": [
"-DCMAKE_BUILD_TYPE=RelWithDebInfo",
"-DCMAKE_INSTALL_LIBDIR=/app/lib",
"-DBUILD_SHARED_LIBS=ON"
],
"sources": [ { "type": "archive", "url": "https://github.com/libical/libical/archive/v3.0.5.tar.gz", "sha256": "483acbf7fee66ca071c2ff8183e46b6f2b3a89e1e866eadf4870eaaa281c8db1" } ]
},
{
"name": "bluez",
"config-opts": [
"--disable-datafiles",
"--disable-systemd",
"--enable-library",
"--prefix=/app",
"--sysconfdir=/app/etc"
],
"sources": [ { "type": "archive", "url": "https://mirrors.edge.kernel.org/pub/linux/bluetooth/bluez-5.50.tar.xz", "sha256": "5ffcaae18bbb6155f1591be8c24898dc12f062075a40b538b745bfd477481911"} ]
},
{
"name": "qtconnectivity",
"buildsystem": "simple",
"cleanup-platform": [
"/bin",
"/mkspecs"
],
"sources": [ { "type": "git", "url": "https://github.com/qt/qtconnectivity", "branch": "5.12.4", "commit": "f6be1f73a810514335ab3d27e1d05825a36b06af" } ],
"build-commands": [
"qmake",
"make -j $FLATPAK_BUILDER_N_JOBS",
"cp -r -n bin /app",
"cp -r -n include /app",
"cp -r -n lib /app",
"mkdir -p /app/src/bluetooth",
"cp -r src/bluetooth /app/src/"
]
},
{
"name": "qiflora",
"buildsystem": "cmake-ninja",
"builddir": true,
"sources": [ { "type": "dir", "path": ".", "skip": [".git"] } ]
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 145 KiB

View File

@@ -7,6 +7,7 @@
<project_license>GPL-3.0-or-later</project_license> <project_license>GPL-3.0-or-later</project_license>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<url type="homepage">https://git.eyecreate.org/eyecreate/qiflora</url> <url type="homepage">https://git.eyecreate.org/eyecreate/qiflora</url>
<url type="contact">https://www.eyecreate.org</url>
<developer_name>eyecreate</developer_name> <developer_name>eyecreate</developer_name>
<description> <description>
<p>Mobile friendly application to monitor Mi Flora devices.</p> <p>Mobile friendly application to monitor Mi Flora devices.</p>
@@ -14,10 +15,15 @@
</description> </description>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image type="source">https://git.eyecreate.org/eyecreate/qiflora/raw/v1.0/packaging/main_window.png</image> <image type="source">https://git.eyecreate.org/eyecreate/qiflora/raw/v1.1/packaging/main_window.png</image>
</screenshot> </screenshot>
</screenshots> </screenshots>
<releases> <releases>
<release version="1.1" date="2019-11-13" type="stable">
<description>
<p>This release adds history graph for the last 48 hours.</p>
</description>
</release>
<release version="1.0" date="2019-11-8" type="stable"/> <release version="1.0" date="2019-11-8" type="stable"/>
</releases> </releases>
</component> </component>

View File

@@ -1,7 +1,7 @@
[Desktop Entry] [Desktop Entry]
Name=QiFlora Name=QiFlora
Comment=Monitor plants with Mi Flora sensors. Comment=Monitor plants with Mi Flora sensors.
Version=1.0 Version=1.1
Exec=qiflora Exec=qiflora
MimeType=application/x-qiflora; MimeType=application/x-qiflora;
Icon=org.eyecreate.qiflora Icon=org.eyecreate.qiflora

View File

@@ -0,0 +1,96 @@
{
"id": "org.eyecreate.qiflora",
"runtime": "org.kde.Platform",
"command": "qiflora",
"finish-args": [
"--share=ipc",
"--allow=bluetooth",
"runtime-version": "5.13",
"sdk": "org.kde.Sdk",
"--system-talk-name=org.bluez",
"--share=network",
"--socket=x11",
"--socket=wayland",
"--device=dri",
"--filesystem=home",
"--talk-name=org.freedesktop.Notifications"
],
"separate-locales": false,
"modules": [
{
"name": "ical",
"cleanup": [
"/lib/cmake"
],
"buildsystem": "cmake-ninja",
"config-opts": [
"-DCMAKE_BUILD_TYPE=RelWithDebInfo",
"-DCMAKE_INSTALL_LIBDIR=/app/lib",
"-DBUILD_SHARED_LIBS=ON"
],
"sources": [
{
"type": "archive",
"url": "https://github.com/libical/libical/archive/v3.0.5.tar.gz",
"sha256": "483acbf7fee66ca071c2ff8183e46b6f2b3a89e1e866eadf4870eaaa281c8db1"
}
]
},
{
"name": "bluez",
"config-opts": [
"--disable-datafiles",
"--disable-systemd",
"--enable-library",
"--prefix=/app",
"--sysconfdir=/app/etc",
"--disable-udev"
],
"sources": [
{
"type": "archive",
"url": "https://mirrors.edge.kernel.org/pub/linux/bluetooth/bluez-5.52.tar.xz",
"sha256": "f7144ce2039202cfac18ccb52426efea11c98e4f6e1bb8041bcb994b8378560a"
}
]
},
{
"name": "qtconnectivity",
"buildsystem": "simple",
"cleanup-platform": [
"/bin",
"/mkspecs"
],
"sources": [
{
"type": "git",
"url": "https://github.com/qt/qtconnectivity",
"branch": "5.13.2",
"commit": "f6be1f73a810514335ab3d27e1d05825a36b06af"
}
],
"build-commands": [
"qmake",
"make -j $FLATPAK_BUILDER_N_JOBS",
"cp -r -n bin /app",
"cp -r -n include /app",
"cp -r -n lib /app",
"mkdir -p /app/src/bluetooth",
"cp -r src/bluetooth /app/src/"
]
},
{
"name": "qiflora",
"buildsystem": "cmake-ninja",
"builddir": true,
"sources": [
{
"type": "git",
"url": "https://git.eyecreate.org/eyecreate/qiflora.git",
"tag": "v1.1",
"commit": "9bad2c73ff21515f63f5a09d7a8434bdfd98e6ee"
}
]
}
]
}

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -2,10 +2,11 @@
set(qiflora_SRCS set(qiflora_SRCS
miflora/miflora.cpp miflora/miflora.cpp
miflora/bluetoothdevices.cpp miflora/bluetoothdevices.cpp
miflora/florahistory.cpp
main.cpp main.cpp
) )
qt5_add_resources(RESOURCES resources.qrc) qt5_add_resources(RESOURCES resources.qrc)
add_executable(qiflora ${qiflora_SRCS} ${RESOURCES}) add_executable(qiflora ${qiflora_SRCS} ${RESOURCES})
target_link_libraries(qiflora Qt5::Core Qt5::Qml Qt5::Quick Qt5::Svg Qt5::Bluetooth) target_link_libraries(qiflora Qt5::Core Qt5::Qml Qt5::Quick Qt5::Svg Qt5::Bluetooth Qt5::Charts KF5::CoreAddons)
install(TARGETS qiflora ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(TARGETS qiflora ${KF5_INSTALL_TARGETS_DEFAULT_ARGS})

View File

@@ -1,7 +1,8 @@
import QtQuick 2.6 import QtQuick 2.6
import org.kde.kirigami 2.4 as Kirigami import org.kde.kirigami 2.6 as Kirigami
import QtQuick.Controls 2.0 as Controls import QtQuick.Controls 2.0 as Controls
import QtQuick.Layouts 1.12 as Layouts import QtQuick.Layouts 1.12 as Layouts
import QtCharts 2.3 as Charts
import org.eyecreate.qiflora 1.0 import org.eyecreate.qiflora 1.0
Kirigami.ApplicationWindow { Kirigami.ApplicationWindow {
@@ -11,6 +12,7 @@ Kirigami.ApplicationWindow {
pageStack.initialPage: mainPageComponent pageStack.initialPage: mainPageComponent
contextDrawer: Kirigami.ContextDrawer {}
QiFlora { QiFlora {
id: qiflora id: qiflora
@@ -19,7 +21,8 @@ Kirigami.ApplicationWindow {
Component { Component {
id: mainPageComponent id: mainPageComponent
Kirigami.ScrollablePage { Kirigami.Page {
id: mainPage
mainAction: Kirigami.Action { mainAction: Kirigami.Action {
iconName: "view-refresh" iconName: "view-refresh"
text: i18n("Query Device") text: i18n("Query Device")
@@ -27,54 +30,165 @@ Kirigami.ApplicationWindow {
deviceSelect.open(); deviceSelect.open();
} }
} }
contextualActions: [
Kirigami.Action {
iconName: "help-about"
text: i18n("About")
onTriggered: {
pageStack.replace(aboutPageComponent);
}
}
]
title: "Monitor" title: "Monitor"
leftPadding: 0
rightPadding: 0
Component.onCompleted: { Layouts.ColumnLayout {
monitorTypes.append({"chartType": "temperature", "title": i18n("Temperature"), "icon": "filename-bpm-amarok"}); anchors.fill: parent
monitorTypes.append({"chartType": "moisture", "title": i18n("Moisture"), "icon": "colors-chromablue"}); Kirigami.InlineMessage {
monitorTypes.append({"chartType": "conductivity", "title": i18n("Conductivity"), "icon": "quickopen"}); Layouts.Layout.fillWidth: true
monitorTypes.append({"chartType": "brightness", "title": i18n("Brightness"), "icon": "contrast"}); Layouts.Layout.leftMargin: 10
} Layouts.Layout.rightMargin: 10
z: 9997
Kirigami.CardsListView { type: Kirigami.MessageType.Error
id: monitorView id: errorMessage
model: ListModel { showCloseButton: true
id: monitorTypes Connections {
target: qiflora
onErrorHappened: {
errorMessage.text = description;
errorMessage.visible = true;
}
}
} }
delegate: Kirigami.Card { Kirigami.CardsListView {
id: card Layouts.Layout.fillWidth: true
banner { Layouts.Layout.fillHeight: true
title: model.title id: monitorView
titleIcon: model.icon model: ListModel {
titleLevel: 2 id: monitorTypes
ListElement{
chartType: "temperature"
title: "Temperature"
icon: "filename-bpm-amarok"
lineColor: "red"
yMin: -20
yMax: 40
modelCol: 1
}
ListElement{
chartType: "moisture"
title: "Moisture"
icon: "colors-chromablue"
lineColor: "cyan"
yMin: 0
yMax: 75
modelCol: 3
}
ListElement{
chartType: "conductivity"
title: "Conductivity"
icon: "quickopen"
lineColor: "gold"
yMin: 0
yMax: 6000
modelCol: 4
}
ListElement{
chartType: "brightness"
title: "Brightness"
icon: "contrast"
lineColor: "orange"
yMin: 0
yMax: 30000
modelCol: 2
}
} }
header: Row {
layoutDirection: Qt.RightToLeft delegate: Kirigami.Card {
topPadding: 10.0 id: card
rightPadding: 10.0 banner {
Layouts.ColumnLayout { title: i18n(model.title)
Kirigami.Heading { titleIcon: model.icon
Layouts.Layout.alignment: Qt.AlignCenter titleLevel: 2
level: 4 }
text: i18n("Last Measured") header: Row {
} layoutDirection: Qt.RightToLeft
Kirigami.Heading { topPadding: 10.0
Layouts.Layout.alignment: Qt.AlignCenter rightPadding: 10.0
level: 3 Layouts.ColumnLayout {
text: { Kirigami.Heading {
if(model.chartType == "temperature") qiflora.temperature + "°C" Layouts.Layout.alignment: Qt.AlignCenter
else if(model.chartType == "moisture") qiflora.moisture + "%" level: 4
else if(model.chartType == "conductivity") qiflora.conduction + " µS/cm" text: i18n("Last Measured")
else if(model.chartType == "brightness") qiflora.brightness + " lux" }
Kirigami.Heading {
Layouts.Layout.alignment: Qt.AlignCenter
level: 3
text: {
if(model.chartType == "temperature") qiflora.temperature + "°C\n" + (qiflora.temperature*1.8+32) + "°F"
else if(model.chartType == "moisture") qiflora.moisture + "%"
else if(model.chartType == "conductivity") qiflora.conduction + " µS/cm"
else if(model.chartType == "brightness") qiflora.brightness + " lux"
}
} }
} }
} }
} contentItem: Item {
contentItem: Controls.Label { implicitWidth: 300
wrapMode: Text.WordWrap implicitHeight: 200
text: i18n("") //TODO: replace with graph? Charts.ChartView {
id: chart
antialiasing: true
backgroundColor: Kirigami.Theme.buttonBackgroundColor
titleColor: Kirigami.Theme.textColor
legend.visible: false
anchors.fill: parent
Charts.LineSeries {
id: series
axisX: Charts.DateTimeAxis {
id: dateAx
labelsColor: Kirigami.Theme.textColor
format: "MMM d yyyy ha"
Component.onCompleted: {
//On load, change date span to last 24 to remove awkward 1969 empty dates.
var yesterday = new Date();
yesterday.setDate(yesterday.getDate() -1);
dateAx.min = yesterday;
dateAx.max = new Date();
}
}
axisY: Charts.ValueAxis {
id: valueAx
labelsColor: Kirigami.Theme.textColor
min: yMin
max: yMax
}
color: model.lineColor
name: model.title
Charts.VXYModelMapper {
model: qiflora.model
xColumn: 0
yColumn: modelCol
}
}
Connections {
target: qiflora.model
onRowsInserted: {
//Readjust graphs to show date range from earliest item.
dateAx.max = new Date();
dateAx.min = qiflora.model.data(qiflora.model.index(0,0));
}
}
}
} }
}
Controls.ScrollBar.vertical: Controls.ScrollBar {}
} }
} }
@@ -86,7 +200,6 @@ Kirigami.ApplicationWindow {
Rectangle { Rectangle {
height: 10 height: 10
color: "transparent" color: "transparent"
width: parent.width
} }
Kirigami.Heading { Kirigami.Heading {
text: i18n("Select Device to Query") text: i18n("Select Device to Query")
@@ -94,7 +207,6 @@ Kirigami.ApplicationWindow {
Rectangle { Rectangle {
height: 10 height: 10
color: "transparent" color: "transparent"
width: parent.width
} }
} }
id: deviceList id: deviceList
@@ -126,4 +238,20 @@ Kirigami.ApplicationWindow {
} }
} }
} }
Component {
id:aboutPageComponent
Kirigami.AboutPage {
id: aboutPage
actions.main: Kirigami.Action {
iconName: "window-close"
text: "Close"
onTriggered: {
pageStack.clear();
pageStack.push(mainPageComponent);
}
}
aboutData: appAboutData
}
}
} }

View File

@@ -4,18 +4,30 @@
#include <QUrl> #include <QUrl>
#include "miflora/miflora.h" #include "miflora/miflora.h"
#include "miflora/bluetoothdevices.h" #include "miflora/bluetoothdevices.h"
#include <KAboutData>
#include <QIcon>
Q_DECL_EXPORT int main(int argc, char *argv[]) Q_DECL_EXPORT int main(int argc, char *argv[])
{ {
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QApplication app(argc, argv); QApplication app(argc, argv);
KAboutData aboutData("org.eyecreate.qiflora", "QiFlora", "1.1", "Mobile friendly application to monitor Mi Flora devices.",KAboutLicense::GPL_V3);//TODO:i18n
aboutData.setProductName("qiflora");
aboutData.addAuthor("Kevin Whitaker",QString(),"eyecreate@eyecreate.org","https://www.eyecreate.org");
aboutData.setDesktopFileName("org.eyecreate.qiflora");
QCoreApplication::setOrganizationName("eyecreate"); QCoreApplication::setOrganizationName("eyecreate");
QCoreApplication::setOrganizationDomain("eyecreate.org"); QCoreApplication::setOrganizationDomain("eyecreate.org");
QCoreApplication::setApplicationName("qiflora"); QCoreApplication::setApplicationName(aboutData.productName());
QCoreApplication::setApplicationVersion(aboutData.version());
app.setWindowIcon(QIcon::fromTheme("org.eyecreate.qiflora"));
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
qmlRegisterType<BluetoothDevices>(); qmlRegisterType<BluetoothDevices>();
qmlRegisterType<MiFlora>("org.eyecreate.qiflora",1,0,"QiFlora"); qmlRegisterType<MiFlora>("org.eyecreate.qiflora",1,0,"QiFlora");
engine.rootContext()->setContextProperty(QStringLiteral("appAboutData"), QVariant::fromValue(aboutData));
engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
if (engine.rootObjects().isEmpty()) { if (engine.rootObjects().isEmpty()) {

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2019 Kevin Whitaker <eyecreate@eyecreate.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "florahistory.h"
#include <QDebug>
QVariant FloraHistory::data(const QModelIndex& index, int role) const
{
if(role == Qt::DisplayRole && tableData.size()-1 >= index.row()) {
if(index.column() == 0) {
return QVariant(tableData[index.row()].time);
} else if(index.column() == 1) {
return QVariant(tableData[index.row()].temperature);
} else if(index.column() == 2) {
return QVariant(tableData[index.row()].brightness);
} else if(index.column() == 3) {
return QVariant(tableData[index.row()].moisture);
} else if(index.column() == 4) {
return QVariant(tableData[index.row()].conductivity);
} else {
return QVariant();
}
} else {
return QVariant();
}
}
int FloraHistory::columnCount(const QModelIndex& parent) const
{
return 5;
}
int FloraHistory::rowCount(const QModelIndex& parent) const
{
return tableData.size();
}
void FloraHistory::addData(QDateTime time, float temperature, quint32 brightness, quint8 moisture, quint16 conductivity)
{
beginInsertRows(QModelIndex(),tableData.size(),tableData.size());
tableData.append(flora_data{time, temperature, brightness, moisture, conductivity});
endInsertRows();
}
void FloraHistory::clear()
{
tableData.clear();
emit layoutChanged();
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2019 Kevin Whitaker <eyecreate@eyecreate.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef FLORAHISTORY_H
#define FLORAHISTORY_H
#include <qabstractitemmodel.h>
#include <QDateTime>
#include <QList>
/**
* Model class to hold history data from MiFlora device.
*/
class FloraHistory : public QAbstractTableModel
{
Q_OBJECT
public:
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
int columnCount(const QModelIndex& parent) const override;
int rowCount(const QModelIndex& parent) const override;
void clear();
void addData(QDateTime time, float temperature, quint32 brightness, quint8 moisture, quint16 conductivity);
private:
struct flora_data {
QDateTime time;
float temperature;
quint32 brightness;
quint8 moisture;
quint16 conductivity;
};
QList<flora_data> tableData;
};
#endif // FLORAHISTORY_H

View File

@@ -55,6 +55,11 @@ void MiFlora::updateDataFromDevice ( QString mac )
} }
} }
QAbstractTableModel * MiFlora::getModel()
{
return floraModel;
}
void MiFlora::foundDevice ( const QBluetoothDeviceInfo& device ) void MiFlora::foundDevice ( const QBluetoothDeviceInfo& device )
{ {
if(device.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration && device.address().toString().startsWith("C4:7C")) { if(device.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration && device.address().toString().startsWith("C4:7C")) {
@@ -72,20 +77,34 @@ QQmlListProperty<BluetoothDevices> MiFlora::getDeviceList()
void MiFlora::logControllerError ( QLowEnergyController::Error err ) void MiFlora::logControllerError ( QLowEnergyController::Error err )
{ {
qDebug() << "Error:" << err; qDebug() << "Error:" << err;
emit errorHappened(currentController->errorString());
} }
void MiFlora::logServiceError(QLowEnergyService::ServiceError err) void MiFlora::logServiceError(QLowEnergyService::ServiceError err)
{ {
qDebug() << "Service Error:" << err; qDebug() << "Service Error:" << err;
emit errorHappened("Possible Read/Write error!");
} }
void MiFlora::serviceStateChanges ( QLowEnergyService::ServiceState state ) void MiFlora::sensorServiceStateChanges ( QLowEnergyService::ServiceState state )
{ {
if(state == QLowEnergyService::ServiceState::ServiceDiscovered) { if(state == QLowEnergyService::ServiceState::ServiceDiscovered) {
for(QLowEnergyCharacteristic chrt : sensorService->characteristics()) { for(QLowEnergyCharacteristic chrt : sensorService->characteristics()) {
if(chrt.uuid().toUInt16() == magicChar) { if(chrt.uuid().toUInt16() == magicChar) {
sensorService->writeCharacteristic(chrt, QByteArray::fromHex("a01f")); sensorService->writeCharacteristic(chrt, magicBytes);
}
}
}
}
void MiFlora::historyServiceStateChanges ( QLowEnergyService::ServiceState state )
{
if(state == QLowEnergyService::ServiceState::ServiceDiscovered) {
for(QLowEnergyCharacteristic chrt : historyService->characteristics()) {
if(chrt.uuid().toUInt16() == historyTimeChar) {
qDebug() << "Asking for device's view on time.";
historyService->readCharacteristic(chrt);
} }
} }
} }
@@ -134,15 +153,135 @@ void MiFlora::serviceCharRead(QLowEnergyCharacteristic readChar, QByteArray valu
} }
} }
QLowEnergyCharacteristic MiFlora::getCharFromValue(QLowEnergyService *service, quint16 characteristic)
{
for(QLowEnergyCharacteristic chrt : service->characteristics()) {
if(chrt.uuid().toUInt16() == characteristic) {
return chrt;
}
}
return QLowEnergyCharacteristic();
}
void MiFlora::historyServiceCharRead(QLowEnergyCharacteristic readChar, QByteArray value)
{
if(readChar.uuid().toUInt16() == historyReaderChar) {
QDataStream parser(value);
parser.setByteOrder(QDataStream::LittleEndian);
quint16 size;
parser >> size;
//change connect signals/slots to perform history read task.
disconnect(historyService, &QLowEnergyService::characteristicRead, this, &MiFlora::historyServiceCharRead);
connect(historyService, &QLowEnergyService::characteristicRead, this, &MiFlora::historyServiceCharReadData);
disconnect(historyService, &QLowEnergyService::characteristicWritten, this, &MiFlora::historyServiceCharWritten);
connect(historyService, &QLowEnergyService::characteristicWritten, this, &MiFlora::historyServiceCharWrittenData);
//Take size given and start by giving the last 48 numbers(if that many) to represent the last day of data. TODO: look to have amount of time be configurable.
lastHistoryEntry = 0;
if(size > 48) {
currentHistoryEntry = 48;
} else {
currentHistoryEntry = size;
}
floraModel->clear();
//Ask for first history value
QByteArray historyIndex(QByteArray::fromHex("a1"));
QDataStream historyParser(&historyIndex, QIODevice::ReadWrite);
historyParser.setByteOrder(QDataStream::LittleEndian);
historyParser.skipRawData(1);
historyParser << currentHistoryEntry;
historyService->writeCharacteristic(getCharFromValue(historyService, historyControllerChar), historyIndex);
} else if(readChar.uuid().toUInt16() == historyTimeChar) {
//Determine when device started counting by comparing system and device time.
quint32 time;
QDataStream parser(value);
parser.setByteOrder(QDataStream::LittleEndian);
parser >> time;
qint64 systemTime = QDateTime::currentSecsSinceEpoch();
deviceStartTime = systemTime - time;
//Now we start the history reading process.
qDebug() << "Writing history init.";
historyService->writeCharacteristic(getCharFromValue(historyService, historyControllerChar), historyReadInit);
}
}
void MiFlora::historyServiceCharReadData(QLowEnergyCharacteristic readChar, QByteArray value)
{
//We are in a read loop. Read data and determine if we should stop asking for history.
if(value == QByteArray::fromHex("ffffffffffffffffffffffffffffffff") || value == QByteArray::fromHex("00000000000000000000000000000000") || value == QByteArray::fromHex("aabbccddeeff99887766554433221110")) {
qDebug() << "invalid history response:" << value;
} else {
quint16 origTemp;
quint32 time;
quint32 bright;
quint8 moisture;
quint16 conduct;
float temp;
QDataStream parser(value);
parser.setByteOrder(QDataStream::LittleEndian);
parser >> time;
parser >> origTemp;
temp = origTemp/ 10.0; //original value in 0.1 C
parser.skipRawData(1);
parser >> bright;
parser >> moisture;
parser >> conduct;
QDateTime convTime;
convTime.setSecsSinceEpoch(deviceStartTime+time);
//Write out items to table
floraModel->addData(convTime, temp, bright, moisture, conduct);
}
if(lastHistoryEntry == currentHistoryEntry) {
disconnect(historyService, &QLowEnergyService::characteristicRead, this, &MiFlora::historyServiceCharReadData);
connect(historyService, &QLowEnergyService::characteristicRead, this, &MiFlora::historyServiceCharRead);
disconnect(historyService, &QLowEnergyService::characteristicWritten, this, &MiFlora::historyServiceCharWrittenData);
connect(historyService, &QLowEnergyService::characteristicWritten, this, &MiFlora::historyServiceCharWritten);
} else {
//Get next history entry
currentHistoryEntry -= 1;
QByteArray historyIndex(QByteArray::fromHex("a1"));
QDataStream parser(&historyIndex, QIODevice::ReadWrite);
parser.setByteOrder(QDataStream::LittleEndian);
parser.skipRawData(1);
parser << currentHistoryEntry;
historyService->writeCharacteristic(getCharFromValue(historyService, historyControllerChar), historyIndex);
}
}
void MiFlora::historyServiceCharWrittenData(QLowEnergyCharacteristic changedChar, QByteArray value)
{
//We are in a read loop. Ask to read from history what should have just been requested by a write.
historyService->readCharacteristic(getCharFromValue(historyService, historyReaderChar));
}
void MiFlora::historyServiceCharWritten(QLowEnergyCharacteristic changedChar, QByteArray value)
{
if(changedChar.uuid().toUInt16() == historyControllerChar) {
for(QLowEnergyCharacteristic chrt : historyService->characteristics()) {
if(chrt.uuid().toUInt16() == historyReaderChar) {
historyService->readCharacteristic(chrt);
}
}
}
}
void MiFlora::serviceDiscovered ( const QBluetoothUuid& gatt ) void MiFlora::serviceDiscovered ( const QBluetoothUuid& gatt )
{ {
if(gatt == QBluetoothUuid(dataService)) { if(gatt == QBluetoothUuid(dataServiceChar)) {
sensorService = currentController->createServiceObject(gatt); sensorService = currentController->createServiceObject(gatt);
connect(sensorService, &QLowEnergyService::stateChanged, this, &MiFlora::serviceStateChanges); connect(sensorService, &QLowEnergyService::stateChanged, this, &MiFlora::sensorServiceStateChanges);
connect(sensorService, static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error), this, &MiFlora::logServiceError); connect(sensorService, static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error), this, &MiFlora::logServiceError);
connect(sensorService, &QLowEnergyService::characteristicWritten, this, &MiFlora::serviceCharWritten); connect(sensorService, &QLowEnergyService::characteristicWritten, this, &MiFlora::serviceCharWritten);
connect(sensorService, &QLowEnergyService::characteristicRead, this, &MiFlora::serviceCharRead); connect(sensorService, &QLowEnergyService::characteristicRead, this, &MiFlora::serviceCharRead);
sensorService->discoverDetails(); sensorService->discoverDetails();
} else if(gatt == QBluetoothUuid(historyServiceChar)) {
historyService = currentController->createServiceObject(gatt);
connect(historyService, &QLowEnergyService::stateChanged, this, &MiFlora::historyServiceStateChanges);
connect(historyService, static_cast<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(&QLowEnergyService::error), this, &MiFlora::logServiceError);
connect(historyService, &QLowEnergyService::characteristicWritten, this, &MiFlora::historyServiceCharWritten);
connect(historyService, &QLowEnergyService::characteristicRead, this, &MiFlora::historyServiceCharRead);
historyService->discoverDetails();
} }
} }

View File

@@ -26,6 +26,10 @@
#include <QtBluetooth/QLowEnergyCharacteristic> #include <QtBluetooth/QLowEnergyCharacteristic>
#include "bluetoothdevices.h" #include "bluetoothdevices.h"
#include <QQmlListProperty> #include <QQmlListProperty>
#include <QAbstractItemModel>
#include <QDateTime>
#include "florahistory.h"
#include <QXYSeries>
/** /**
* Class using QtBluetooth to find and retrive info from Mi Flora devices. * Class using QtBluetooth to find and retrive info from Mi Flora devices.
@@ -39,17 +43,21 @@ class MiFlora : public QObject
Q_PROPERTY(quint16 conduction NOTIFY conductionChanged MEMBER conduct) Q_PROPERTY(quint16 conduction NOTIFY conductionChanged MEMBER conduct)
Q_PROPERTY(quint8 battery NOTIFY batteryChanged MEMBER battery) Q_PROPERTY(quint8 battery NOTIFY batteryChanged MEMBER battery)
Q_PROPERTY(QQmlListProperty<BluetoothDevices> devices READ getDeviceList NOTIFY newDeviceFound) Q_PROPERTY(QQmlListProperty<BluetoothDevices> devices READ getDeviceList NOTIFY newDeviceFound)
Q_PROPERTY(QAbstractTableModel* model READ getModel NOTIFY modelUpdated)
public: public:
Q_INVOKABLE void startSearch(); Q_INVOKABLE void startSearch();
Q_INVOKABLE void stopSearch(); Q_INVOKABLE void stopSearch();
Q_INVOKABLE void updateDataFromDevice(QString mac); Q_INVOKABLE void updateDataFromDevice(QString mac);
QQmlListProperty<BluetoothDevices> getDeviceList(); QQmlListProperty<BluetoothDevices> getDeviceList();
//TODO:History? QAbstractTableModel* getModel();
signals: signals:
void newDeviceFound(); void newDeviceFound();
void modelUpdated();
void errorHappened(QString description);
void temperatureChanged(float temperature); void temperatureChanged(float temperature);
void brightnessChanged(quint32 brightness); void brightnessChanged(quint32 brightness);
void moistureChanged(quint8 moisture); void moistureChanged(quint8 moisture);
@@ -61,19 +69,33 @@ private:
void serviceDiscovered(const QBluetoothUuid &gatt); void serviceDiscovered(const QBluetoothUuid &gatt);
void logControllerError(QLowEnergyController::Error err); void logControllerError(QLowEnergyController::Error err);
void logServiceError(QLowEnergyService::ServiceError err); void logServiceError(QLowEnergyService::ServiceError err);
void serviceStateChanges(QLowEnergyService::ServiceState state); void sensorServiceStateChanges(QLowEnergyService::ServiceState state);
void historyServiceStateChanges(QLowEnergyService::ServiceState state);
void serviceCharWritten(QLowEnergyCharacteristic changedChar, QByteArray value); void serviceCharWritten(QLowEnergyCharacteristic changedChar, QByteArray value);
void serviceCharRead(QLowEnergyCharacteristic readChar, QByteArray value); void serviceCharRead(QLowEnergyCharacteristic readChar, QByteArray value);
void historyServiceCharWritten(QLowEnergyCharacteristic changedChar, QByteArray value);
void historyServiceCharWrittenData(QLowEnergyCharacteristic changedChar, QByteArray value);
void historyServiceCharRead(QLowEnergyCharacteristic readChar, QByteArray value);
void historyServiceCharReadData(QLowEnergyCharacteristic readChar, QByteArray value);
QLowEnergyCharacteristic getCharFromValue(QLowEnergyService *service, quint16 characteristic);
QBluetoothDeviceDiscoveryAgent *agent; QBluetoothDeviceDiscoveryAgent *agent;
QList<BluetoothDevices*> devices; QList<BluetoothDevices*> devices;
QLowEnergyController *currentController; QLowEnergyController *currentController;
QLowEnergyService *sensorService; QLowEnergyService *sensorService;
QLowEnergyService *historyService;
const quint16 dataService = 4612; //0x1204 const quint16 dataServiceChar = 4612; //0x1204
const quint16 historyServiceChar = 4614; //0x1206
const quint16 batteryFirmwareChar = 6658;//0x1a02 const quint16 batteryFirmwareChar = 6658;//0x1a02
const quint16 sensorsChar = 6657; //0x1a01 const quint16 sensorsChar = 6657; //0x1a01
const quint16 magicChar = 6656; //0x1a00 const quint16 magicChar = 6656; //0x1a00
const quint16 historyControllerChar = 6672; //0x1a10
const quint16 historyReaderChar = 6673; //0x1a11
const quint16 historyTimeChar = 6674; //0x1a12
const QByteArray magicBytes = QByteArray::fromHex("a01f");
const QByteArray historyReadInit = QByteArray::fromHex("a00000");
float temp = 0.0; float temp = 0.0;
quint32 bright = 0; quint32 bright = 0;
@@ -81,6 +103,11 @@ private:
quint16 conduct = 0; quint16 conduct = 0;
quint8 battery = 0; quint8 battery = 0;
QString version; QString version;
qint64 deviceStartTime;
qint16 currentHistoryEntry;
qint16 lastHistoryEntry;
FloraHistory *floraModel = new FloraHistory();
}; };