├── .clang-format
├── .gitignore
├── LICENSE
├── README.md
├── doc
├── intensity-calculation.svg
├── miters1.png
├── pathologically-sharp-spikes.svg
├── screenshot-weather.png
├── screenshot.png
└── vertex-shader.svg
├── examples
├── GraphStack.qml
├── HoverCursor.qml
├── InfluxWeatherGraph.qml
├── LabeledSensorGraph.qml
├── LineGraphWithHoverFeedback.qml
├── README
├── hardcoded-data.qml
├── memory.qml
├── pool-pump-current-desktop-widget.qml
├── sensor-graphs.qml
├── sensor-summary-desktop-widget.qml
├── sensors.qml
├── summary.qml
├── temperatures.qml
└── weather-summary-desktop-widget.qml
├── graph.pro
├── graph.qrc
├── influxdb.cpp
├── influxdb.h
├── linegraph.cpp
├── linegraph.h
├── linegraphmodel.cpp
├── linegraphmodel.h
├── linenode.cpp
├── linenode_p.h
├── lmsensors.cpp
├── lmsensors.h
├── main.cpp
├── org
└── ecloud
│ └── charts
│ └── qmldir
├── plugin.cpp
└── shaders
├── LineNode.fsh
└── LineNode.vsh
/.clang-format:
--------------------------------------------------------------------------------
1 | ---
2 | Language: Cpp
3 | # BasedOnStyle: LLVM
4 | AccessModifierOffset: -4
5 | AlignAfterOpenBracket: true
6 | AlignConsecutiveAssignments: false
7 | AlignEscapedNewlinesLeft: false
8 | AlignOperands: true
9 | AlignTrailingComments: true
10 | AllowAllParametersOfDeclarationOnNextLine: true
11 | AllowShortBlocksOnASingleLine: false
12 | AllowShortCaseLabelsOnASingleLine: false
13 | AllowShortFunctionsOnASingleLine: All
14 | AllowShortIfStatementsOnASingleLine: false
15 | AllowShortLoopsOnASingleLine: false
16 | AlwaysBreakAfterDefinitionReturnType: None
17 | AlwaysBreakBeforeMultilineStrings: false
18 | AlwaysBreakTemplateDeclarations: false
19 | BinPackArguments: true
20 | BinPackParameters: true
21 | BreakBeforeBinaryOperators: None
22 | BreakBeforeBraces: Linux
23 | BreakBeforeTernaryOperators: true
24 | BreakConstructorInitializersBeforeComma: false
25 | ColumnLimit: 132
26 | CommentPragmas: '^ IWYU pragma:'
27 | ConstructorInitializerAllOnOneLineOrOnePerLine: false
28 | ConstructorInitializerIndentWidth: 4
29 | ContinuationIndentWidth: 4
30 | Cpp11BracedListStyle: true
31 | DerivePointerAlignment: false
32 | DisableFormat: false
33 | ExperimentalAutoDetectBinPacking: false
34 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ]
35 | IndentCaseLabels: false
36 | IndentWidth: 4
37 | IndentWrappedFunctionNames: false
38 | KeepEmptyLinesAtTheStartOfBlocks: true
39 | MacroBlockBegin: ''
40 | MacroBlockEnd: ''
41 | MaxEmptyLinesToKeep: 1
42 | NamespaceIndentation: None
43 | ObjCBlockIndentWidth: 2
44 | ObjCSpaceAfterProperty: false
45 | ObjCSpaceBeforeProtocolList: true
46 | PenaltyBreakBeforeFirstCallParameter: 19
47 | PenaltyBreakComment: 300
48 | PenaltyBreakFirstLessLess: 120
49 | PenaltyBreakString: 1000
50 | PenaltyExcessCharacter: 1000000
51 | PenaltyReturnTypeOnItsOwnLine: 60
52 | PointerAlignment: Right
53 | SpaceAfterCStyleCast: false
54 | SpaceBeforeAssignmentOperators: true
55 | SpaceBeforeParens: ControlStatements
56 | SpaceInEmptyParentheses: false
57 | SpacesBeforeTrailingComments: 1
58 | SpacesInAngles: false
59 | SpacesInContainerLiterals: true
60 | SpacesInCStyleCastParentheses: false
61 | SpacesInParentheses: false
62 | SpacesInSquareBrackets: false
63 | Standard: Cpp11
64 | TabWidth: 4
65 | UseTab: Never
66 | ...
67 |
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # C++ objects and libs
2 |
3 | *.slo
4 | *.lo
5 | *.o
6 | *.a
7 | *.la
8 | *.lai
9 | *.so
10 | *.dll
11 | *.dylib
12 |
13 | # Qt-es
14 |
15 | /.qmake.cache
16 | /.qmake.stash
17 | *.pro.user
18 | *.pro.user.*
19 | *.qbs.user
20 | *.qbs.user.*
21 | *.moc
22 | moc_*.cpp
23 | qrc_*.cpp
24 | ui_*.h
25 | Makefile*
26 | *build-*
27 |
28 | # QtCreator
29 |
30 | *.autosave
31 |
32 | # QtCtreator Qml
33 | *.qmlproject.user
34 | *.qmlproject.user.*
35 |
36 | # QtCtreator CMake
37 | CMakeLists.txt.user
38 |
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # qqchart
2 | GPU-powered line charts in Qt Quick
3 |
4 |
5 |
6 |
7 | The holy grail is to be able to send raw data to the GPU and have it
8 | draw a line graph, without needing much preparation on the CPU side
9 | (other than having it in time-ordered form, which comes naturally).
10 |
11 | Of course it's still pretty far from that. So far I have to pad out
12 | a vertex array to send over, and then the GPU can rearrange the vertices
13 | to achieve an antialiased line rendering.
14 |
15 | But I can monitor my system sensors with a pretty low CPU usage, anyway.
16 | Can also graph data from InfluxDB; I'm using that for a weather station,
17 | among other things.
18 |
19 | To try it out:
20 | ```
21 | qmake; make
22 | qml -I . examples/sensor-summary-desktop-widget.qml
23 | ```
24 |
25 | To install:
26 | ```
27 | make install
28 | ```
29 |
30 | You can then use it in your own QML scripts:
31 | ```
32 | import org.ecloud.charts 1.0
33 | ```
34 |
35 | Issues
36 |
37 | - [ ] data with sharp spikes renders "ghostly", shimmering, coming and going...
38 |
39 | Incomplete features
40 |
41 | - [ ] labels, grids and tickmarks (see the axis-labels branch)
42 |
43 |
--------------------------------------------------------------------------------
/doc/intensity-calculation.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
133 |
--------------------------------------------------------------------------------
/doc/miters1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ec1oud/qqchart/905a55ab94e1025fe9a7b6c0282942f55188122c/doc/miters1.png
--------------------------------------------------------------------------------
/doc/pathologically-sharp-spikes.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
379 |
--------------------------------------------------------------------------------
/doc/screenshot-weather.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ec1oud/qqchart/905a55ab94e1025fe9a7b6c0282942f55188122c/doc/screenshot-weather.png
--------------------------------------------------------------------------------
/doc/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ec1oud/qqchart/905a55ab94e1025fe9a7b6c0282942f55188122c/doc/screenshot.png
--------------------------------------------------------------------------------
/doc/vertex-shader.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
643 |
--------------------------------------------------------------------------------
/examples/GraphStack.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.0
2 | import QtQuick.Controls 1.4
3 |
4 | import org.ecloud.charts 1.0
5 |
6 | Item {
7 | id: root
8 | property alias color: graph.color
9 | property alias antialiasing: graph.antialiasing
10 | property real timeScale: 1
11 | property LineGraphModel model: null
12 |
13 | function newSample(i) {
14 | // return (Math.sin(i / 20.0 * Math.PI * 2) + 1) * 0.4;
15 | // return (Math.sin(i / 20.0 * Math.PI * 2) + 1) * 0.4 + Math.random() * 0.05;
16 | return Math.sin(i / 20.0 * Math.PI * 2) * 0.95 + Math.random() * 0.05;
17 | }
18 |
19 | property int offset: 25;
20 |
21 | LineGraph {
22 | id: graph
23 | anchors.fill: parent
24 | model: root.model
25 | minValue: -1 + offsetSlider.value
26 | maxValue: 1 + offsetSlider.value
27 | lineWidth: widthSlider.value
28 | color: "lightsteelblue"
29 | fillColorBelow: fillBelowCb.checked ? "#22FF2222" : "transparent"
30 | fillColorAbove: fillAboveCb.checked ? "#222222FF" : "transparent"
31 | warningMinColor: "yellow"
32 | warningMaxColor: "orange"
33 | wireframe: false
34 | visible: fillCb.checked
35 | opacity: 0.8
36 | timeSpan: 40 * root.timeScale
37 | }
38 |
39 | LineGraph {
40 | id: wireframe
41 | anchors.fill: graph
42 | model: root.model
43 | minValue: -1 + offsetSlider.value
44 | maxValue: 1 + offsetSlider.value
45 | lineWidth: widthSlider.value
46 | color: graph.visible ? "black" : "white"
47 | fillColorBelow: fillBelowCb.checked ? "#F22" : "transparent"
48 | fillColorAbove: fillAboveCb.checked ? "#22F" : "transparent"
49 | warningMinColor: "black"
50 | warningMaxColor: "black"
51 | wireframe: true
52 | timeSpan: graph.timeSpan
53 | visible: wireframeCb.checked
54 | }
55 |
56 | LineGraph {
57 | id: plainLine
58 | anchors.fill: graph
59 | model: root.model
60 | minValue: -1 + offsetSlider.value
61 | maxValue: 1 + offsetSlider.value
62 | lineWidth: 0
63 | color: "red"
64 | warningMinColor: "red"
65 | warningMaxColor: "red"
66 | wireframe: true
67 | timeSpan: graph.timeSpan
68 | visible: originalLineCb.checked
69 | }
70 |
71 | Timer {
72 | id: timer
73 | interval: 500
74 | repeat: true
75 | running: timerRun.checked
76 | onTriggered: model.appendSample(root.newSample(++root.offset))
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/examples/HoverCursor.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.5
2 |
3 | MouseArea {
4 | id: hoverCursor
5 | anchors.fill: parent
6 | hoverEnabled: true
7 | property real hoverX: containsMouse ? rect.x : -1
8 | property real timeSpan: -1
9 | property date lastSampleTime: new Date()
10 | Rectangle {
11 | id: rect
12 | width: 1
13 | height: parent.height
14 | color: "red"
15 | x: visible ? Math.min(parent.mouseX, parent.width - 10) : -1
16 | visible: parent.containsMouse
17 | Rectangle {
18 | color: "#AAFFDDDD"
19 | border.color: "red"
20 | anchors.fill: text
21 | anchors.margins: -2
22 | anchors.leftMargin: 0
23 | anchors.topMargin: 2
24 | rotation: -90
25 | transformOrigin: Item.TopLeft
26 | }
27 | Text {
28 | id: text
29 | color: "#882222"
30 | visible: hoverCursor.timeSpan > 0
31 | property date cursorTime: new Date(hoverCursor.lastSampleTime.getTime() + (hoverCursor.timeSpan * (hoverCursor.mouseX - hoverCursor.width) / hoverCursor.width * 1000))
32 | text: cursorTime.toISOString().slice(0, 10) + " " + cursorTime.toISOString().slice(11, 16)
33 | rotation: -90
34 | transformOrigin: Item.TopLeft
35 | y: parent.height / 2 + width / 2
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/examples/InfluxWeatherGraph.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.5
2 | import org.ecloud.charts 1.0
3 |
4 | Rectangle {
5 | id: root
6 | width: 580
7 | height: 360
8 | color: "transparent"
9 | border.color: "#6e4b4d"
10 | antialiasing: true
11 | radius: 20
12 | property int timespanHours : 240
13 | property bool currentVisible: false
14 | InfluxQuery {
15 | id: query
16 | server: "http://localhost:8086"
17 | database: "weather"
18 | measurement: "uradmonitor"
19 | fields: ["temperature", "pressure", "humidity"]
20 | //wherePairs: [{"stationId": "41000008"}]
21 | wherePairs: [{"stationId": "820000ED"}]
22 | timeConstraint: "> now() - " + root.timespanHours + "h"
23 | updateIntervalMs: sampleInterval * 1000
24 | sampleInterval: timespanHours * 3600 / root.width * 2
25 | Component.onCompleted: sampleAllValues()
26 | }
27 | ListView {
28 | id: list
29 | anchors.fill: parent
30 | anchors.margins: 8
31 | model: query.values
32 | spacing: 4
33 | clip: true
34 |
35 | delegate: Item {
36 | width: list.width
37 | property int visibleRows: Math.min(4, list.model.length)
38 | property bool isTemperature: label === "temperature"
39 | property bool isPressure: label === "pressure"
40 | height: (list.height - 4 * (visibleRows - 1)) / visibleRows
41 | Text {
42 | anchors.verticalCenter: parent.verticalCenter
43 | anchors.right: parent.right
44 | color: "darkgrey"
45 | styleColor: "black"
46 | style: Text.Outline
47 | font.pointSize: 24
48 | font.bold: true
49 | text: model.currentValue.toFixed(isPressure ? 0 : 2) + model.unit
50 | visible: currentVisible
51 | }
52 | LineGraphWithHoverFeedback {
53 | id: graph
54 | model: modelData
55 | anchors.fill: parent
56 | timeSpan: root.timespanHours * 3600
57 | color: isTemperature ? "green" : "yellow"
58 | minValue: model.minValue // continuously, along with autoScale
59 | maxValue: model.maxValue
60 | warningMinColor: isTemperature ? "cyan" : "orange"
61 | warningMaxColor: "orange"
62 | lineWidth: isTemperature ? 2 : 1
63 | hoverX: hoverCursor.mouseX
64 | Component.onCompleted: {
65 | model.clipValues = false // because we will auto-scale
66 | if (isTemperature)
67 | model.additiveCorrection = -5 // -11 // known-inaccurate sensor
68 | }
69 | onSamplesChanged: model.autoScale()
70 | }
71 | Text {
72 | text: label + ": last " + (root.timespanHours > 24 ?
73 | (root.timespanHours / 24) + " days" :
74 | root.timespanHours + " hours")
75 | font.bold: true
76 | color: "black"
77 | }
78 | Text {
79 | anchors {
80 | right: parent.right
81 | top: parent.top
82 | }
83 | horizontalAlignment: Text.Right
84 | text: maxValue
85 | color: "black"
86 | }
87 | Text {
88 | anchors {
89 | right: parent.right
90 | bottom: parent.bottom
91 | }
92 | horizontalAlignment: Text.Right
93 | text: minValue
94 | color: "black"
95 | }
96 | }
97 | }
98 | HoverCursor {
99 | id: hoverCursor
100 | anchors.margins: 8
101 | timeSpan: query1.values[0].timeSpan
102 | lastSampleTime: query1.lastSampleTime
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/examples/LabeledSensorGraph.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.5
2 | import org.ecloud.charts 1.0
3 |
4 | LineGraphWithHoverFeedback {
5 | id: root
6 | property int labelLine: 0
7 | timeSpan: 600
8 | height: 100
9 | x: 4
10 | width: parent.width - 8
11 | property alias lineHeight: label.font.pixelSize
12 | Text {
13 | id: label
14 | color: parent.color
15 | text: model.label + (model.chipName === "" ? "" : " (" + model.chipName + ")")
16 | y: labelLine * font.pixelSize
17 | font.weight: Font.Bold
18 | }
19 | Row {
20 | anchors.left: parent.horizontalCenter
21 | spacing: 10
22 | Text {
23 | color: "white"
24 | text: model.currentValue.toFixed(2) + " " + model.unit
25 | y: labelLine * font.pixelSize
26 | font.weight: Font.Bold
27 | }
28 | Text {
29 | color: "darkgrey"
30 | text: "(max " + Math.round(model.maxSampleValue) + " / " + Math.round(root.maxValue) + ")"
31 | y: labelLine * font.pixelSize
32 | font.weight: Font.Bold
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/examples/LineGraphWithHoverFeedback.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.5
2 | import org.ecloud.charts 1.0
3 |
4 | LineGraph {
5 | id: root
6 | property int hoverX: -1
7 | property var hoverSample: null
8 |
9 | function updateHover() {
10 | if (hoverX < 0) {
11 | hoverSample = null
12 | } else {
13 | hoverSample = sampleNearestX(hoverX)
14 | }
15 | }
16 |
17 | onHoverXChanged: updateHover()
18 | onSamplesChanged: updateHover()
19 |
20 | Rectangle {
21 | id: pointFeedback
22 | x: hoverSample ? hoverSample.x - width / 2 : -100
23 | y: hoverSample ? hoverSample.y - height / 2 : -100
24 | color: "red" // root.color
25 | width: 7
26 | height: 7
27 | radius: width / 2
28 | border.color: "white"
29 | antialiasing: true
30 | // visible: hoverSample
31 |
32 | Rectangle {
33 | id: labelBackground
34 | color: "#55555533"
35 | border.color: root.color
36 | width: valueLabel.implicitWidth * 1.2
37 | height: valueLabel.implicitHeight * 1.5
38 | radius: height * 0.2
39 | anchors.right: parent.horizontalCenter
40 | anchors.bottom: parent.bottom
41 | anchors.bottomMargin: 10
42 | Text {
43 | id: valueLabel
44 | anchors.centerIn: parent
45 | text: hoverSample ? hoverSample.value.toFixed(2) : ""
46 | color: "white"
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/examples/README:
--------------------------------------------------------------------------------
1 | QML2_IMPORT_PATH=.. qml example.qml
2 |
--------------------------------------------------------------------------------
/examples/hardcoded-data.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.0
2 | import QtQuick.Controls 1.4
3 | import QtQuick.Controls.Styles 1.4
4 |
5 | import org.ecloud.charts 1.0
6 |
7 | Rectangle {
8 | width: 990
9 | height: 480
10 | color: "black"
11 |
12 | GraphStack {
13 | id: graphs
14 | anchors.fill: parent
15 | anchors.margins: 20
16 | anchors.bottomMargin: 100
17 | antialiasing: aaCb.checked
18 | timeScale: hZoomSlider.value
19 | color: "lightsteelblue"
20 | model: LineGraphModel {
21 | label: "example data"
22 | }
23 | Component.onCompleted: {
24 | var time = new Date()
25 | time = new Date(time.getTime() - 1000 * 30)
26 | model.appendSample(0.3, time)
27 | time = new Date(time.getTime() + 1000)
28 | model.appendSample(0.5, time)
29 |
30 | // upward spikes
31 | time = new Date(time.getTime() + 50)
32 | model.appendSample(0, time)
33 | time = new Date(time.getTime() + 100)
34 | model.appendSample(0, time)
35 | time = new Date(time.getTime() + 50)
36 | model.appendSample(1, time)
37 | time = new Date(time.getTime() + 20)
38 | model.appendSample(1, time)
39 | time = new Date(time.getTime() + 50)
40 | model.appendSample(0, time)
41 | time = new Date(time.getTime() + 100)
42 | model.appendSample(0, time)
43 |
44 | time = new Date(time.getTime() + 50)
45 | model.appendSample(0, time)
46 | time = new Date(time.getTime() + 100)
47 | model.appendSample(0, time)
48 | time = new Date(time.getTime() + 50)
49 | model.appendSample(1, time)
50 | time = new Date(time.getTime() + 50)
51 | model.appendSample(1, time)
52 | time = new Date(time.getTime() + 50)
53 | model.appendSample(0, time)
54 | time = new Date(time.getTime() + 200)
55 | model.appendSample(0, time)
56 |
57 | // downward spikes
58 | time = new Date(time.getTime() + 50)
59 | model.appendSample(1, time)
60 | time = new Date(time.getTime() + 100)
61 | model.appendSample(1, time)
62 |
63 | time = new Date(time.getTime() + 50)
64 | model.appendSample(0, time)
65 | time = new Date(time.getTime() + 20)
66 | model.appendSample(0, time)
67 | time = new Date(time.getTime() + 50)
68 | model.appendSample(1, time)
69 | time = new Date(time.getTime() + 100)
70 | model.appendSample(1, time)
71 |
72 | time = new Date(time.getTime() + 50)
73 | model.appendSample(0, time)
74 | time = new Date(time.getTime() + 50)
75 | model.appendSample(0, time)
76 | time = new Date(time.getTime() + 50)
77 | model.appendSample(1, time)
78 | time = new Date(time.getTime() + 100)
79 | model.appendSample(1, time)
80 |
81 | // "square" waves
82 | time = new Date(time.getTime() + 40)
83 | model.appendSample(0, time)
84 | time = new Date(time.getTime() + 1000)
85 | model.appendSample(0, time)
86 | time = new Date(time.getTime() + 40)
87 | model.appendSample(1, time)
88 | time = new Date(time.getTime() + 1000)
89 | model.appendSample(1, time)
90 | time = new Date(time.getTime() + 100)
91 | model.appendSample(0, time)
92 | time = new Date(time.getTime() + 1000)
93 | model.appendSample(0, time)
94 | time = new Date(time.getTime() + 100)
95 | model.appendSample(1, time)
96 | time = new Date(time.getTime() + 1000)
97 | model.appendSample(1, time)
98 | time = new Date(time.getTime() + 100)
99 | model.appendSample(0, time)
100 | time = new Date(time.getTime() + 1000)
101 | model.appendSample(0, time)
102 | time = new Date(time.getTime() + 100)
103 | model.appendSample(1, time)
104 | time = new Date(time.getTime() + 1000)
105 | model.appendSample(1, time)
106 | time = new Date(time.getTime() + 100)
107 | model.appendSample(0, time)
108 | time = new Date(time.getTime() + 1000)
109 | model.appendSample(0, time)
110 | time = new Date(time.getTime() + 100)
111 | model.appendSample(1, time)
112 | time = new Date(time.getTime() + 1000)
113 | model.appendSample(0, time)
114 |
115 | // backtrack
116 | time = new Date(time.getTime() - 500)
117 | model.appendSample(0.5, time)
118 | time = new Date(time.getTime() + 700)
119 | model.appendSample(0, time)
120 | time = new Date(time.getTime() + 100)
121 | model.appendSample(0.2, time)
122 | time = new Date(time.getTime() + 100)
123 | model.appendSample(0, time)
124 | time = new Date(time.getTime() + 100)
125 | model.appendSample(1, time)
126 | time = new Date(time.getTime() + 1850)
127 | model.appendSample(0, time)
128 | time = new Date(time.getTime() + 1000)
129 | model.appendSample(1, time)
130 | time = new Date(time.getTime() + 1000)
131 | model.appendSample(0.9, time)
132 | time = new Date(time.getTime() + 1000)
133 | model.appendSample(1.01, time)
134 | time = new Date(time.getTime() + 1000)
135 | model.appendSample(0.98, time)
136 | time = new Date(time.getTime() + 1000)
137 | model.appendSample(0.9, time)
138 | time = new Date(time.getTime() + 1000)
139 | model.appendSample(0.2, time)
140 | time = new Date(time.getTime() + 1000)
141 | model.appendSample(0.02, time)
142 | time = new Date(time.getTime() + 1000)
143 | model.appendSample(0, time)
144 | time = new Date(time.getTime() + 1000)
145 | model.appendSample(0.01, time)
146 | time = new Date(time.getTime() + 1000)
147 | model.appendSample(0.03, time)
148 | time = new Date(time.getTime() + 1000)
149 | model.appendSample(0.02, time)
150 | time = new Date(time.getTime() + 1000)
151 | model.appendSample(0.04, time)
152 | time = new Date(time.getTime() + 1000)
153 | model.appendSample(0.07, time)
154 | time = new Date(time.getTime() + 1000)
155 | model.appendSample(0.05, time)
156 | time = new Date(time.getTime() + 1000)
157 | model.appendSample(0.06, time)
158 | time = new Date(time.getTime() + 1000)
159 | model.appendSample(0.18, time)
160 | time = new Date(time.getTime() + 1000)
161 | model.appendSample(0.09, time)
162 | time = new Date(time.getTime() + 1000)
163 | model.appendSample(0.1, time)
164 | time = new Date(time.getTime() + 1000)
165 | model.appendSample(0.08, time)
166 | time = new Date(time.getTime() + 1000)
167 | model.appendSample(-0.06, time)
168 | time = new Date(time.getTime() + 1000)
169 | model.appendSample(0.04, time)
170 | time = new Date(time.getTime() + 1000)
171 | model.appendSample(0.02, time)
172 | time = new Date(time.getTime() + 1000)
173 | model.appendSample(0, time)
174 | time = new Date(time.getTime() + 1000)
175 | model.appendSample(0.1, time)
176 | }
177 |
178 | }
179 |
180 | Rectangle {
181 | anchors.fill: graphs
182 | color: "transparent"
183 | border.color: "#40444444"
184 | border.width: 2
185 | Flow {
186 | anchors.top: parent.bottom
187 | anchors.margins: 6
188 | width: parent.width
189 | spacing: 20
190 | opacity: 0.5
191 | Column {
192 | width: 300
193 | Slider {
194 | id: widthSlider
195 | width: 300
196 | minimumValue: 1
197 | maximumValue: 25
198 | value: 1.3
199 | }
200 | Text {
201 | text: "line width " + widthSlider.value.toFixed(2)
202 | color: "white"
203 | }
204 | }
205 | Column {
206 | width: 300
207 | Slider {
208 | id: offsetSlider
209 | width: 300
210 | minimumValue: -1
211 | maximumValue: +1
212 | }
213 | Text {
214 | text: "y offset " + offsetSlider.value.toFixed(2)
215 | color: "white"
216 | }
217 | }
218 | Column {
219 | width: 300
220 | Slider {
221 | id: hZoomSlider
222 | width: 300
223 | minimumValue: 1
224 | maximumValue: 50
225 | }
226 | Text {
227 | text: "time scale " + hZoomSlider.value.toFixed(2)
228 | color: "white"
229 | }
230 | }
231 | Component {
232 | id: whiteCheckboxStyle
233 | CheckBoxStyle {
234 | label: Text {
235 | text: control.text
236 | color: "white"
237 | }
238 | }
239 | }
240 |
241 | CheckBox {
242 | id: fillCb
243 | text: "fill"
244 | style: whiteCheckboxStyle
245 | checked: true
246 | }
247 | CheckBox {
248 | id: aaCb
249 | text: "antialiasing"
250 | style: whiteCheckboxStyle
251 | checked: true
252 | }
253 | CheckBox {
254 | id: originalLineCb
255 | text: "actual samples"
256 | style: whiteCheckboxStyle
257 | // checked: true
258 | }
259 | CheckBox {
260 | id: wireframeCb
261 | text: "wireframe"
262 | style: whiteCheckboxStyle
263 | // checked: true
264 | }
265 | CheckBox {
266 | id: timerRun
267 | text: "periodic update"
268 | style: whiteCheckboxStyle
269 | }
270 | CheckBox {
271 | id: fillBelowCb
272 | text: "fill below"
273 | style: whiteCheckboxStyle
274 | }
275 | CheckBox {
276 | id: fillAboveCb
277 | text: "fill above"
278 | style: whiteCheckboxStyle
279 | }
280 | }
281 | }
282 | }
283 |
--------------------------------------------------------------------------------
/examples/memory.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.5
2 | import QtQuick.Window 2.1
3 | import QtGraphicalEffects 1.0
4 | import org.ecloud.charts 1.0
5 |
6 | Window {
7 | width: 480
8 | height: 320
9 | //x: 1920 - width - 4
10 | y: 1080 - height
11 | color: "transparent"
12 | flags: Qt.FramelessWindowHint | Qt.WindowStaysOnBottomHint
13 | visible: true
14 |
15 | Rectangle {
16 | id: content
17 | anchors.fill: parent
18 | visible: false
19 | color: "transparent"
20 | border.color: "#6e4b4d"
21 | antialiasing: true
22 | radius: 20
23 | Repeater {
24 | model: LmSensors.filtered(Sensor.Memory)
25 | LabeledSensorGraph {
26 | model: modelData
27 | color: Qt.rgba(0.3 + index * 0.33, 1 - index * 0.27, 0.7 + index * 0.11, 1)
28 | labelLine: index + 2
29 | lineWidth: 1
30 | visible: modelData.maxValue > 0
31 | }
32 | }
33 | }
34 | DropShadow {
35 | source: content
36 | anchors.fill: content
37 | horizontalOffset: 1
38 | verticalOffset: 1
39 | radius: 8.0
40 | samples: 7
41 | color: "#80000000"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/examples/pool-pump-current-desktop-widget.qml:
--------------------------------------------------------------------------------
1 | #!/bin/env qml
2 |
3 | import QtQuick 2.12
4 | import QtQuick.Window 2.12
5 | import Qt.labs.settings 1.0
6 | import org.ecloud.charts 1.0
7 |
8 | Window {
9 | id: window
10 | width: 740
11 | height: 360
12 | color: "transparent"
13 | flags: Qt.FramelessWindowHint | Qt.WindowStaysOnBottomHint
14 | visible: true
15 |
16 | Settings {
17 | id: settings
18 | category: "PoolPumpDesktopWidget"
19 | property alias x: window.x
20 | property alias y: window.y
21 | property string server
22 | property string user
23 | property string password
24 | }
25 |
26 | property int timespanHours : 720
27 | InfluxQuery {
28 | id: query1
29 | server: settings.server
30 | user: settings.user
31 | password: settings.password
32 | ignoreSslErrors: true // such as self-signed certificate or hostname not matching
33 | // graph for data from https://github.com/ec1oud/devcron/blob/master/statistics/cs5490-current.lua
34 | database: "homeauto"
35 | measurement: "current_max_hourly"
36 | fields: ["max_current"]
37 | wherePairs: [{"device": "poolpump"}]
38 | timeConstraint: "> now() - " + window.timespanHours + "h"
39 | updateIntervalMs: sampleInterval * 1000
40 | sampleInterval: 3600
41 | Component.onCompleted: {
42 | sampleAllValues()
43 | values[0].maxValue = 3.5
44 | }
45 | }
46 | LineGraphWithHoverFeedback {
47 | id: graph
48 | model: query1.values[0]
49 | anchors.fill: parent
50 | anchors.margins: 10
51 | timeSpan: query1.values[0].timeSpan
52 | color: "lightsteelblue"
53 | // fillColorBelow: "lightsteelblue"
54 | warningMaxColor: "lightsteelblue"
55 | minValue: 0
56 | maxValue: 3.5
57 | lineWidth: 1
58 | hoverX: hoverCursor.hoverX
59 | HoverCursor {
60 | id: hoverCursor
61 | timeSpan: query1.values[0].timeSpan
62 | lastSampleTime: query1.lastSampleTime
63 | }
64 | }
65 | Text {
66 | text: "Pool pump current: last " + (window.timespanHours > 24 ?
67 | (window.timespanHours / 24) + " days" :
68 | window.timespanHours + " hours") +
69 | " as of " + query1.lastSampleTime
70 | font.bold: true
71 | color: "black"
72 | }
73 | Text {
74 | anchors {
75 | right: parent.right
76 | top: parent.top
77 | rightMargin: 10
78 | }
79 | horizontalAlignment: Text.Right
80 | text: query1.values[0].maxValue
81 | color: "black"
82 | }
83 | Text {
84 | anchors {
85 | right: parent.right
86 | bottom: parent.bottom
87 | rightMargin: 10
88 | }
89 | horizontalAlignment: Text.Right
90 | text: query1.values[0].minValue
91 | color: "black"
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/examples/sensor-graphs.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.5
2 | import org.ecloud.charts 1.0
3 |
4 | Rectangle {
5 | width: 600
6 | height: 640
7 | color: "black"
8 | ListView {
9 | id: list
10 | anchors.fill: parent
11 | anchors.margins: 6
12 | model: LmSensors.sensors
13 | delegate: Rectangle {
14 | width: parent.width
15 | height: 75
16 | border.color: "#111"
17 | color: "transparent"
18 | LineGraph {
19 | id: graph
20 | model: modelData
21 | anchors.fill: parent
22 | timeSpan: width
23 | }
24 | Text {
25 | text: label + " (" + chipName + "):\nscale " + parent.width / parent.timeSpan +
26 | " min " + minSampleValue.toFixed(2) + " max " + maxSampleValue.toFixed(2) +
27 | " norm " + normalMinValue.toFixed(2) + ".." + normalMaxValue.toFixed(2)
28 | color: "grey"
29 | }
30 | Text {
31 | text: currentValue.toFixed(2) + " " + unit
32 | anchors.horizontalCenter: parent.horizontalCenter
33 | color: "white"
34 | }
35 | Text {
36 | anchors {
37 | right: parent.right
38 | top: parent.top
39 | }
40 | horizontalAlignment: Text.Right
41 | text: maxValue
42 | color: "grey"
43 | }
44 | Text {
45 | anchors {
46 | right: parent.right
47 | bottom: parent.bottom
48 | }
49 | horizontalAlignment: Text.Right
50 | text: minValue
51 | color: "grey"
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/examples/sensor-summary-desktop-widget.qml:
--------------------------------------------------------------------------------
1 | #!/bin/env qml
2 |
3 | import QtQuick 2.5
4 | import QtQuick.Window 2.1
5 | import QtGraphicalEffects 1.0
6 | import Qt.labs.settings 1.0
7 | import org.ecloud.charts 1.0
8 |
9 | Window {
10 | id: window
11 | width: 580
12 | height: 360
13 | color: "transparent"
14 | flags: Qt.FramelessWindowHint | Qt.WindowStaysOnBottomHint
15 | visible: true
16 |
17 | Settings {
18 | category: "SensorSummaryDesktopWidget"
19 | property alias x: window.x
20 | property alias y: window.y
21 | property alias width: window.width
22 | property alias height: window.height
23 | }
24 |
25 | Rectangle {
26 | id: content
27 | anchors.fill: parent
28 | visible: false
29 | color: "transparent"
30 | border.color: "#6e4b4d"
31 | antialiasing: true
32 | radius: 20
33 | Item {
34 | id: bottom
35 | anchors.fill: parent
36 | anchors.topMargin: cpuFrequencyRepeater.implicitHeight
37 |
38 | Repeater {
39 | id: batteries
40 | model: LmSensors.filtered(Sensor.Energy)
41 | LabeledSensorGraph {
42 | model: modelData
43 | color: "lightgreen"
44 | labelLine: index + 1
45 | lineWidth: 1
46 | //visible: modelData.maxSampleValue > 0
47 | hoverX: hoverCursor.hoverX - 4
48 | }
49 | }
50 | Repeater {
51 | id: fans
52 | model: LmSensors.filtered(Sensor.Fan)
53 | LabeledSensorGraph {
54 | model: modelData
55 | color: Qt.rgba(0.3 + index * 0.03, 0.2 + index * 0.17, 1, 1)
56 | labelLine: batteries.count + index + 2
57 | lineWidth: 3
58 | //visible: modelData.maxSampleValue > 0
59 | hoverX: hoverCursor.hoverX - 4
60 | }
61 | }
62 | Repeater {
63 | model: LmSensors.filtered(Sensor.Temperature, "Core 0")
64 | LabeledSensorGraph {
65 | model: modelData
66 | color: "#F85"
67 | labelLine: 0
68 | minValue: 25
69 | maxValue: 85
70 | hoverX: hoverCursor.hoverX - 4
71 | }
72 | }
73 | Repeater {
74 | model: LmSensors.filtered(Sensor.Cpu)
75 | LabeledSensorGraph {
76 | model: modelData
77 | color: "wheat"
78 | labelLine: batteries.count + 1
79 | hoverX: hoverCursor.hoverX - 4
80 | }
81 | }
82 | Repeater {
83 | model: LmSensors.filtered(Sensor.Memory, "Memory Used")
84 | LabeledSensorGraph {
85 | model: modelData
86 | color: "cyan"
87 | labelLine: batteries.count + fans.count + 2
88 | lineWidth: 2
89 | hoverX: hoverCursor.hoverX - 4
90 | }
91 | }
92 | Repeater {
93 | model: LmSensors.filtered(Sensor.Memory, "Memory Cached")
94 | LabeledSensorGraph {
95 | model: modelData
96 | color: "darkgreen"
97 | labelLine: batteries.count + fans.count + 3
98 | lineWidth: 1
99 | hoverX: hoverCursor.hoverX - 4
100 | }
101 | }
102 | Repeater {
103 | model: LmSensors.filtered(Sensor.Memory, "Swap Free")
104 | LabeledSensorGraph {
105 | model: modelData
106 | color: "midnightblue"
107 | labelLine: batteries.count + fans.count + 4
108 | lineWidth: 3
109 | visible: modelData.maxValue > 0
110 | hoverX: hoverCursor.hoverX - 4
111 | }
112 | }
113 | }
114 | Item {
115 | anchors.top: parent.top
116 | anchors.topMargin: 4
117 | width: parent.width
118 | height: 100
119 | Repeater {
120 | id: cpuFrequencyRepeater
121 | model: LmSensors.filtered(Sensor.Frequency)
122 | implicitHeight: count * 15.5
123 | LabeledSensorGraph {
124 | y: lineHeight * index
125 | height: lineHeight / 3
126 | model: modelData
127 | warningMinColor: "saddlebrown"
128 | color: Qt.rgba(0, 0.2 + index * 0.07, 0.1, 1)
129 | warningMaxColor: "cyan"
130 | lineWidth: 1
131 | }
132 | onItemAdded: model[index].downsampleInterval = 15
133 | }
134 | }
135 | Rectangle {
136 | anchors.bottom: bottom.top
137 | anchors.bottomMargin: 2
138 | anchors.margins: 8
139 | anchors.left: parent.left
140 | anchors.right: parent.right
141 | height: 1
142 | color: content.border.color
143 | }
144 | }
145 | DropShadow {
146 | source: content
147 | anchors.fill: content
148 | horizontalOffset: 1
149 | verticalOffset: 1
150 | radius: 8.0
151 | samples: 7
152 | color: "#80000000"
153 | }
154 |
155 | HoverCursor { id: hoverCursor }
156 | }
157 |
--------------------------------------------------------------------------------
/examples/sensors.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.5
2 | import org.ecloud.charts 1.0
3 |
4 | ListView {
5 | width: 200
6 | height: 200
7 | model: LmSensors.sensors
8 | delegate: Text { text: label + ": " + currentValue.toFixed(2) }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/summary.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.5
2 | import org.ecloud.charts 1.0
3 |
4 | Rectangle {
5 | width: 480
6 | height: 320
7 | color: "black"
8 |
9 | Repeater {
10 | model: LmSensors.filtered(Sensor.Fan)
11 | LabeledSensorGraph {
12 | model: modelData
13 | color: Qt.rgba(0.3 + index * 0.03, 0.2 + index * 0.17, 1, 1)
14 | labelLine: index + 2
15 | lineWidth: 3
16 | visible: model.maxSampleValue > 0
17 | hoverX: hoverCursor.hoverX - 10
18 | }
19 | }
20 | Repeater {
21 | model: LmSensors.filtered(Sensor.Temperature, "Core 0")
22 | LabeledSensorGraph {
23 | model: modelData
24 | color: "#F85"
25 | labelLine: 0
26 | minValue: 25
27 | maxValue: 85
28 | hoverX: hoverCursor.hoverX - 10
29 | }
30 | }
31 | Repeater {
32 | model: LmSensors.filtered(Sensor.Cpu)
33 | LabeledSensorGraph {
34 | model: modelData
35 | color: "wheat"
36 | labelLine: 1
37 | hoverX: hoverCursor.hoverX - 10
38 | }
39 | }
40 |
41 | HoverCursor { id: hoverCursor }
42 |
43 | // possible but unnecessary:
44 | // Component.onCompleted: LmSensors.setDownsampleInterval(2)
45 | }
46 |
--------------------------------------------------------------------------------
/examples/temperatures.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.5
2 | import org.ecloud.charts 1.0
3 |
4 | Rectangle {
5 | width: 480
6 | height: 320
7 | color: "black"
8 | property var colors: ["grey", "cyan", "magenta", "violet", "steelblue", "wheat", "beige", "brown"]
9 | Repeater {
10 | id: list
11 | anchors.fill: parent
12 | anchors.margins: 6
13 | model: LmSensors.filtered(Sensor.Temperature)
14 | LineGraph {
15 | model: modelData
16 | anchors.fill: parent
17 | color: colors[index]
18 | Text {
19 | color: parent.color
20 | text: modelData.label + " (" + modelData.chipName + ")"
21 | y: index * font.pixelSize
22 | }
23 | Text {
24 | color: "white"
25 | text: modelData.currentValue.toFixed(2) + " " + modelData.unit
26 | anchors.horizontalCenter: parent.horizontalCenter
27 | y: index * font.pixelSize
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/examples/weather-summary-desktop-widget.qml:
--------------------------------------------------------------------------------
1 | #!/bin/env qml
2 |
3 | import QtQuick 2.5
4 | import QtQuick.Window 2.1
5 | import Qt.labs.settings 1.0
6 |
7 | Window {
8 | id: window
9 | width: 580
10 | height: 728
11 | color: "transparent"
12 | flags: Qt.FramelessWindowHint | Qt.WindowStaysOnBottomHint
13 | visible: true
14 |
15 | Settings {
16 | category: "WeatherSummaryDesktopWidget"
17 | property alias x: window.x
18 | property alias y: window.y
19 | property alias width: window.width
20 | property alias height: window.height
21 | }
22 |
23 | Column {
24 | id: column
25 | spacing: 8
26 | anchors.fill: parent
27 | InfluxWeatherGraph {
28 | timespanHours: 72
29 | height: (window.height - column.spacing) / 2
30 | currentVisible: true
31 | }
32 | InfluxWeatherGraph {
33 | timespanHours: 720 // a month
34 | height: (window.height - column.spacing) / 2
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/graph.pro:
--------------------------------------------------------------------------------
1 | QT += qml quick network
2 | TARGET = org/ecloud/charts/chartsplugin
3 | TEMPLATE = lib
4 |
5 | SOURCES += plugin.cpp \
6 | influxdb.cpp \
7 | linegraph.cpp \
8 | linegraphmodel.cpp \
9 | linenode.cpp \
10 | lmsensors.cpp
11 |
12 | HEADERS += \
13 | influxdb.h \
14 | linegraph.h \
15 | linegraphmodel.h \
16 | linenode_p.h \
17 | lmsensors.h
18 |
19 | RESOURCES += graph.qrc
20 |
21 | LIBS += -lsensors
22 |
23 | QMAKE_CXXFLAGS += -std=c++14
24 |
25 | OTHER_FILES += examples/*.qml org/ecloud/charts/qmldir
26 |
27 | target.path = $$[QT_INSTALL_QML]/org/ecloud/charts
28 | qmldir.path = $$[QT_INSTALL_QML]/org/ecloud/charts
29 | qmldir.files = org/ecloud/charts/qmldir
30 | INSTALLS += target qmldir
31 |
--------------------------------------------------------------------------------
/graph.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | shaders/LineNode.vsh
4 | shaders/LineNode.fsh
5 |
6 |
7 |
--------------------------------------------------------------------------------
/influxdb.cpp:
--------------------------------------------------------------------------------
1 | #include "influxdb.h"
2 |
3 | #include
4 |
5 | Q_LOGGING_CATEGORY(lcInflux, "org.ecloud.charts.model.influxdb")
6 |
7 | InfluxValueSeries::InfluxValueSeries(QString fieldName, QObject *parent)
8 | : LineGraphModel(parent)
9 | {
10 | setLabel(fieldName);
11 | setTimeSpan(INT_MAX);
12 | setDownsampleMethod(NoDownsample); // influx will do that for us
13 | }
14 |
15 | void InfluxValueSeries::setAdditiveCorrection(qreal additiveCorrection)
16 | {
17 | if (m_additiveCorrection == additiveCorrection)
18 | return;
19 |
20 | m_additiveCorrection = additiveCorrection;
21 | emit additiveCorrectionChanged();
22 | }
23 |
24 | void InfluxValueSeries::setMultiplicativeCorrection(qreal multiplicativeCorrection)
25 | {
26 | if (m_multiplicativeCorrection == multiplicativeCorrection)
27 | return;
28 |
29 | m_multiplicativeCorrection = multiplicativeCorrection;
30 | emit multiplicativeCorrectionChanged();
31 | }
32 |
33 | void InfluxValueSeries::finagle(qreal &time, qreal &value)
34 | {
35 | Q_UNUSED(time);
36 | value = value * m_multiplicativeCorrection + m_additiveCorrection;
37 | }
38 |
39 | void appendItems(QQmlListProperty *property, InfluxValueSeries *item)
40 | {
41 | Q_UNUSED(property);
42 | Q_UNUSED(item);
43 | // Do nothing; can't actually append
44 | }
45 |
46 | int itemSize(QQmlListProperty *property) { return static_cast *>(property->data)->size(); }
47 |
48 | InfluxValueSeries *itemAt(QQmlListProperty *property, int index)
49 | {
50 | return static_cast *>(property->data)->at(index);
51 | }
52 |
53 | void clearitemPtr(QQmlListProperty *property) { return static_cast *>(property->data)->clear(); }
54 |
55 | QQmlListProperty InfluxQuery::values()
56 | {
57 | return QQmlListProperty(this, &m_values, &appendItems, &itemSize, &itemAt, &clearitemPtr);
58 | }
59 |
60 | void InfluxQuery::setServer(QUrl server)
61 | {
62 | server.setPath(QLatin1String("/query"));
63 | if (m_server == server)
64 | return;
65 | m_server = server;
66 | emit serverChanged();
67 | }
68 |
69 | void InfluxQuery::setUser(QString user)
70 | {
71 | if (m_user == user)
72 | return;
73 |
74 | m_user = user;
75 | emit userChanged();
76 | }
77 |
78 | void InfluxQuery::setPassword(QString password)
79 | {
80 | if (m_password == password)
81 | return;
82 |
83 | m_password = password;
84 | emit passwordChanged();
85 | }
86 |
87 | void InfluxQuery::setIgnoreSslErrors(bool ignoreSslErrors)
88 | {
89 | if (m_ignoreSslErrors == ignoreSslErrors)
90 | return;
91 |
92 | m_ignoreSslErrors = ignoreSslErrors;
93 | emit ignoreSslErrorsChanged();
94 | }
95 |
96 | void InfluxQuery::setDatabase(QString db)
97 | {
98 | if (m_database == db)
99 | return;
100 |
101 | m_initialized = false;
102 | m_database = db;
103 | emit databaseChanged();
104 | }
105 |
106 | void InfluxQuery::setMeasurement(QString measurement)
107 | {
108 | if (m_measurement == measurement)
109 | return;
110 |
111 | m_initialized = false;
112 | m_measurement = measurement;
113 | emit measurementChanged();
114 | }
115 |
116 | void InfluxQuery::setFields(QStringList fields)
117 | {
118 | if (m_fields == fields)
119 | return;
120 |
121 | m_initialized = false;
122 | m_fields = fields;
123 | emit fieldsChanged();
124 | }
125 |
126 | void InfluxQuery::setWherePairs(QJsonArray wherePairs)
127 | {
128 | m_initialized = false;
129 | m_wherePairs = wherePairs;
130 | emit wherePairsChanged();
131 | }
132 |
133 | void InfluxQuery::setTimeConstraint(QString timeConstraint)
134 | {
135 | if (m_timeConstraint == timeConstraint)
136 | return;
137 |
138 | m_initialized = false;
139 | m_timeConstraint = timeConstraint;
140 | emit timeConstraintChanged();
141 | }
142 |
143 | void InfluxQuery::setSampleInterval(int sampleInterval)
144 | {
145 | if (m_sampleInterval == sampleInterval)
146 | return;
147 |
148 | m_sampleInterval = sampleInterval;
149 | emit sampleIntervalChanged();
150 | }
151 |
152 | void InfluxQuery::onAuthenticationRequired(QNetworkReply *reply, QAuthenticator *authenticator)
153 | {
154 | authenticator->setUser(m_user);
155 | authenticator->setPassword(m_password);
156 | }
157 |
158 | void InfluxQuery::setUpdateIntervalMs(int updateIntervalMs)
159 | {
160 | if (m_updateIntervalMs == updateIntervalMs)
161 | return;
162 |
163 | m_updateIntervalMs = updateIntervalMs;
164 | if (m_timerId > 0)
165 | killTimer(m_timerId);
166 | m_timerId = startTimer(m_updateIntervalMs);
167 | emit updateIntervalMsChanged();
168 | }
169 |
170 | void InfluxQuery::init()
171 | {
172 | // SELECT temperature,pressure FROM \"uradmonitor\" WHERE "stationId" = '41000008' AND time > now() - 10h"
173 | // or
174 | // SELECT mean(temperature),mean(pressure) FROM uradmonitor WHERE "stationId" = '41000008' AND time > now() - 240h GROUP BY time(10m)
175 | m_queryString = QLatin1String("SELECT ");
176 | int i = 0;
177 | for (auto f : m_fields) {
178 | if (i++)
179 | m_queryString += QLatin1Char(',');
180 | if (m_sampleInterval)
181 | m_queryString += QString(QLatin1String("mean(%1)")).arg(f);
182 | else
183 | m_queryString += f;
184 | }
185 | m_queryString += QLatin1String(" FROM ") + m_measurement;
186 | QStringList whereAnds;
187 | for (const QJsonValue &wpair : m_wherePairs) {
188 | const QJsonObject o = wpair.toObject();
189 | for (const QString &k : o.keys())
190 | whereAnds << QString(QLatin1String("%1 = '%2'")).arg(k).arg(o.value(k).toString());
191 | }
192 | m_queryString += QLatin1String(" WHERE ");
193 | //qDebug() << whereAnds;
194 | if (!whereAnds.isEmpty())
195 | m_queryString += whereAnds.join(QLatin1String(" AND "));
196 |
197 | // only get the full range initially; otherwise just get incremental updates
198 | if (m_lastUpdate.isNull()) {
199 | if (!m_timeConstraint.isEmpty()) {
200 | if (!whereAnds.isEmpty())
201 | m_queryString += QLatin1String(" AND ");
202 | m_queryString += QLatin1String("time ") + m_timeConstraint;
203 | }
204 | } else {
205 | int secsSinceLast = m_lastUpdate.secsTo(QDateTime::currentDateTime());
206 | if (!whereAnds.isEmpty())
207 | m_queryString += QLatin1String("AND ");
208 | m_queryString += QString(QLatin1String("time > now() - %1s")).arg(secsSinceLast);
209 | }
210 |
211 | if (m_sampleInterval)
212 | m_queryString += QString(QLatin1String(" GROUP BY time(%1s)")).arg(m_sampleInterval);
213 |
214 | qCDebug(lcInflux) << m_queryString;
215 |
216 | QUrlQuery quq;
217 | quq.addQueryItem(QLatin1String("db"), m_database);
218 | quq.addQueryItem(QLatin1String("q"), m_queryString);
219 | m_queryUrl = m_server;
220 | m_queryUrl.setQuery(quq);
221 | qCDebug(lcInflux) << m_queryUrl;
222 | if (m_lastUpdate.isNull()) {
223 | m_values.clear();
224 | for (const QString & field : m_fields) {
225 | InfluxValueSeries *v = new InfluxValueSeries(field);
226 | // initial min/max; we'll refine it later from influx
227 | if (field == QLatin1String("temperature")) {
228 | v->setMinValue(-20);
229 | v->setNormalMinValue(0);
230 | v->setMaxValue(40);
231 | v->setNormalMaxValue(25);
232 | v->setUnit(QString::fromUtf8("°C"));
233 | } else if (field == QLatin1String("pressure")) {
234 | v->setMinValue(95000);
235 | v->setNormalMinValue(98000);
236 | v->setMaxValue(105000);
237 | v->setNormalMaxValue(103000);
238 | v->setUnit(QLatin1String("hPa"));
239 | v->setMultiplier(0.01);
240 | } else if (field == QLatin1String("humidity")) {
241 | v->setMinValue(0);
242 | v->setNormalMinValue(50);
243 | v->setMaxValue(100);
244 | v->setNormalMaxValue(90);
245 | v->setUnit(QLatin1String("%"));
246 | } else if (field == QLatin1String("moisture")) {
247 | v->setMinValue(0);
248 | v->setNormalMinValue(40);
249 | v->setMaxValue(100);
250 | v->setNormalMaxValue(90);
251 | v->setUnit(QLatin1String("%"));
252 | }
253 | m_values.append(v);
254 | }
255 | emit valuesChanged();
256 | }
257 |
258 | m_influxReq = QNetworkRequest(m_queryUrl);
259 | m_initialized = m_lastUpdate.isValid();
260 | m_lastUpdate = QDateTime::currentDateTime();
261 | emit initializedChanged();
262 | }
263 |
264 | InfluxQuery::InfluxQuery(QObject *parent) : QObject(parent)
265 | {
266 | connect(&m_nam, &QNetworkAccessManager::authenticationRequired,
267 | this, &InfluxQuery::onAuthenticationRequired);
268 | }
269 |
270 | bool InfluxQuery::sampleAllValues()
271 | {
272 | if (!m_initialized)
273 | init();
274 |
275 | m_netReply = m_nam.get(m_influxReq);
276 | if (m_ignoreSslErrors)
277 | m_netReply->ignoreSslErrors();
278 | connect(m_netReply, &QNetworkReply::finished, this, &InfluxQuery::networkFinished);
279 | connect(m_netReply, SIGNAL(error(QNetworkReply::NetworkError)),
280 | this, SLOT(networkError(QNetworkReply::NetworkError)));
281 |
282 | return true;
283 | }
284 |
285 | void InfluxQuery::networkError(QNetworkReply::NetworkError e)
286 | {
287 | qCWarning(lcInflux) << e;
288 | m_netReply->disconnect();
289 | m_netReply->deleteLater();
290 | m_netReply = nullptr;
291 | }
292 |
293 | void InfluxQuery::networkFinished()
294 | {
295 | QJsonParseError err;
296 | QJsonDocument jdoc = QJsonDocument::fromJson(m_netReply->readAll(), &err);
297 | if (err.error == QJsonParseError::NoError) {
298 | // qDebug() << jdoc.object().value(QLatin1String("results")).toArray();
299 | QJsonObject o = jdoc.object().value(QLatin1String("results")).toArray().first().toObject();
300 | QJsonArray arr = o.value("series").toArray().first().toObject().value("values").toArray();
301 | QDateTime first;
302 | int count = 0;
303 | for (auto o : arr) {
304 | QJsonArray samples = o.toArray();
305 | m_lastSampleTime = QDateTime::fromString(samples.takeAt(0).toString(), Qt::ISODateWithMs);
306 | if (first.isNull())
307 | first = m_lastSampleTime;
308 | ++count;
309 | // qDebug() << last.toString() << samples;
310 | for (int i = 0; i < m_values.count(); ++i) {
311 | qreal val = samples.takeAt(0).toDouble(qQNaN());
312 | m_values[i]->appendSampleMs(val, m_lastSampleTime);
313 | }
314 | }
315 | int timeSpan = int(qAbs(first.secsTo(m_lastSampleTime)));
316 | m_lastUpdate = QDateTime::currentDateTime();
317 | qCDebug(lcInflux) << "for" << m_fields << "got" << count << "samples from" << first << "to" << m_lastSampleTime << "timespan" << timeSpan;
318 | if (!m_initialized) {
319 | for (int i = 0; i < m_values.count(); ++i) {
320 | m_values[i]->setTimeSpan(timeSpan);
321 | qCDebug(lcInflux) << "value range of" << m_fields.at(i) << m_values.at(i)->minSampleValue() << m_values.at(i)->maxSampleValue();
322 | }
323 | }
324 | emit valuesUpdated();
325 | } else {
326 | qCWarning(lcInflux) << err.errorString();
327 | }
328 | m_netReply->disconnect();
329 | m_netReply->deleteLater();
330 | m_netReply = nullptr;
331 | }
332 |
--------------------------------------------------------------------------------
/influxdb.h:
--------------------------------------------------------------------------------
1 | #ifndef INFLUXDB_H
2 | #define INFLUXDB_H
3 |
4 | #include
5 |
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 | #include "linegraphmodel.h"
15 |
16 | class InfluxValueSeries : public LineGraphModel
17 | {
18 | Q_OBJECT
19 | Q_PROPERTY(qreal additiveCorrection READ additiveCorrection WRITE setAdditiveCorrection NOTIFY additiveCorrectionChanged)
20 | Q_PROPERTY(qreal multiplicativeCorrection READ multiplicativeCorrection WRITE setMultiplicativeCorrection NOTIFY multiplicativeCorrectionChanged)
21 |
22 | public:
23 | explicit InfluxValueSeries(QString fieldName, QObject *parent = 0);
24 |
25 | qreal additiveCorrection() const { return m_additiveCorrection; }
26 | void setAdditiveCorrection(qreal additiveCorrection);
27 |
28 | qreal multiplicativeCorrection() const { return m_multiplicativeCorrection; }
29 | void setMultiplicativeCorrection(qreal multiplicativeCorrection);
30 |
31 | signals:
32 | void additiveCorrectionChanged();
33 | void multiplicativeCorrectionChanged();
34 |
35 | protected:
36 | virtual void finagle(qreal &time, qreal &value);
37 |
38 | private:
39 | qreal m_additiveCorrection = 0;
40 | qreal m_multiplicativeCorrection = 1;
41 | };
42 |
43 |
44 | // curl -GET 'http://localhost:8086/query' --data-urlencode "db=weather" --data-urlencode "q=SELECT temperature,pressure FROM \"uradmonitor\" WHERE "stationId" = '41000008' AND time > now() - 10h"
45 |
46 | class InfluxQuery : public QObject
47 | {
48 | Q_OBJECT
49 | // input
50 | Q_PROPERTY(QUrl server READ server WRITE setServer NOTIFY serverChanged)
51 | Q_PROPERTY(QString user READ user WRITE setUser NOTIFY userChanged)
52 | Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged)
53 | Q_PROPERTY(bool ignoreSslErrors READ ignoreSslErrors WRITE setIgnoreSslErrors NOTIFY ignoreSslErrorsChanged)
54 | Q_PROPERTY(QString database READ database WRITE setDatabase NOTIFY databaseChanged)
55 | Q_PROPERTY(QString measurement READ measurement WRITE setMeasurement NOTIFY measurementChanged)
56 | Q_PROPERTY(QStringList fields READ fields WRITE setFields NOTIFY fieldsChanged)
57 | Q_PROPERTY(QJsonArray wherePairs READ wherePairs WRITE setWherePairs NOTIFY wherePairsChanged)
58 | Q_PROPERTY(QString timeConstraint READ timeConstraint WRITE setTimeConstraint NOTIFY timeConstraintChanged) // e.g. "> now() - 12h"
59 | Q_PROPERTY(int updateIntervalMs READ updateIntervalMs WRITE setUpdateIntervalMs NOTIFY updateIntervalMsChanged)
60 | Q_PROPERTY(int sampleInterval READ sampleInterval WRITE setSampleInterval NOTIFY sampleIntervalChanged) // seconds between graphed samples
61 |
62 | // output
63 | Q_PROPERTY(bool initialized READ initialized NOTIFY initializedChanged)
64 | Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged)
65 | Q_PROPERTY(QQmlListProperty values READ values NOTIFY valuesChanged)
66 | Q_PROPERTY(QDateTime lastUpdate READ lastUpdate NOTIFY valuesUpdated) // time when the query ran
67 | Q_PROPERTY(QDateTime lastSampleTime READ lastSampleTime NOTIFY valuesUpdated) // timestamp of the last datapoint in values
68 |
69 | public:
70 | explicit InfluxQuery(QObject *parent = nullptr);
71 |
72 | Q_INVOKABLE bool sampleAllValues();
73 |
74 | bool initialized() { return m_initialized; }
75 | QString errorMessage() { return m_errorMessage; }
76 |
77 | int updateIntervalMs() const { return m_updateIntervalMs; }
78 | void setUpdateIntervalMs(int updateIntervalMs);
79 |
80 | QQmlListProperty values();
81 |
82 | QDateTime lastUpdate() { return m_lastUpdate; }
83 | QDateTime lastSampleTime() const { return m_lastSampleTime; }
84 |
85 | QUrl server() const { return m_server; }
86 | void setServer(QUrl server);
87 |
88 | QString user() const { return m_user;}
89 | void setUser(QString user);
90 |
91 | QString password() const { return m_password; }
92 | void setPassword(QString password);
93 |
94 | bool ignoreSslErrors() const { return m_ignoreSslErrors; }
95 | void setIgnoreSslErrors(bool ignoreSslErrors);
96 |
97 | QString database() const { return m_database; }
98 | void setDatabase(QString database);
99 |
100 | QString measurement() const { return m_measurement; }
101 | void setMeasurement(QString measurement);
102 |
103 | QStringList fields() const { return m_fields; }
104 | void setFields(QStringList fields);
105 |
106 | QJsonArray wherePairs() const { return m_wherePairs; }
107 | void setWherePairs(QJsonArray wherePairs);
108 |
109 | QString timeConstraint() const { return m_timeConstraint; }
110 | void setTimeConstraint(QString timeConstraint);
111 |
112 | int sampleInterval() const { return m_sampleInterval; }
113 | void setSampleInterval(int sampleInterval);
114 |
115 | signals:
116 | void valuesChanged(); // that means values property definition, not actual data!
117 | void valuesUpdated(); // actual data points
118 | void initializedChanged();
119 | void errorMessageChanged();
120 | void updateIntervalMsChanged();
121 | void serverChanged();
122 | void userChanged();
123 | void passwordChanged();
124 | void ignoreSslErrorsChanged();
125 | void databaseChanged();
126 | void measurementChanged();
127 | void fieldsChanged();
128 | void wherePairsChanged();
129 | void timeConstraintChanged();
130 | void sampleIntervalChanged();
131 |
132 | protected:
133 | void timerEvent(QTimerEvent *) Q_DECL_OVERRIDE { sampleAllValues(); }
134 |
135 | protected slots:
136 | void onAuthenticationRequired(QNetworkReply *reply, QAuthenticator *authenticator);
137 | void networkError(QNetworkReply::NetworkError e);
138 | void networkFinished();
139 |
140 | private:
141 | void init();
142 |
143 | private:
144 | QList m_values;
145 | QString m_errorMessage;
146 | QUrl m_server;
147 | QString m_user;
148 | QString m_password;
149 | QString m_database;
150 | QString m_measurement;
151 | QStringList m_fields;
152 | QJsonArray m_wherePairs;
153 | QString m_timeConstraint;
154 | QString m_queryString;
155 | bool m_initialized = false;
156 | bool m_ignoreSslErrors = false;
157 | int m_updateIntervalMs = -1;
158 | int m_timerId = 0;
159 | QUrl m_queryUrl;
160 | QDateTime m_lastUpdate;
161 | QDateTime m_lastSampleTime;
162 |
163 | QNetworkAccessManager m_nam;
164 | QNetworkRequest m_influxReq;
165 | QNetworkReply *m_netReply = nullptr;
166 | int m_sampleInterval = 0;
167 | };
168 |
169 | #endif // INFLUXDB_H
170 |
--------------------------------------------------------------------------------
/linegraph.cpp:
--------------------------------------------------------------------------------
1 | #include "linegraph.h"
2 | #include "linenode_p.h"
3 | #include
4 | #include
5 |
6 | LineGraph::LineGraph()
7 | {
8 | setFlag(ItemHasContents, true);
9 | setAntialiasing(true);
10 | }
11 |
12 | void LineGraph::registerMetaType()
13 | {
14 | qmlRegisterType("LineGraph", 1, 0, "LineGraph");
15 | }
16 |
17 | void LineGraph::appendSample(qreal value)
18 | {
19 | m_model->appendSampleMs(value);
20 | }
21 |
22 | void LineGraph::removeFirstSample()
23 | {
24 | m_model->removeFirstSample();
25 | }
26 |
27 | void LineGraph::setLineWidth(qreal lineWidth)
28 | {
29 | if (m_lineWidth == lineWidth)
30 | return;
31 |
32 | m_lineWidth = lineWidth;
33 | m_propertiesChanged = true;
34 | emit lineWidthChanged();
35 | update();
36 | }
37 |
38 | void LineGraph::setColor(QColor color)
39 | {
40 | if (m_color == color)
41 | return;
42 |
43 | m_color = color;
44 | m_propertiesChanged = true;
45 | emit colorChanged();
46 | update();
47 | }
48 |
49 | void LineGraph::setFillColorBelow(QColor fillColorBelow)
50 | {
51 | if (m_fillColorBelow == fillColorBelow)
52 | return;
53 |
54 | m_fillColorBelow = fillColorBelow;
55 | m_propertiesChanged = true;
56 | emit fillColorBelowChanged();
57 | update();
58 | }
59 |
60 | void LineGraph::setFillColorAbove(QColor fillColorAbove)
61 | {
62 | if (m_fillColorAbove == fillColorAbove)
63 | return;
64 |
65 | m_fillColorAbove = fillColorAbove;
66 | m_propertiesChanged = true;
67 | emit fillColorAboveChanged();
68 | update();
69 | }
70 |
71 | void LineGraph::setWarningMinColor(QColor warningMinColor)
72 | {
73 | if (m_warningMinColor == warningMinColor)
74 | return;
75 |
76 | m_warningMinColor = warningMinColor;
77 | m_propertiesChanged = true;
78 | emit warningMinColorChanged();
79 | update();
80 | }
81 |
82 | void LineGraph::setWarningMaxColor(QColor warningMaxColor)
83 | {
84 | if (m_warningMaxColor == warningMaxColor)
85 | return;
86 |
87 | m_warningMaxColor = warningMaxColor;
88 | m_propertiesChanged = true;
89 | emit warningMaxColorChanged();
90 | update();
91 | }
92 |
93 | void LineGraph::setMinValue(qreal minValue)
94 | {
95 | if (m_model)
96 | m_model->setMinValue(minValue);
97 |
98 | m_geometryChanged = true;
99 | m_propertiesChanged = true;
100 | emit minValueChanged();
101 | update();
102 | }
103 |
104 | void LineGraph::setMaxValue(qreal maxValue)
105 | {
106 | if (m_model)
107 | m_model->setMaxValue(maxValue);
108 |
109 | m_geometryChanged = true;
110 | m_propertiesChanged = true;
111 | emit maxValueChanged();
112 | update();
113 | }
114 |
115 | void LineGraph::setTimeSpan(qreal timeSpan)
116 | {
117 | if (m_timeSpan == timeSpan)
118 | return;
119 |
120 | m_timeSpan = timeSpan;
121 | m_geometryChanged = true;
122 | emit timeSpanChanged();
123 | if (m_model)
124 | m_model->setDownsampleInterval(timeSpan / width());
125 | update();
126 | }
127 |
128 | qreal LineGraph::xAtTime(qreal time)
129 | {
130 | return width() - (m_model->maxSampleTime() - time) / m_timeSpan * width();
131 | }
132 |
133 | QJSValue LineGraph::sampleNearestX(qreal x)
134 | {
135 | static QJSValue nullJS(QJSValue::NullValue);
136 | QJSEngine *engine = qmlEngine(this);
137 |
138 | qreal time = m_model->maxSampleTime() - m_timeSpan + x / width() * m_timeSpan;
139 | LineNode::LineVertex v = m_model->sampleNearest(time);
140 | if (qIsNaN(v.y))
141 | return nullJS;
142 |
143 | qreal vscale = height() / (m_model->maxValue() - m_model->minValue());
144 | QJSValue ret = engine->newObject();
145 | ret.setProperty(QLatin1String("time"), v.x); // TODO add m_timeOffset
146 | ret.setProperty(QLatin1String("value"), v.y);
147 | ret.setProperty(QLatin1String("x"), xAtTime(v.x));
148 | ret.setProperty(QLatin1String("y"), height() - vscale * (v.y - m_model->minValue()));
149 | return ret;
150 | }
151 |
152 | void LineGraph::setModel(LineGraphModel *model)
153 | {
154 | if (m_model == model)
155 | return;
156 |
157 | if (m_model) {
158 | disconnect(m_model, &LineGraphModel::samplesChanged, this, &LineGraph::updateVertices);
159 | disconnect(m_model, &LineGraphModel::minValueChanged, this, &LineGraph::minValueChanged);
160 | disconnect(m_model, &LineGraphModel::maxValueChanged, this, &LineGraph::maxValueChanged);
161 | }
162 | m_model = model;
163 | connect(model, &LineGraphModel::samplesChanged, this, &LineGraph::updateVertices);
164 | connect(model, &LineGraphModel::minValueChanged, this, &LineGraph::minValueChanged);
165 | connect(model, &LineGraphModel::maxValueChanged, this, &LineGraph::maxValueChanged);
166 | if (m_model)
167 | m_model->setDownsampleInterval(m_timeSpan / width());
168 | emit modelChanged();
169 | updateVertices();
170 | }
171 |
172 | void LineGraph::setWireframe(bool wireframe)
173 | {
174 | if (m_wireframe == wireframe)
175 | return;
176 |
177 | m_wireframe = wireframe;
178 | m_propertiesChanged = true;
179 | emit wireframeChanged();
180 | }
181 |
182 | void LineGraph::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
183 | {
184 | m_geometryChanged = true;
185 | if (m_model)
186 | m_model->setDownsampleInterval(m_timeSpan / width());
187 | update();
188 | QQuickItem::geometryChanged(newGeometry, oldGeometry);
189 | }
190 |
191 | void LineGraph::updateVertices()
192 | {
193 | m_samplesChanged = true;
194 | update();
195 | }
196 |
197 |
198 | class GraphNode : public QSGNode
199 | {
200 | public:
201 | LineNode *line;
202 | };
203 |
204 |
205 | QSGNode *LineGraph::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
206 | {
207 | if (!m_model)
208 | return oldNode;
209 |
210 | GraphNode *n = static_cast(oldNode);
211 |
212 | QRectF rect = boundingRect();
213 |
214 | if (rect.isEmpty()) {
215 | delete n;
216 | return 0;
217 | }
218 | if (!n) {
219 | n = new GraphNode();
220 | n->line = new LineNode();
221 | n->appendChildNode(n->line);
222 | }
223 | if (m_propertiesChanged) {
224 | float fillDirection = 0;
225 | QColor fillColor = m_color;
226 | if (m_fillColorAbove != Qt::transparent) {
227 | fillDirection = -1;
228 | fillColor = m_fillColorAbove;
229 | }
230 | if (m_fillColorBelow != Qt::transparent) {
231 | fillDirection = 1;
232 | fillColor = m_fillColorBelow;
233 | }
234 | n->line->setHeight(height());
235 | n->line->setLineWidth(m_lineWidth);
236 | n->line->setColor(fillColor);
237 | n->line->setWarningMinColor(m_warningMinColor);
238 | n->line->setWarningMaxColor(m_warningMaxColor);
239 | n->line->setWarningMinValue(m_model->normalMinValue());
240 | n->line->setWarningMaxValue(m_model->normalMaxValue());
241 | n->line->setFillDirection(fillDirection);
242 | n->line->setMinValue(m_model->minValue());
243 | n->line->setMaxValue(m_model->maxValue());
244 | n->line->setWireframe(m_wireframe);
245 | }
246 | n->line->setSpread(antialiasing() && !m_wireframe ? 1.0 : 0.0);
247 |
248 | if ((m_geometryChanged || m_samplesChanged) && !m_model->vertices()->isEmpty())
249 | n->line->updateGeometry(rect, m_model->vertices(), width() / m_timeSpan);
250 |
251 | m_geometryChanged = false;
252 | m_samplesChanged = false;
253 | m_propertiesChanged = false;
254 |
255 | emit samplesChanged();
256 |
257 | return n;
258 | }
259 |
--------------------------------------------------------------------------------
/linegraph.h:
--------------------------------------------------------------------------------
1 | #ifndef LINEGRAPH_H
2 | #define LINEGRAPH_H
3 |
4 | #include
5 | #include "linegraphmodel.h"
6 |
7 | class LineGraph : public QQuickItem
8 | {
9 | Q_OBJECT
10 | Q_PROPERTY(LineGraphModel * model READ model WRITE setModel NOTIFY modelChanged)
11 | Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged)
12 | Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
13 | Q_PROPERTY(QColor fillColorBelow READ fillColorBelow WRITE setFillColorBelow NOTIFY fillColorBelowChanged)
14 | Q_PROPERTY(QColor fillColorAbove READ fillColorAbove WRITE setFillColorAbove NOTIFY fillColorAboveChanged)
15 | Q_PROPERTY(QColor warningMinColor READ warningMinColor WRITE setWarningMinColor NOTIFY warningMinColorChanged)
16 | Q_PROPERTY(QColor warningMaxColor READ warningMaxColor WRITE setWarningMaxColor NOTIFY warningMaxColorChanged)
17 | Q_PROPERTY(qreal minValue READ minValue WRITE setMinValue NOTIFY minValueChanged) // convenience
18 | Q_PROPERTY(qreal maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged) // convenience
19 | Q_PROPERTY(qreal timeSpan READ timeSpan WRITE setTimeSpan NOTIFY timeSpanChanged) // in seconds
20 | Q_PROPERTY(bool wireframe READ wireframe WRITE setWireframe NOTIFY wireframeChanged)
21 |
22 | public:
23 | LineGraph();
24 |
25 | LineGraphModel * model() const { return m_model; }
26 | void setModel(LineGraphModel * model);
27 |
28 | qreal lineWidth() const { return m_lineWidth; }
29 | void setLineWidth(qreal lineWidth);
30 |
31 | QColor color() const { return m_color; }
32 | void setColor(QColor color);
33 |
34 | QColor fillColorBelow() const { return m_fillColorBelow; }
35 | void setFillColorBelow(QColor fillColorBelow);
36 |
37 | QColor fillColorAbove() const { return m_fillColorAbove; }
38 | void setFillColorAbove(QColor fillColorAbove);
39 |
40 | QColor warningMinColor() const { return m_warningMinColor; }
41 | void setWarningMinColor(QColor warningMinColor);
42 |
43 | QColor warningMaxColor() const { return m_warningMaxColor; }
44 | void setWarningMaxColor(QColor warningMaxColor);
45 |
46 | qreal minValue() const { return m_model ? m_model->minValue() : 0; }
47 | void setMinValue(qreal minValue);
48 |
49 | qreal maxValue() const { return m_model ? m_model->maxValue() : 0; }
50 | void setMaxValue(qreal maxValue);
51 |
52 | qreal timeSpan() const { return m_timeSpan; }
53 | void setTimeSpan(qreal timeSpan);
54 |
55 | bool wireframe() const { return m_wireframe; }
56 | void setWireframe(bool wireframe);
57 |
58 | Q_INVOKABLE QJSValue sampleNearestX(qreal x);
59 | Q_INVOKABLE qreal xAtTime(qreal time);
60 |
61 | static void registerMetaType();
62 |
63 | protected:
64 | QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *);
65 | void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry);
66 | void updateVertices();
67 |
68 | public slots:
69 | void appendSample(qreal value); // convenience
70 | void removeFirstSample(); // convenience
71 |
72 | signals:
73 | void modelChanged();
74 | void lineWidthChanged();
75 | void colorChanged();
76 | void fillColorBelowChanged();
77 | void fillColorAboveChanged();
78 | void warningMinColorChanged();
79 | void warningMaxColorChanged();
80 | void minValueChanged();
81 | void maxValueChanged();
82 | void timeSpanChanged();
83 | void samplesChanged(); // the model has samplesChanged too, but this occurs at a max of once per frame
84 | void wireframeChanged();
85 |
86 | protected:
87 | LineGraphModel *m_model = 0;
88 | bool m_samplesChanged = false;
89 | bool m_geometryChanged = false;
90 | bool m_propertiesChanged = true;
91 | bool m_wireframe = false;
92 | QColor m_color = Qt::cyan;
93 | QColor m_fillColorBelow = Qt::transparent;
94 | QColor m_fillColorAbove = Qt::transparent;
95 | QColor m_warningMinColor = Qt::yellow;
96 | QColor m_warningMaxColor = Qt::red;
97 | qreal m_lineWidth = 1.3;
98 | qreal m_timeSpan = 60;
99 | };
100 |
101 | #endif // LINEGRAPH_H
102 |
--------------------------------------------------------------------------------
/linegraphmodel.cpp:
--------------------------------------------------------------------------------
1 | #include "linegraphmodel.h"
2 | #include
3 | #include
4 |
5 | Q_LOGGING_CATEGORY(lcLineGraphModel, "org.ecloud.charts.model")
6 |
7 | const qint64 LineGraphModel::m_timeOffset(LineGraphModel::timeNowMs());
8 |
9 | /*!
10 | \qmltype LineGraphModel
11 | \instantiates LineGraphModel
12 | \brief A model to store vertices for LineGraph to render
13 | \inherits QObject
14 |
15 | A LineGraphModel stores a time-value series of samples in a form
16 | which is useful both for in-memory storage and later querying,
17 | and for direct rendering: that is, a vector of vertex structs
18 | ready to be sent to the line graph vertex shader.
19 |
20 | It's required to append samples in time order, with the oldest first.
21 | */
22 | LineGraphModel::LineGraphModel(QObject *parent) : QObject(parent)
23 | {
24 | }
25 |
26 | double LineGraphModel::triangleArea(
27 | const LineGraphModel::TimeValue &a, const LineGraphModel::TimeValue &b, const LineGraphModel::TimeValue &c)
28 | {
29 | // www.mathopenref.com/coordtrianglearea.html
30 | return qAbs( (a.time * (b.value - c.value) +
31 | b.time * (c.value - a.value) +
32 | c.time * (a.value - b.value)) / 2);
33 | }
34 |
35 | LineGraphModel::TimeValue LineGraphModel::largestTriangle(const TimeValue &previous, const QVector ¤t, const TimeValue &next)
36 | {
37 | if (current.count() == 1)
38 | return current.first();
39 |
40 | int chosen = 0;
41 | double chosenArea = triangleArea(previous, current.first(), next);
42 | for (int i = 1; i < current.size(); ++i) {
43 | if (triangleArea(previous, current[i], next) > chosenArea)
44 | chosen = i;
45 | }
46 |
47 | return current[chosen];
48 | }
49 |
50 | /*!
51 | Returns the average time and value of the given series.
52 | */
53 | LineGraphModel::TimeValue LineGraphModel::average(const QVector &tvv)
54 | {
55 | TimeValue ret = {0, 0};
56 | for (const TimeValue &tv : tvv) {
57 | ret.time += tv.time;
58 | ret.value += tv.value;
59 | }
60 | ret.time /= tvv.count();
61 | ret.value /= tvv.count();
62 | return ret;
63 | }
64 |
65 | LineGraphModel::TimeValue LineGraphModel::endVertex(int fromLast)
66 | {
67 | int i = (m_vertices.count() / LineNode::verticesPerSample - fromLast) * LineNode::verticesPerSample;
68 | TimeValue ret;
69 | ret.time = m_vertices[i].x /* + m_timeOffset */;
70 | ret.value = m_vertices[i].y;
71 | return ret;
72 | }
73 |
74 | /*!
75 | Append the given sample \a value taken at the given \a time,
76 | in seconds since startup.
77 | */
78 | void LineGraphModel::appendSample(qreal value, qreal time)
79 | {
80 | TimeValue tv { time, value };
81 | // On currentBucket overflow:
82 | // finalize the vertex from m_previousBucket
83 | // current becomes previous
84 | bool modify = !m_currentBucket.isEmpty();
85 | if (m_downsampleMethod == LargestTriangleThreeBuckets) {
86 | if (!m_currentBucket.isEmpty() && time > m_currentBucket.first().time + m_downsampleBucketInterval) {
87 | if (m_vertices.count() > LineNode::verticesPerSample * 2) {
88 | TimeValue previousVtx = largestTriangle(endVertex(2), m_previousBucket, tv);
89 | modifyEndVertices(previousVtx.time, previousVtx.value, 1);
90 | }
91 | m_previousBucket = m_currentBucket;
92 | m_currentBucket.clear();
93 | TimeValue previousVtx = average(m_previousBucket);
94 | modifyEndVertices(previousVtx.time, previousVtx.value);
95 | modify = false;
96 | }
97 | m_currentBucket.append(tv);
98 | // qCDebug(lcLineGraphModel) << "buckets" << m_previousBucket.count() << m_currentBucket.count();
99 | } else {
100 | modify = false;
101 | }
102 | if (modify)
103 | modifyEndVertices(time, value);
104 | else
105 | appendVertices(time, value);
106 | }
107 |
108 | /*!
109 | Append the given sample \a value taken at the given \a timestamp,
110 | in milliseconds since the epoch.
111 | */
112 | void LineGraphModel::appendSampleMs(qreal value, qint64 timestamp)
113 | {
114 | appendSample(value, (timestamp - m_timeOffset) / 1000.0);
115 | }
116 |
117 | void LineGraphModel::removeFirstSample()
118 | {
119 | qWarning("poorly tested, maybe wrong"); // removing from the bucket(s) depends on m_downsampleMethod if we even need to do that
120 | m_vertices.remove(0, LineNode::verticesPerSample);
121 | }
122 |
123 | /*!
124 | Get the LineVertex instance which is nearest the given \a time,
125 | which is in seconds, beginning with zero from the first sample recorded.
126 | */
127 | LineNode::LineVertex LineGraphModel::sampleNearest(qreal time)
128 | {
129 | LineNode::LineVertex proto;
130 | proto.x = time;
131 | auto found = std::upper_bound(m_vertices.begin(), m_vertices.end(), proto, [](const auto &lhs, const auto &rhs) {
132 | return lhs.x < rhs.x;
133 | });
134 | if (found == m_vertices.end()) {
135 | proto.x = -1;
136 | proto.y = qQNaN();
137 | return proto;
138 | }
139 | //qDebug() << Q_FUNC_INFO << time << "found" << found->t << found->x << found->y;
140 | return *found;
141 | }
142 |
143 | /**
144 | Returns a "nice" number approximately equal to \a range
145 | Rounds the number if \a round = true; takes the ceiling if \a round = false.
146 |
147 | From http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks
148 | */
149 | qreal LineGraphModel::niceNum(qreal range, bool round)
150 | {
151 | qreal exponent; /** exponent of range */
152 | qreal fraction; /** fractional part of range */
153 | qreal niceFraction; /** nice, rounded fraction */
154 |
155 | exponent = floor(log10(range));
156 | fraction = range / pow(10.f, exponent);
157 |
158 | if (round) {
159 | if (fraction < 1.5)
160 | niceFraction = 1;
161 | else if (fraction < 3)
162 | niceFraction = 2;
163 | else if (fraction < 7)
164 | niceFraction = 5;
165 | else
166 | niceFraction = 10;
167 | } else {
168 | if (fraction <= 1)
169 | niceFraction = 1;
170 | else if (fraction <= 2)
171 | niceFraction = 2;
172 | else if (fraction <= 5)
173 | niceFraction = 5;
174 | else
175 | niceFraction = 10;
176 | }
177 |
178 | return niceFraction * pow(10, exponent);
179 | }
180 |
181 | void LineGraphModel::autoScale()
182 | {
183 | qreal range = niceNum(m_maxSampleValue - m_minSampleValue, false);
184 | qreal tickSpacing = niceNum(range / (m_maxTicks - 1), true);
185 | setMinValue(floor(m_minSampleValue / tickSpacing) * tickSpacing);
186 | setMaxValue(ceil(m_maxSampleValue / tickSpacing) * tickSpacing);
187 | qCDebug(lcLineGraphModel) << Q_FUNC_INFO << m_minSampleValue << m_maxSampleValue
188 | << "tickSpacing" << tickSpacing << "autoscaled:" << minValue() << maxValue();
189 | }
190 |
191 | void LineGraphModel::setNormalMinValue(qreal normalMinValue)
192 | {
193 | if (m_normalMinValue == normalMinValue)
194 | return;
195 |
196 | m_normalMinValue = normalMinValue;
197 | emit normalMinValueChanged();
198 | }
199 |
200 | void LineGraphModel::setNormalMaxValue(qreal normalMaxValue)
201 | {
202 | if (m_normalMaxValue == normalMaxValue)
203 | return;
204 |
205 | m_normalMaxValue = normalMaxValue;
206 | emit normalMaxValueChanged();
207 | }
208 |
209 | void LineGraphModel::setLabel(QString label)
210 | {
211 | if (m_label == label)
212 | return;
213 |
214 | m_label = label;
215 | emit labelChanged();
216 | }
217 |
218 | void LineGraphModel::setUnit(QString unit)
219 | {
220 | if (m_unit == unit)
221 | return;
222 |
223 | m_unit = unit;
224 | emit unitChanged();
225 | }
226 |
227 | void LineGraphModel::setTimeSpan(int timeSpan)
228 | {
229 | if (m_timeSpan == timeSpan)
230 | return;
231 |
232 | m_timeSpan = timeSpan;
233 | emit timeSpanChanged();
234 | }
235 |
236 | void LineGraphModel::setDownsampleInterval(qreal downsampleInterval)
237 | {
238 | if (m_downsampleBucketInterval == downsampleInterval)
239 | return;
240 |
241 | m_downsampleBucketInterval = downsampleInterval;
242 | emit downsampleIntervalChanged();
243 | }
244 |
245 | void LineGraphModel::setDownsampleMethod(LineGraphModel::DownsampleMethod downsampleMethod)
246 | {
247 | if (m_downsampleMethod == downsampleMethod)
248 | return;
249 |
250 | m_downsampleMethod = downsampleMethod;
251 | emit downsampleMethodChanged();
252 | }
253 |
254 | qreal LineGraphModel::currentValue() const
255 | {
256 | if (m_vertices.length())
257 | return m_vertices.last().y;
258 | else
259 | return 0;
260 | }
261 |
262 | void LineGraphModel::setMinValue(qreal minValue)
263 | {
264 | if (m_minValue == minValue)
265 | return;
266 |
267 | m_minValue = minValue;
268 | emit minValueChanged();
269 | }
270 |
271 | void LineGraphModel::setMaxValue(qreal maxValue)
272 | {
273 | if (m_maxValue == maxValue)
274 | return;
275 |
276 | m_maxValue = maxValue;
277 | emit maxValueChanged();
278 | }
279 |
280 | void LineGraphModel::setClipValues(bool clipValues)
281 | {
282 | if (m_clipValues == clipValues)
283 | return;
284 |
285 | m_clipValues = clipValues;
286 | emit clipValuesChanged();
287 | }
288 |
289 | const QVector *LineGraphModel::vertices()
290 | {
291 | return &m_vertices;
292 | }
293 |
294 | /*!
295 | Append vertices representing the given sample \a value
296 | taken at the given \a time, in seconds since startup.
297 | */
298 | void LineGraphModel::appendVertices(qreal time, qreal value)
299 | {
300 | // qDebug() << m_label << time << value << "already have samples:" << m_vertices.size();
301 | if (m_clipValues) {
302 | if (value < m_minValue)
303 | value = m_minValue;
304 | if (value > m_maxValue)
305 | value = m_maxValue;
306 | }
307 | value *= m_multiplier;
308 | finagle(time, value);
309 | if (value > m_maxSampleValue) {
310 | m_maxSampleValue = value;
311 | emit maxSampleValueChanged();
312 | }
313 | if (value < m_minSampleValue) {
314 | m_minSampleValue = value;
315 | emit minSampleValueChanged();
316 | }
317 | int i = m_vertices.size() - LineNode::verticesPerSample;
318 | if (i >= 0) {
319 | m_vertices[i].nextX = time;
320 | m_vertices[i++].nextY = value;
321 | m_vertices[i].nextX = time;
322 | m_vertices[i++].nextY = value;
323 | m_vertices[i].nextX = time;
324 | m_vertices[i++].nextY = value;
325 | m_vertices[i].nextX = time;
326 | m_vertices[i++].nextY = value;
327 | }
328 | i = m_vertices.size();
329 | m_vertices.resize(i + LineNode::verticesPerSample);
330 | float tp = i > 0 ? m_vertices[i - 1].x : time;
331 | float samplePrev = i > 0 ? m_vertices[i - 1].y : value;
332 | m_vertices[i++].set(0, -1, time, value, tp, samplePrev, time + 0.01, value);
333 | m_vertices[i++].set(1, 1, time, value, tp, samplePrev, time + 0.01, value);
334 | m_vertices[i++].set(2, -1, time, value, tp, samplePrev, time + 0.01, value);
335 | m_vertices[i++].set(3, 1, time, value, tp, samplePrev, time + 0.01, value);
336 | Q_ASSERT(i == m_vertices.size());
337 |
338 | qint64 earliestTimeAllowed = m_vertices.last().x - m_timeSpan;
339 | while (m_vertices.size() > 2 && m_vertices[1].x < earliestTimeAllowed)
340 | m_vertices.remove(0, 4);
341 | emit samplesChanged();
342 | }
343 |
344 | void LineGraphModel::modifyEndVertices(qreal time, qreal value, int fromLast)
345 | {
346 | // qDebug() << m_label << time << value << "already have samples:" << m_vertices.size();
347 | value *= m_multiplier;
348 | finagle(time, value);
349 | int i = (m_vertices.length() / LineNode::verticesPerSample - fromLast - 1) * LineNode::verticesPerSample;
350 | Q_ASSERT(i >= 0);
351 | qreal tp = time - 0.01;
352 | qreal samplePrev = value;
353 | if (i >= LineNode::verticesPerSample) {
354 | int j = i - LineNode::verticesPerSample;
355 | tp = m_vertices[j].x;
356 | samplePrev = m_vertices[j].y;
357 | m_vertices[j].nextX = time;
358 | m_vertices[j++].nextY = value;
359 | m_vertices[j].nextX = time;
360 | m_vertices[j++].nextY = value;
361 | m_vertices[j].nextX = time;
362 | m_vertices[j++].nextY = value;
363 | m_vertices[j].nextX = time;
364 | m_vertices[j++].nextY = value;
365 | }
366 | qreal tn = time + 0.001;
367 | qreal sampleNext = value;
368 | if (fromLast > 0) {
369 | int j = i + LineNode::verticesPerSample;
370 | tn = m_vertices[j].x;
371 | sampleNext = m_vertices[j].y;
372 | m_vertices[j].prevX = time;
373 | m_vertices[j++].prevY = value;
374 | m_vertices[j].prevX = time;
375 | m_vertices[j++].prevY = value;
376 | m_vertices[j].prevX = time;
377 | m_vertices[j++].prevY = value;
378 | m_vertices[j].prevX = time;
379 | m_vertices[j++].prevY = value;
380 | }
381 | m_vertices[i++].set(0, -1, time, value, tp, samplePrev, tn, sampleNext);
382 | m_vertices[i++].set(1, 1, time, value, tp, samplePrev, tn, sampleNext);
383 | m_vertices[i++].set(2, -1, time, value, tp, samplePrev, tn, sampleNext);
384 | m_vertices[i++].set(3, 1, time, value, tp, samplePrev, tn, sampleNext);
385 | emit samplesChanged();
386 | }
387 |
--------------------------------------------------------------------------------
/linegraphmodel.h:
--------------------------------------------------------------------------------
1 | #ifndef LINEGRAPHMODEL_H
2 | #define LINEGRAPHMODEL_H
3 |
4 | #include
5 | #include
6 | #include "linenode_p.h"
7 |
8 | class LineGraphModel : public QObject
9 | {
10 | Q_OBJECT
11 | Q_PROPERTY(int timeSpan READ timeSpan WRITE setTimeSpan NOTIFY timeSpanChanged)
12 | Q_PROPERTY(qreal downsampleInterval READ downsampleInterval WRITE setDownsampleInterval NOTIFY downsampleIntervalChanged)
13 | Q_PROPERTY(DownsampleMethod downsampleMethod READ downsampleMethod WRITE setDownsampleMethod NOTIFY downsampleMethodChanged)
14 | Q_PROPERTY(qreal currentValue READ currentValue NOTIFY samplesChanged)
15 | Q_PROPERTY(qreal minValue READ minValue WRITE setMinValue NOTIFY minValueChanged)
16 | Q_PROPERTY(qreal maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged)
17 | Q_PROPERTY(bool clipValues READ clipValues WRITE setClipValues NOTIFY clipValuesChanged)
18 | Q_PROPERTY(qreal normalMinValue READ normalMinValue WRITE setNormalMinValue NOTIFY normalMinValueChanged)
19 | Q_PROPERTY(qreal normalMaxValue READ normalMaxValue WRITE setNormalMaxValue NOTIFY normalMaxValueChanged)
20 | Q_PROPERTY(qreal minSampleValue READ minSampleValue NOTIFY minSampleValueChanged)
21 | Q_PROPERTY(qreal maxSampleValue READ maxSampleValue NOTIFY maxSampleValueChanged)
22 | Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged)
23 | Q_PROPERTY(QString unit READ unit WRITE setUnit NOTIFY unitChanged)
24 |
25 | public:
26 | enum DownsampleMethod { NoDownsample = 0, LargestTriangleThreeBuckets };
27 |
28 | explicit LineGraphModel(QObject *parent = 0);
29 |
30 | int timeSpan() const { return m_timeSpan; }
31 | void setTimeSpan(int timeSpan);
32 |
33 | qreal downsampleInterval() const { return m_downsampleBucketInterval; }
34 | void setDownsampleInterval(qreal downsampleInterval);
35 |
36 | DownsampleMethod downsampleMethod() const { return m_downsampleMethod; }
37 | void setDownsampleMethod(DownsampleMethod downsampleMethod);
38 |
39 | qreal currentValue() const;
40 |
41 | qreal minValue() const { return m_minValue; }
42 | void setMinValue(qreal minValue);
43 |
44 | qreal maxValue() const { return m_maxValue; }
45 | void setMaxValue(qreal maxValue);
46 |
47 | bool clipValues() const { return m_clipValues; }
48 | void setClipValues(bool clipValues);
49 |
50 | qreal minSampleValue() const { return m_minSampleValue; }
51 | qreal maxSampleValue() const { return m_maxSampleValue; }
52 | qreal minSampleTime() const { return m_vertices.isEmpty() ? -1 : m_vertices.first().x; } // in seconds
53 | qreal maxSampleTime() const { return m_vertices.isEmpty() ? -1 : m_vertices.last().x; }
54 |
55 | QString label() const { return m_label; }
56 | void setLabel(QString label);
57 |
58 | QString unit() { return m_unit; }
59 | void setUnit(QString unit);
60 |
61 | void setMultiplier(qreal m) { m_multiplier = m; }
62 |
63 | Q_INVOKABLE LineNode::LineVertex sampleNearest(qreal time);
64 | Q_INVOKABLE void autoScale();
65 |
66 | qreal normalMinValue() const { return m_normalMinValue; }
67 | void setNormalMinValue(qreal normalMinValue);
68 |
69 | qreal normalMaxValue() const { return m_normalMaxValue; }
70 | void setNormalMaxValue(qreal normalMaxValue);
71 |
72 | static qreal niceNum(qreal range, bool round);
73 |
74 | signals:
75 | void timeSpanChanged();
76 | void downsampleIntervalChanged();
77 | void downsampleMethodChanged();
78 | void samplesChanged();
79 | void minSampleValueChanged();
80 | void maxSampleValueChanged();
81 | void minValueChanged();
82 | void maxValueChanged();
83 | void normalMinValueChanged();
84 | void normalMaxValueChanged();
85 | void labelChanged();
86 | void unitChanged();
87 | void clipValuesChanged();
88 |
89 | public slots:
90 | void appendSample(qreal value, qreal time); // time in seconds
91 | void appendSampleMs(qreal value, qint64 timestamp = timeNowMs());
92 | void appendSampleMs(qreal value, QDateTime timestamp) { appendSampleMs(value, timestamp.toMSecsSinceEpoch()); }
93 | void removeFirstSample();
94 |
95 | protected:
96 | struct TimeValue {
97 | qreal time;
98 | qreal value;
99 | };
100 |
101 | static qint64 timeNowMs() { return QDateTime::currentMSecsSinceEpoch(); }
102 |
103 | protected:
104 | virtual const QVector *vertices();
105 | void appendVertices(qreal time, qreal value);
106 | void modifyEndVertices(qreal time, qreal value, int fromLast = 0);
107 | TimeValue endVertex(int fromLast = 0);
108 | static double triangleArea(const LineGraphModel::TimeValue &a, const LineGraphModel::TimeValue &b, const LineGraphModel::TimeValue &c);
109 | static LineGraphModel::TimeValue largestTriangle(const TimeValue &previous, const QVector ¤t, const TimeValue &next);
110 | static LineGraphModel::TimeValue average(const QVector &tvv);
111 | virtual void finagle(qreal &time, qreal &value) { Q_UNUSED(time); Q_UNUSED(value); }
112 |
113 | protected:
114 | QVector m_currentBucket;
115 | QVector m_previousBucket;
116 | QVector m_vertices;
117 | int m_timeSpan = 1000; // in seconds
118 | DownsampleMethod m_downsampleMethod = LargestTriangleThreeBuckets;
119 | bool m_clipValues = true;
120 | int m_maxTicks = 10; // TODO adjust based on view size
121 | qreal m_downsampleBucketInterval = 0; // time in seconds: duration of one bucket
122 | qreal m_minValue = 0;
123 | qreal m_maxValue = 1;
124 | qreal m_normalMinValue = 0;
125 | qreal m_normalMaxValue = 1;
126 | qreal m_minSampleValue = qInf(); // seen for all time, even if it's no longer in m_vertices
127 | qreal m_maxSampleValue = -qInf();
128 | qreal m_multiplier = 1;
129 | QString m_label;
130 | QString m_unit;
131 |
132 | static const qint64 m_timeOffset;
133 |
134 | friend class LineGraph;
135 | };
136 |
137 | #endif // LINEGRAPHMODEL_H
138 |
--------------------------------------------------------------------------------
/linenode.cpp:
--------------------------------------------------------------------------------
1 | #include "linenode_p.h"
2 |
3 | #include
4 |
5 | #include
6 |
7 | class LineShader : public QSGSimpleMaterialShader
8 | {
9 | QSG_DECLARE_SIMPLE_SHADER(LineShader, LineNode::LineMaterial)
10 |
11 | public:
12 | LineShader() {
13 | setShaderSourceFile(QOpenGLShader::Vertex, ":/shaders/LineNode.vsh");
14 | setShaderSourceFile(QOpenGLShader::Fragment, ":/shaders/LineNode.fsh");
15 | }
16 |
17 | QList attributes() const { return QList() << "pos" << "prevNext"; }
18 |
19 | void updateState(const LineNode::LineMaterial *m, const LineNode::LineMaterial *) {
20 | program()->setUniformValue(id_height, m->height);
21 | program()->setUniformValue(id_lineWidth, GLfloat(m->aa ? m->lineWidth * 1.7 : m->lineWidth));
22 | program()->setUniformValue(id_warningBelowMinimum, m->warningMinValue);
23 | program()->setUniformValue(id_warningAboveMaximum, m->warningMaxValue);
24 | program()->setUniformValue(id_fillDirection, m->fillDirection);
25 | program()->setUniformValue(id_normalColor, m->color);
26 | program()->setUniformValue(id_warningMinColor, m->warningMinColor);
27 | program()->setUniformValue(id_warningMaxColor, m->warningMaxColor);
28 | program()->setUniformValue(id_dataScalingTransform, m->dataTransform.toGenericMatrix<2, 2>());
29 | program()->setUniformValue(id_dataOffset, m->dataTransform.column(3).toVector2D());
30 | program()->setUniformValue(id_aa, m->fillDirection == 0 ? m->aa : 0);
31 | //qDebug() << "colors" << m->color << m->warningMinColor << m->warningMaxColor;
32 | }
33 |
34 | void resolveUniforms() {
35 | id_height = program()->uniformLocation("height");
36 | id_lineWidth = program()->uniformLocation("lineWidth");
37 | id_warningBelowMinimum = program()->uniformLocation("warningBelowMinimum");
38 | id_warningAboveMaximum = program()->uniformLocation("warningAboveMaximum");
39 | id_fillDirection = program()->uniformLocation("fillDirection");
40 | id_normalColor = program()->uniformLocation("normalColor");
41 | id_warningMinColor = program()->uniformLocation("warningMinColor");
42 | id_warningMaxColor = program()->uniformLocation("warningMaxColor");
43 | id_dataScalingTransform = program()->uniformLocation("dataScalingTransform");
44 | id_dataOffset = program()->uniformLocation("dataOffset");
45 | id_aa = program()->uniformLocation("aa");
46 | }
47 |
48 | private:
49 | int id_height;
50 | int id_lineWidth;
51 | int id_warningBelowMinimum;
52 | int id_warningAboveMaximum;
53 | int id_fillDirection;
54 | int id_normalColor;
55 | int id_warningMinColor;
56 | int id_warningMaxColor;
57 | int id_dataScalingTransform;
58 | int id_dataOffset;
59 | int id_aa;
60 | };
61 |
62 | static const QSGGeometry::AttributeSet &attributes()
63 | {
64 | static QSGGeometry::Attribute attr[] = {
65 | QSGGeometry::Attribute::create(0, 4, GL_FLOAT),
66 | QSGGeometry::Attribute::create(1, 4, GL_FLOAT)
67 | };
68 | static QSGGeometry::AttributeSet set = { 2, 8 * sizeof(float), attr };
69 | return set;
70 | }
71 |
72 | LineNode::LineNode()
73 | : m_geometry(attributes(), 0)
74 | {
75 | setGeometry(&m_geometry);
76 |
77 | m_material = LineShader::createMaterial();
78 | m_material->setFlag(QSGMaterial::Blending);
79 | setMaterial(m_material);
80 | setFlag(OwnsMaterial);
81 | }
82 |
83 | void LineNode::updateGeometry(const QRectF &bounds, const QVector *v, qreal timeScale)
84 | {
85 | float vscale = bounds.height() / (m_maxValue - m_minValue);
86 | // TODO only adjust the x offset each frame
87 | QMatrix4x4 &matrix = m_material->state()->dataTransform;
88 | matrix.setToIdentity();
89 | matrix.scale(timeScale, -vscale);
90 | matrix.translate(bounds.width() / timeScale - v->last().x, -m_maxValue);
91 | //qDebug() << v->size() << timeScale << vscale << matrix << v->first().x << v->first().y << v->last().x << v->last().y;
92 |
93 | m_geometry.setDrawingMode(m_wireframe ? GL_LINE_STRIP : GL_TRIANGLE_STRIP);
94 | if (m_geometry.vertexCount() != v->size())
95 | m_geometry.allocate(v->size());
96 | // TODO limit on left side to stay in bounds
97 | memcpy(m_geometry.vertexData(), v->constData(), sizeof(LineVertex) * v->size());
98 | markDirty(QSGNode::DirtyGeometry);
99 | }
100 |
101 | void LineNode::setHeight(float height)
102 | {
103 | m_material->state()->height = height;
104 | markDirty(QSGNode::DirtyMaterial);
105 | }
106 |
107 | void LineNode::setLineWidth(float width)
108 | {
109 | m_material->state()->lineWidth = width;
110 | markDirty(QSGNode::DirtyMaterial);
111 | }
112 |
113 | void LineNode::setColor(QColor color)
114 | {
115 | m_material->state()->color = color;
116 | markDirty(QSGNode::DirtyMaterial);
117 | }
118 |
119 | void LineNode::setWarningMinColor(QColor color)
120 | {
121 | m_material->state()->warningMinColor = color;
122 | markDirty(QSGNode::DirtyMaterial);
123 | }
124 |
125 | void LineNode::setWarningMaxColor(QColor color)
126 | {
127 | m_material->state()->warningMaxColor = color;
128 | markDirty(QSGNode::DirtyMaterial);
129 | }
130 |
131 | void LineNode::setMinValue(qreal v)
132 | {
133 | m_minValue = v;
134 | markDirty(QSGNode::DirtyMaterial);
135 | }
136 |
137 | void LineNode::setMaxValue(qreal v)
138 | {
139 | m_maxValue = v;
140 | markDirty(QSGNode::DirtyMaterial);
141 | }
142 |
143 | void LineNode::setWarningMinValue(qreal v)
144 | {
145 | m_material->state()->warningMinValue = v;
146 | markDirty(QSGNode::DirtyMaterial);
147 | }
148 |
149 | void LineNode::setWarningMaxValue(qreal v)
150 | {
151 | m_material->state()->warningMaxValue = v;
152 | markDirty(QSGNode::DirtyMaterial);
153 | }
154 |
155 | void LineNode::setFillDirection(qreal v)
156 | {
157 | m_material->state()->fillDirection = v;
158 | markDirty(QSGNode::DirtyMaterial);
159 | }
160 |
161 | void LineNode::setSpread(qreal v)
162 | {
163 | m_material->state()->aa = v;
164 | markDirty(QSGNode::DirtyMaterial);
165 | }
166 |
167 | void LineNode::setWireframe(bool v)
168 | {
169 | m_wireframe = v;
170 | }
171 |
--------------------------------------------------------------------------------
/linenode_p.h:
--------------------------------------------------------------------------------
1 | #ifndef LINENODE_H
2 | #define LINENODE_H
3 |
4 | #include
5 | #include
6 | #include
7 |
8 | class LineNode : public QSGGeometryNode
9 | {
10 | public:
11 | LineNode();
12 |
13 | struct LineVertex {
14 | float x; // time (assuming your x axis is that); first sample has x=0
15 | float y; // original sample value
16 | float i; // incrementing index
17 | float t; // -1 or 1 to inset or outset by half-line-width and achieve antialiasing
18 | float prevX; // previous/next values are for the shader to calculate slopes, miter angles etc.
19 | float prevY;
20 | float nextX;
21 | float nextY;
22 | inline void set(int ii, float tt, float xx, float yy, float px, float py, float nx, float ny) {
23 | x = xx; y = yy; i = ii; t = tt;
24 | //qDebug() << "x" << xx << "y" << yy << "i" << ii << "t" << tt;
25 | prevX = px; prevY = py; nextX = nx; nextY = ny;
26 | }
27 | };
28 |
29 | static const int verticesPerSample = 4;
30 |
31 | void updateGeometry(const QRectF &bounds, const QVector *v, qreal timeScale);
32 | void setHeight(float height);
33 | void setLineWidth(float width);
34 | void setColor(QColor color);
35 | void setWarningMinColor(QColor color);
36 | void setWarningMaxColor(QColor color);
37 | void setMinValue(qreal v);
38 | void setMaxValue(qreal v);
39 | void setWarningMinValue(qreal v);
40 | void setWarningMaxValue(qreal v);
41 | void setFillDirection(qreal v);
42 | void setSpread(qreal v);
43 | void setWireframe(bool v);
44 |
45 | struct LineMaterial
46 | {
47 | QColor color;
48 | QColor warningMinColor;
49 | QColor warningMaxColor;
50 | float height;
51 | float lineWidth;
52 | float warningMinValue;
53 | float warningMaxValue;
54 | float fillDirection;
55 | float aa;
56 | QMatrix4x4 dataTransform;
57 | };
58 |
59 | private:
60 | QSGGeometry m_geometry;
61 | QSGSimpleMaterial *m_material;
62 | qreal m_minValue = 0;
63 | qreal m_maxValue = 1;
64 | bool m_wireframe;
65 |
66 | friend class LineShader;
67 | friend class LineGraph;
68 | };
69 |
70 | #endif // LINENODE_H
71 |
--------------------------------------------------------------------------------
/lmsensors.cpp:
--------------------------------------------------------------------------------
1 | #include "lmsensors.h"
2 | #include
3 |
4 | LmSensors::LmSensors(QObject *parent) : QObject(parent)
5 | {
6 | m_initialized = init();
7 | // throw away first sample - tends to be wrong
8 | for (Sensor *item : m_sensors)
9 | item->sample();
10 | m_timerId = startTimer(m_updateIntervalMs);
11 | }
12 |
13 | void appendItems(QQmlListProperty *property, Sensor *item)
14 | {
15 | Q_UNUSED(property);
16 | Q_UNUSED(item);
17 | // Do nothing. can't add to a directory using this method
18 | }
19 |
20 | int itemSize(QQmlListProperty *property) { return static_cast *>(property->data)->size(); }
21 |
22 | Sensor *itemAt(QQmlListProperty *property, int index)
23 | {
24 | return static_cast *>(property->data)->at(index);
25 | }
26 |
27 | void clearitemPtr(QQmlListProperty *property) { return static_cast *>(property->data)->clear(); }
28 |
29 | QQmlListProperty LmSensors::sensors()
30 | {
31 | return QQmlListProperty(this, &m_sensors, &appendItems, &itemSize, &itemAt, &clearitemPtr);
32 | }
33 |
34 | bool LmSensors::init()
35 | {
36 | #define BUF_SIZE 200
37 | static char buf[BUF_SIZE];
38 |
39 | int chip_nr, a;
40 | const sensors_chip_name *chip;
41 | const sensors_subfeature *sub;
42 | const sensors_feature *feature;
43 | const char *adap = NULL;
44 |
45 | // add CPU load
46 |
47 | Sensor *new_item = new Sensor(Sensor::Cpu);
48 | new_item->m_index = m_sensors.count();
49 | new_item->setLabel(tr("CPU Load"));
50 | new_item->m_adapter = "proc-stat";
51 | new_item->setMinValue(0);
52 | new_item->setMaxValue(100);
53 | new_item->m_unit = "%";
54 | m_sensors.append(new_item);
55 |
56 | // add memory metrics
57 |
58 | qreal memoryTotal = Sensor(Sensor::MemoryTotal).sample();
59 |
60 | new_item = new Sensor(Sensor::MemoryFree);
61 | new_item->m_index = m_sensors.count();
62 | new_item->setLabel(tr("Memory Free"));
63 | new_item->m_adapter = "proc-stat";
64 | new_item->setMinValue(0);
65 | new_item->setMaxValue(memoryTotal);
66 | new_item->setNormalMinValue(memoryTotal * 0.02);
67 | new_item->setNormalMaxValue(memoryTotal);
68 | new_item->m_unit = "KB";
69 | m_sensors.append(new_item);
70 |
71 | new_item = new Sensor(Sensor::MemoryUsed);
72 | new_item->m_index = m_sensors.count();
73 | new_item->setLabel(tr("Memory Used"));
74 | new_item->m_adapter = "proc-stat";
75 | new_item->setMinValue(0);
76 | new_item->setMaxValue(memoryTotal);
77 | new_item->setNormalMinValue(0);
78 | new_item->setNormalMaxValue(memoryTotal * 0.98);
79 | new_item->m_unit = "KB";
80 | m_sensors.append(new_item);
81 |
82 | new_item = new Sensor(Sensor::MemoryCache);
83 | new_item->m_index = m_sensors.count();
84 | new_item->setLabel(tr("Memory Cached"));
85 | new_item->m_adapter = "proc-stat";
86 | new_item->setMinValue(0);
87 | new_item->setMaxValue(memoryTotal);
88 | new_item->setNormalMinValue(0);
89 | new_item->setNormalMaxValue(memoryTotal);
90 | new_item->m_unit = "KB";
91 | m_sensors.append(new_item);
92 |
93 | qreal swapTotal = Sensor(Sensor::SwapTotal).sample();
94 |
95 | new_item = new Sensor(Sensor::SwapFree);
96 | new_item->m_index = m_sensors.count();
97 | new_item->setLabel(tr("Swap Free"));
98 | new_item->m_adapter = "proc-stat";
99 | new_item->setMinValue(0);
100 | new_item->setMaxValue(swapTotal);
101 | new_item->setNormalMinValue(swapTotal * 0.02);
102 | new_item->setNormalMaxValue(swapTotal);
103 | new_item->m_unit = "KB";
104 | m_sensors.append(new_item);
105 |
106 | new_item = new Sensor(Sensor::SwapUsed);
107 | new_item->m_index = m_sensors.count();
108 | new_item->setLabel(tr("Swap Used"));
109 | new_item->m_adapter = "proc-stat";
110 | new_item->setMinValue(0);
111 | new_item->setMaxValue(swapTotal);
112 | new_item->setNormalMinValue(0);
113 | new_item->setNormalMaxValue(swapTotal * 0.98);
114 | new_item->m_unit = "KB";
115 | m_sensors.append(new_item);
116 |
117 | // add lm-sensors
118 |
119 | if (int err = sensors_init(NULL)) {
120 | m_errorMessage = sensors_strerror(err);
121 | emit errorMessageChanged();
122 | emit sensorsChanged();
123 | return false;
124 | } else {
125 | chip_nr = 0;
126 |
127 | while ((chip = sensors_get_detected_chips(NULL, &chip_nr))) {
128 | if (sensors_snprintf_chip_name(buf, BUF_SIZE, chip) < 0)
129 | sprintf(buf, "%i", chip_nr);
130 |
131 | adap = sensors_get_adapter_name(&chip->bus);
132 | // if(adap) qDebug() << " " << adap;
133 | a = 0;
134 | while ((feature = sensors_get_features(chip, &a))) {
135 | // qDebug() << " " << sensors_get_label(chip, feature) << "type" << feature->type;
136 | Sensor *new_item = new Sensor();
137 |
138 | sub = sensors_get_subfeature(chip, feature, (sensors_subfeature_type)(((int)feature->type) << 8));
139 |
140 | new_item->m_index = m_sensors.count();
141 | new_item->setLabel(QLatin1String(sensors_get_label(chip, feature)));
142 | if (adap)
143 | new_item->m_adapter = adap;
144 | new_item->m_chip = chip;
145 | new_item->m_chipId = chip_nr;
146 | new_item->m_chipName = buf;
147 | new_item->m_feature = feature;
148 | new_item->m_subfeature = sub;
149 | new_item->setMinValue(0);
150 | new_item->setMaxValue(100);
151 |
152 | const sensors_subfeature *limitSub = nullptr;
153 |
154 | switch (new_item->m_feature->type) {
155 | case SENSORS_FEATURE_IN:
156 | new_item->m_type = Sensor::SensorType::Input;
157 | // fallthrough
158 | case SENSORS_FEATURE_VID:
159 | if (new_item->m_type == Sensor::SensorType::Unknown)
160 | new_item->m_type = Sensor::SensorType::Vid;
161 | new_item->setMinValue(0);
162 | new_item->setMaxValue(15);
163 | new_item->m_unit = "V";
164 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_IN_MIN);
165 | if (limitSub)
166 | sensors_get_value(chip, limitSub->number, &new_item->m_normalMinValue);
167 | else
168 | qDebug() << "no min limit" << new_item->chipName() << new_item->m_feature->name << new_item->m_subfeature->name;
169 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_IN_MAX);
170 | if (limitSub)
171 | sensors_get_value(chip, limitSub->number, &new_item->m_normalMaxValue);
172 | else
173 | qDebug() << "no max limit" << new_item->chipName() << new_item->m_feature->name << new_item->m_subfeature->name;
174 | break;
175 | case SENSORS_FEATURE_FAN:
176 | new_item->m_type = Sensor::SensorType::Fan;
177 | new_item->setMinValue(0);
178 | new_item->setMaxValue(7200);
179 | new_item->setNormalMaxValue(5500);
180 | new_item->m_unit = "RPM";
181 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_FAN_MIN);
182 | if (limitSub) sensors_get_value(chip, limitSub->number, &new_item->m_normalMinValue);
183 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_FAN_MAX);
184 | if (limitSub) sensors_get_value(chip, limitSub->number, &new_item->m_normalMaxValue);
185 | break;
186 | case SENSORS_FEATURE_TEMP:
187 | new_item->m_type = Sensor::SensorType::Temperature;
188 | new_item->m_unit = "°C";
189 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_TEMP_MIN);
190 | if (limitSub) sensors_get_value(chip, limitSub->number, &new_item->m_normalMinValue);
191 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_TEMP_MAX);
192 | if (limitSub) sensors_get_value(chip, limitSub->number, &new_item->m_normalMaxValue);
193 | break;
194 | case SENSORS_FEATURE_POWER:
195 | new_item->m_type = Sensor::SensorType::Power;
196 | new_item->m_unit = "W";
197 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_POWER_MAX);
198 | if (limitSub) sensors_get_value(chip, limitSub->number, &new_item->m_normalMaxValue);
199 | break;
200 | case SENSORS_FEATURE_ENERGY:
201 | new_item->m_type = Sensor::SensorType::Energy;
202 | new_item->m_unit = "J";
203 | break;
204 | case SENSORS_FEATURE_CURR:
205 | new_item->m_type = Sensor::SensorType::Current;
206 | new_item->m_unit = "A";
207 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_CURR_MIN);
208 | if (limitSub) sensors_get_value(chip, limitSub->number, &new_item->m_normalMinValue);
209 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_CURR_MAX);
210 | if (limitSub) sensors_get_value(chip, limitSub->number, &new_item->m_normalMaxValue);
211 | break;
212 | case SENSORS_FEATURE_HUMIDITY:
213 | new_item->m_type = Sensor::SensorType::Humidity;
214 | new_item->m_unit = "%";
215 | break;
216 | case SENSORS_FEATURE_INTRUSION:
217 | new_item->m_type = Sensor::SensorType::Intrusion;
218 | new_item->setMinValue(0);
219 | new_item->setMaxValue(12);
220 | break;
221 | default:;
222 | }
223 | // qDebug() << "range of" << new_item->m_label << new_item->m_normalMinValue << new_item->m_normalMaxValue;
224 |
225 | m_sensors.append(new_item);
226 | }
227 | }
228 | }
229 |
230 | // take inventory of batteries and power adapters
231 | QDir powerSupplyDir(QLatin1String("/sys/class/power_supply"));
232 | for (const QString &powerSupply : powerSupplyDir.entryList()) {
233 | if (powerSupply.startsWith(QLatin1String(".")))
234 | continue;
235 |
236 | bool isBattery = powerSupply.toLower().startsWith(QLatin1String("bat"));
237 | QDir dir(powerSupplyDir);
238 | dir.cd(powerSupply);
239 | if (isBattery) {
240 | for (QString metric : dir.entryList(QStringList() << "*_now")) {
241 | QString fullMetric(metric);
242 | fullMetric.replace(QLatin1String("_now"), QLatin1String("_full"));
243 | qreal fullAmount = readRealFile(dir.absoluteFilePath(fullMetric)) / 1000000;
244 | if (qIsNaN(fullAmount))
245 | continue;
246 | // OK we're good: we have a means of reading the "now" value, and have already read the "full" value
247 | new_item = new Sensor(Sensor::Energy);
248 | new_item->m_file = new QFile(dir.absoluteFilePath(metric));
249 | new_item->m_index = m_sensors.count();
250 | new_item->setLabel(powerSupply);
251 | new_item->m_adapter = "power_supply";
252 | new_item->setMinValue(0);
253 | new_item->setMaxValue(fullAmount);
254 | new_item->setNormalMinValue(fullAmount * 0.02);
255 | new_item->setNormalMaxValue(fullAmount);
256 | new_item->m_unit = "Ah"; // guessing uAh from the size of the number... so we divided down above
257 | new_item->m_scale = 1 / 1000000.0;
258 | m_sensors.append(new_item);
259 | }
260 | } else if (powerSupply.toLower().startsWith(QLatin1String("ac"))) {
261 | // AC adapter
262 | new_item = new Sensor(Sensor::Connected);
263 | new_item->m_file = new QFile(dir.absoluteFilePath(QLatin1String("online")));
264 | new_item->m_index = m_sensors.count();
265 | new_item->setLabel(powerSupply);
266 | new_item->m_adapter = "power_supply";
267 | new_item->setMinValue(0);
268 | new_item->setMaxValue(1);
269 | new_item->setNormalMinValue(0);
270 | new_item->setNormalMaxValue(1);
271 | new_item->m_unit = "connected";
272 | m_sensors.append(new_item);
273 | }
274 | }
275 |
276 | // find relevant cpufreq files
277 | QStringList curFreqs = find(QLatin1String("/sys/devices/system/cpu"), QStringList() << QLatin1String("scaling_cur_freq"));
278 | curFreqs.sort();
279 | QRegularExpression reCpu("cpu(\\d+)");
280 | bool hasPolicyFiles = false;
281 | bool skipPolicyFiles = false;
282 | for (const QString &cf : curFreqs)
283 | if (cf.contains("policy"))
284 | hasPolicyFiles = true;
285 | else
286 | skipPolicyFiles = true;
287 | for (const QString &cf : curFreqs) {
288 | // prefer /sys/devices/system/cpu/cpu2/cpufreq/scaling_cur_freq over /sys/devices/system/cpu/cpufreq/policy2/scaling_cur_freq, iff both exist
289 | if (skipPolicyFiles && cf.contains(QLatin1String("policy")))
290 | continue;
291 | QString minF(cf); minF.replace(QLatin1String("_cur"), QLatin1String("_min"));
292 | QString maxF(cf); maxF.replace(QLatin1String("_cur"), QLatin1String("_max"));
293 | qreal min = readRealFile(minF) / 1000; // kHz to MHz
294 | qreal max = readRealFile(maxF) / 1000;
295 | if (!(qIsNaN(min) || qIsNaN(max))) {
296 | new_item = new Sensor(Sensor::Frequency);
297 | new_item->m_file = new QFile(cf);
298 | new_item->m_index = m_sensors.count();
299 | {
300 | QRegularExpressionMatch match = reCpu.match(cf);
301 | if (match.hasMatch())
302 | new_item->setLabel(QString(QLatin1String("CPU %1")).arg(match.captured(0)));
303 | else
304 | new_item->setLabel(QLatin1String("CPU"));
305 | }
306 | new_item->m_adapter = "CPU";
307 | new_item->setMinValue(min);
308 | new_item->setMaxValue(max);
309 | new_item->setNormalMinValue(min);
310 | new_item->setNormalMaxValue(max * 0.98);
311 | new_item->m_unit = "MHz";
312 | new_item->m_scale = 1 / 1000.0;
313 | m_sensors.append(new_item);
314 | }
315 | }
316 |
317 | emit sensorsChanged();
318 | return true;
319 | }
320 |
321 | bool LmSensors::sampleAllValues()
322 | {
323 | qint64 timestamp = LineGraphModel::timeNowMs();
324 | for (Sensor *item : m_sensors)
325 | item->recordSample(timestamp);
326 | return true;
327 | }
328 |
329 | void LmSensors::setDownsampleInterval(qreal downsampleInterval)
330 | {
331 | for (Sensor *item : m_sensors)
332 | item->setDownsampleInterval(downsampleInterval);
333 | }
334 |
335 | QList LmSensors::filtered(int t, const QString substring)
336 | {
337 | QList ret;
338 | QVector types;
339 | if (t == Sensor::SensorType::Memory)
340 | types << Sensor::MemoryFree << Sensor::MemoryUsed << Sensor::MemoryCache << Sensor::SwapFree << Sensor::SwapUsed;
341 | else
342 | types << static_cast(t);
343 | for (Sensor * item : m_sensors)
344 | if ((t == Sensor::SensorType::Unknown || types.contains(item->type())) &&
345 | (substring.isEmpty() || item->label().contains(substring) ||
346 | item->chipName().contains(substring) || item->adapter().contains(substring)))
347 | ret << item;
348 | //qDebug() << "found" << ret.count() << "of type" << type;
349 | return ret;
350 | }
351 |
352 | qreal LmSensors::readRealFile(const QString &path)
353 | {
354 | QFile f(path);
355 | qreal val = qQNaN();
356 | if (f.open(QFile::ReadOnly)) {
357 | bool ok = false;
358 | val = f.readAll().trimmed().toDouble(&ok);
359 | f.close();
360 | if (!ok)
361 | val = qQNaN();
362 | }
363 | return val;
364 | }
365 |
366 | QStringList LmSensors::find(const QString &dir, const QStringList &nameFilters)
367 | {
368 | QDirIterator it(dir, nameFilters, QDir::NoFilter, QDirIterator::Subdirectories);
369 | QStringList ret;
370 | while (it.hasNext())
371 | ret << it.next();
372 | return ret;
373 | }
374 |
375 | void LmSensors::setUpdateIntervalMs(int updateIntervalMs)
376 | {
377 | if (m_updateIntervalMs == updateIntervalMs)
378 | return;
379 |
380 | m_updateIntervalMs = updateIntervalMs;
381 | killTimer(m_timerId);
382 | m_timerId = startTimer(m_updateIntervalMs);
383 | emit updateIntervalMsChanged();
384 | }
385 |
386 | Sensor::Sensor(SensorType type, QObject *parent) : LineGraphModel(parent)
387 | {
388 | m_type = type;
389 | m_maxValue = 100; // good for temperatures and percentages at least
390 | m_normalMaxValue = 100;
391 | }
392 |
393 | bool Sensor::recordSample(qint64 timestamp)
394 | {
395 | qreal val = sample();
396 | if (val == 65535)
397 | return false;
398 | appendSampleMs(val, timestamp);
399 | return true;
400 | }
401 |
402 | void Sensor::getMemoryMetric(const char *metric, qreal &val)
403 | {
404 | QFile data("/proc/meminfo");
405 | QLatin1String metricStr(metric);
406 | if (data.open(QFile::ReadOnly)) {
407 | QTextStream in(&data);
408 | QString line;
409 |
410 | do {
411 | line = in.readLine(1024);
412 | } while (!line.startsWith(metricStr));
413 |
414 | QString afterColon = line.mid(line.indexOf(':') + 1).trimmed();
415 | int spaceIdx = afterColon.indexOf(' ');
416 | QString beforeUnits = afterColon.left(spaceIdx);
417 | m_unit = afterColon.mid(spaceIdx + 1);
418 | bool ok = false;
419 | val = beforeUnits.toDouble(&ok);
420 | if (!ok)
421 | val = 0;
422 | else if (val > 10000 && m_unit.toLower() == QLatin1String("kb")) {
423 | val /= 1000;
424 | m_unit = QLatin1String("MB");
425 | }
426 | } else {
427 | val = 0;
428 | }
429 | }
430 |
431 | void Sensor::getCPULoad(qreal &val)
432 | {
433 | // http://stackoverflow.com/questions/3017162/how-to-get-total-cpu-usage-in-linux-c
434 | // http://www.linuxhowtos.org/System/procstat.htm
435 |
436 | // cpu 63536 963 11961 946741 2090 0 107 0 0 0
437 |
438 | QFile data("/proc/stat");
439 | if (data.open(QFile::ReadOnly)) {
440 | QTextStream in(&data);
441 | QStringList list;
442 | qint64 total_jiffies = 0, work_jiffies = 0;
443 |
444 | do {
445 | list = in.readLine(1024).split(" ", QString::SkipEmptyParts);
446 | } while (list.at(0) != "cpu");
447 |
448 | for (int x = 1; x < list.length(); x++)
449 | total_jiffies += list.at(x).toInt();
450 | for (int x = 1; x < 4; x++)
451 | work_jiffies += list.at(x).toInt();
452 |
453 | if (m_totalJiffies)
454 | val = qreal(work_jiffies - m_workJiffies) / qreal(total_jiffies - m_totalJiffies) * 100.;
455 | else
456 | val = 0;
457 |
458 | m_workJiffies = work_jiffies;
459 | m_totalJiffies = total_jiffies;
460 | } else {
461 | val = 0;
462 | }
463 | }
464 |
465 | qreal Sensor::sample()
466 | {
467 | qreal valTotal = 0;
468 | qreal val = 0;
469 |
470 | switch (m_type) {
471 | case Sensor::MemoryTotal:
472 | getMemoryMetric("MemTotal", val);
473 | break;
474 | case Sensor::MemoryFree:
475 | getMemoryMetric("MemFree", val);
476 | break;
477 | case Sensor::MemoryUsed:
478 | getMemoryMetric("MemTotal", valTotal);
479 | getMemoryMetric("MemFree", val);
480 | val = valTotal - val;
481 | break;
482 | case Sensor::MemoryCache:
483 | getMemoryMetric("Cached", val);
484 | break;
485 | case Sensor::SwapTotal:
486 | getMemoryMetric("SwapTotal", val);
487 | break;
488 | case Sensor::SwapFree:
489 | getMemoryMetric("SwapFree", val);
490 | break;
491 | case Sensor::SwapUsed:
492 | getMemoryMetric("SwapTotal", valTotal);
493 | getMemoryMetric("SwapFree", val);
494 | val = valTotal - val;
495 | break;
496 | case Sensor::SensorType::Cpu:
497 | getCPULoad(val);
498 | break;
499 | case Sensor::Connected:
500 | case Sensor::Energy:
501 | case Sensor::Frequency:
502 | if (m_file && m_file->open(QFile::ReadOnly)) {
503 | val = m_file->readAll().trimmed().toDouble();
504 | m_file->close();
505 | }
506 | break;
507 | default: // LM sensors
508 | sensors_get_value(m_chip, m_subfeature->number, &val);
509 | break;
510 | }
511 | val *= m_scale;
512 | return val;
513 | }
514 |
--------------------------------------------------------------------------------
/lmsensors.h:
--------------------------------------------------------------------------------
1 | #ifndef LMSENSORS_H
2 | #define LMSENSORS_H
3 |
4 | #include
5 |
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 |
15 | // LM-Sensors Library Header
16 | #include
17 | #include
18 | #include "linegraphmodel.h"
19 |
20 | class Sensor : public LineGraphModel
21 | {
22 | Q_OBJECT
23 | Q_PROPERTY(SensorType type READ type CONSTANT)
24 | Q_PROPERTY(QString adapter READ adapter CONSTANT)
25 | Q_PROPERTY(QString chipName READ chipName CONSTANT)
26 | Q_PROPERTY(int chipId READ chipId CONSTANT)
27 |
28 | public:
29 | enum SensorType { Unknown = 0, Cpu,
30 | Memory = 0x100, MemoryFree, MemoryUsed, MemoryCache, MemoryTotal, SwapFree, SwapUsed, SwapTotal,
31 | Input = 0x200, Fan, Temperature, Power, Energy, Current, Humidity, Vid, Intrusion, Connected, Frequency };
32 | Q_ENUM(SensorType)
33 |
34 | explicit Sensor(SensorType type = Unknown, QObject *parent = 0);
35 |
36 | Q_INVOKABLE qreal sample();
37 |
38 | SensorType type() { return m_type; }
39 | QString adapter() { return m_adapter; }
40 | QString chipName() { return m_chipName; }
41 | int chipId() { return m_chipId; }
42 |
43 | private:
44 | bool recordSample(qint64 timestamp);
45 | void getMemoryMetric(const char *metric, qreal &val);
46 | void getCPULoad(qreal &val);
47 |
48 | private:
49 | int m_index = -1;
50 | int m_chipId = 0;
51 | SensorType m_type = SensorType::Unknown;
52 | const sensors_chip_name *m_chip = 0;
53 | const sensors_feature *m_feature = 0;
54 | const sensors_subfeature *m_subfeature = 0;
55 | qint64 m_totalJiffies = 0;
56 | qint64 m_workJiffies = 0;
57 | qreal m_scale = 1;
58 | QString m_adapter;
59 | QString m_chipName;
60 | QFile *m_file = nullptr;
61 |
62 | friend class LmSensors;
63 | };
64 |
65 |
66 | class LmSensors : public QObject
67 | {
68 | Q_OBJECT
69 | Q_PROPERTY(bool initialized READ initialized NOTIFY sensorsChanged)
70 | Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged)
71 | Q_PROPERTY(QQmlListProperty sensors READ sensors NOTIFY sensorsChanged)
72 | Q_PROPERTY(int updateIntervalMs READ updateIntervalMs WRITE setUpdateIntervalMs NOTIFY updateIntervalMsChanged)
73 |
74 | public:
75 |
76 | explicit LmSensors(QObject *parent = 0);
77 |
78 | Q_INVOKABLE bool sampleAllValues();
79 | Q_INVOKABLE qint64 timestamp() { return (QDateTime().currentDateTime().toMSecsSinceEpoch()); }
80 | Q_INVOKABLE void setDownsampleInterval(qreal downsampleInterval);
81 |
82 | bool initialized() { return m_initialized; }
83 | QString errorMessage() { return m_errorMessage; }
84 |
85 | int updateIntervalMs() const { return m_updateIntervalMs; }
86 | void setUpdateIntervalMs(int updateIntervalMs);
87 |
88 | QQmlListProperty sensors();
89 |
90 | Q_INVOKABLE QList filtered(int type, const QString substring = QString()); // int is really Sensor::SensorType
91 | static qreal readRealFile(const QString &path);
92 |
93 | signals:
94 | void sensorsChanged();
95 | void errorMessageChanged();
96 | void updateIntervalMsChanged();
97 |
98 | protected:
99 | void timerEvent(QTimerEvent *) Q_DECL_OVERRIDE { sampleAllValues(); }
100 | static QStringList find(const QString &dir, const QStringList &nameFilters);
101 |
102 | private:
103 | bool init();
104 |
105 | private:
106 | QList m_sensors;
107 | QString m_errorMessage;
108 | bool m_initialized;
109 | int m_updateIntervalMs = 1000;
110 | int m_timerId = -1;
111 | };
112 |
113 | //QML_DECLARE_TYPE(LmSensors *)
114 | //QML_DECLARE_TYPE(Sensor *)
115 |
116 |
117 | #endif // LMSENSORS_H
118 |
--------------------------------------------------------------------------------
/main.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #include "LineGraph.h"
5 | #include
6 |
7 | int main(int argc, char *argv[])
8 | {
9 | QGuiApplication a(argc, argv);
10 |
11 | LineGraph::registerMetaType();
12 |
13 | QQuickView view;
14 | view.resize(1190, 900);
15 | view.setResizeMode(QQuickView::SizeRootObjectToView);
16 | view.setSource(QUrl("qrc:///main.qml"));
17 | view.show();
18 |
19 | return a.exec();
20 | }
21 |
--------------------------------------------------------------------------------
/org/ecloud/charts/qmldir:
--------------------------------------------------------------------------------
1 | module org.ecloud.charts
2 | plugin chartsplugin
3 | classname ChartsPlugin
4 | typeinfo plugins.qmltypes
5 |
--------------------------------------------------------------------------------
/plugin.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | #include "influxdb.h"
9 | #include "lmsensors.h"
10 | #include "linegraph.h"
11 |
12 | Q_LOGGING_CATEGORY(lcRegistration, "org.ecloud.charts.registration")
13 |
14 | static const char *ModuleName = "org.ecloud.charts";
15 |
16 | static QObject *LmSensorsSingleton(QQmlEngine *engine, QJSEngine *scriptEngine)
17 | {
18 | Q_UNUSED(engine)
19 | Q_UNUSED(scriptEngine)
20 |
21 | LmSensors *ret = new LmSensors();
22 | return ret;
23 | }
24 |
25 | class ChartsPlugin : public QQmlExtensionPlugin
26 | {
27 | Q_OBJECT
28 | Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface/1.0")
29 |
30 | public:
31 | ChartsPlugin() : QQmlExtensionPlugin() { }
32 |
33 | virtual void initializeEngine(QQmlEngine *engine, const char * uri) {
34 | Q_UNUSED(engine)
35 | qCDebug(lcRegistration) << uri;
36 | }
37 |
38 | virtual void registerTypes(const char *uri) {
39 | qCDebug(lcRegistration) << uri;
40 | Q_ASSERT(uri == QLatin1String(ModuleName));
41 | qmlRegisterType(uri, 1, 0, "LineGraph");
42 | qmlRegisterType(uri, 1, 0, "LineGraphModel");
43 | qmlRegisterSingletonType(uri, 1, 0, "LmSensors", LmSensorsSingleton);
44 | qmlRegisterType(uri, 1, 0, "Sensor");
45 | qmlRegisterType(uri, 1, 0, "InfluxQuery");
46 | qmlRegisterUncreatableType(uri, 1, 0, "InfluxValueSeries", "InfluxValueSeries is only available from InfluxQuery");
47 | // qmlRegisterType(uri, 1, 0, "InfluxValueSeries");
48 | }
49 | };
50 |
51 | QT_END_NAMESPACE
52 |
53 | #include "plugin.moc"
54 |
--------------------------------------------------------------------------------
/shaders/LineNode.fsh:
--------------------------------------------------------------------------------
1 | uniform lowp float qt_Opacity;
2 | uniform lowp float aa;
3 |
4 | varying lowp float vT;
5 | varying lowp vec4 vColor;
6 |
7 | #define PI 3.14159265359
8 |
9 | void main(void)
10 | {
11 | lowp float tt = abs(aa - 1.0) + aa * cos(vT * PI);
12 |
13 | gl_FragColor = vColor * qt_Opacity * tt;
14 | }
15 |
--------------------------------------------------------------------------------
/shaders/LineNode.vsh:
--------------------------------------------------------------------------------
1 | #version 120
2 |
3 | // per-vertex givens
4 | attribute highp vec4 pos; // x, y, i, t; x and y are time and value, in real-world units
5 | attribute highp vec4 prevNext; // time and value of the previous datapoint, and the next one
6 |
7 | // constants passed in from LineNode
8 | uniform lowp float height; // total height in pixels
9 | uniform lowp float lineWidth; // in pixels
10 | uniform lowp float warningBelowMinimum; // change to warningMinColor if datapoint > warningBelowMinimum
11 | uniform lowp float warningAboveMaximum; // change to warningMaxColor if datapoint > warningAboveMaximum
12 | uniform lowp float fillDirection; // -1 to fill above, +1 to fill below, 0 to stroke
13 | uniform lowp vec4 normalColor; // color to stroke or fill, if data is within range
14 | uniform lowp vec4 warningMinColor;
15 | uniform lowp vec4 warningMaxColor;
16 | uniform highp mat2 dataScalingTransform;// transform to go from real-world units to pixels
17 | uniform highp vec2 dataOffset; // offset in pixels, to locate the y axis and to put the latest sample at the right
18 | uniform highp mat4 qt_Matrix; // Qt's usual transform to go from Item to Window coordinates
19 |
20 | // variables which will be passed through to the frag shader, and automatically interpolated between vertices
21 | varying lowp float vT; // distance: 0 is on the line; goes to +/-0.5 outwards across the stroke
22 | varying lowp vec4 vColor; // color of the vertex
23 |
24 | void main(void)
25 | {
26 | float i = pos.z; // vertex index from 0 to 3 (there are 4 vertices per datapoint)
27 | float t = pos.w; // -1 for even (lower) vertices, +1 for odd (upper) vertices (redundant, since we could calculate it from i)
28 | vec2 posPx = dataScalingTransform * pos.xy; // the data point, transformed to pixel units, in 2D (we don't need 3D)
29 | float oddMult = mod(i, 2.0); // will be 1 if i is odd, 0 if it's even
30 | float evenMult = mod(i + 1.0, 2.0); // will be 0 if i is odd, 1 if it's even
31 | vec2 offset = vec2(0.); // how much we will shift the vertex from its on-line position
32 |
33 | if (fillDirection == 0.) {
34 | // we are stroking: need to calculate the miter or knee
35 | vec2 prev = dataScalingTransform * prevNext.xy;
36 | vec2 next = dataScalingTransform * prevNext.zw;
37 | vec2 lineToward = normalize(posPx - prev);
38 | vec2 lineAway = normalize(next - posPx);
39 | vec2 normal = vec2(lineAway.y, -lineAway.x);
40 | vec2 averageTangent = (prev == posPx) ? lineAway : normalize(lineToward + lineAway);
41 | vec2 miter = normalize(vec2(-averageTangent.y, averageTangent.x));
42 | float halfLineWidth = lineWidth / 2.0;
43 | float miterLength = halfLineWidth / dot(normal, miter);
44 |
45 | if (dot(lineToward, lineAway) >= 0) {
46 | // angle is right or obtuse: OK to use ordinary miter
47 | offset = -t * miterLength * miter;
48 | } else {
49 | // angle is acute: make a beveled miter to avoid overshooting too far
50 | vec2 upToCap = miter * halfLineWidth * t;
51 | vec2 capDeviation = averageTangent * halfLineWidth;
52 | float innerMiterLimit = max(-lineWidth, min(posPx.x - prev.x, lineWidth)) * 8.;
53 | miterLength = max(-innerMiterLimit, min(miterLength, innerMiterLimit));
54 | if (lineToward.y > 0) {
55 | capDeviation *= sign(i - 2.0) * mod(i - 2.0, 2.0); // lower knee
56 | offset = oddMult * (upToCap + capDeviation) + evenMult * (-t * miterLength * miter);
57 | } else {
58 | capDeviation *= sign(i - 1.0) * mod(i - 1.0, 2.0); // upper knee
59 | offset = evenMult * (upToCap + capDeviation) + oddMult * (-t * miterLength * miter);
60 | }
61 | }
62 | }
63 |
64 | posPx += dataOffset;
65 |
66 | if (fillDirection != 0.) {
67 | // we are filling space above or below the line
68 | // odd vertices stay in place, while even vertices are moved to the top or bottom
69 | posPx.y *= oddMult;
70 | if (fillDirection > 0)
71 | offset.y = evenMult * height * fillDirection;
72 | }
73 |
74 | // apply the calculated offset to the vertex, and then apply Qt's usual
75 | // transform to go from Item coordinates to Window coordinates (and in 3D)
76 | gl_Position = qt_Matrix * vec4(posPx + offset, 0, 1.0);
77 |
78 | vT = t * 0.5;
79 | vColor = pos.y > warningAboveMaximum ? warningMaxColor :
80 | pos.y < warningBelowMinimum ? warningMinColor : normalColor;
81 | }
82 |
--------------------------------------------------------------------------------