diff options
author | Hongmei Gou | 2019-05-09 09:24:07 -0500 |
---|---|---|
committer | Hongmei Gou | 2019-05-09 09:24:07 -0500 |
commit | b1f2a6fc7f38db24b0daa6d972c796a2536e050a (patch) | |
tree | e7a40dc0ff53d9ae174f1f2206e8f64b47dd3b3b | |
parent | d562ac54800e5bb13fe73636f34b283442faacc0 (diff) | |
parent | 8c25ca56383df4a3b71b8f34fde1ffbe1212f11b (diff) | |
download | pdm-anomaly-detection-b1f2a6fc7f38db24b0daa6d972c796a2536e050a.tar.gz pdm-anomaly-detection-b1f2a6fc7f38db24b0daa6d972c796a2536e050a.tar.xz pdm-anomaly-detection-b1f2a6fc7f38db24b0daa6d972c796a2536e050a.zip |
Merge pull request #2 in PROCESSOR-SDK/pdm-anomaly-detection from pdm-demo-bringup to master
* commit '8c25ca56383df4a3b71b8f34fde1ffbe1212f11b':
Add throttling when there is no incoming sensor data
Qt GUI: add setup image and block diagram along with a button to hide/display them
Add Qt GUI to display sensor data and anomaly detection results
-rw-r--r-- | datasource.cpp | 80 | ||||
-rw-r--r-- | datasource.h | 67 | ||||
-rw-r--r-- | logs/normal100-anomaly150-normal100.log | bin | 0 -> 9093122 bytes | |||
-rw-r--r-- | logs/normal270-anomaly170-normal270.log | bin | 0 -> 8601602 bytes | |||
-rw-r--r-- | main.cpp | 60 | ||||
-rwxr-xr-x | mk.sh | 1 | ||||
-rw-r--r-- | motor-pdm.cpp | 99 | ||||
-rw-r--r-- | oscilloscope.pro | 28 | ||||
-rw-r--r-- | psensors.cpp | 12 | ||||
-rw-r--r-- | qml/oscilloscope/ControlPanel.qml | 17 | ||||
-rw-r--r-- | qml/oscilloscope/MultiButton.qml | 40 | ||||
-rw-r--r-- | qml/oscilloscope/PdmView.qml | 74 | ||||
-rw-r--r-- | qml/oscilloscope/ScopeView.qml | 81 | ||||
-rw-r--r-- | qml/oscilloscope/anomaly-detection.png | bin | 0 -> 23510 bytes | |||
-rw-r--r-- | qml/oscilloscope/main.qml | 142 | ||||
-rw-r--r-- | qml/oscilloscope/motor-drive.png | bin | 0 -> 231705 bytes | |||
-rw-r--r-- | qml/oscilloscope/system-model.png | bin | 0 -> 61284 bytes | |||
-rw-r--r-- | qml/oscilloscope/ti-logo.png | bin | 0 -> 19964 bytes | |||
-rw-r--r-- | readme.txt | 21 | ||||
-rw-r--r-- | resources.qrc | 13 | ||||
-rwxr-xr-x | run.sh | 1 |
21 files changed, 693 insertions, 43 deletions
diff --git a/datasource.cpp b/datasource.cpp new file mode 100644 index 0000000..8376f05 --- /dev/null +++ b/datasource.cpp | |||
@@ -0,0 +1,80 @@ | |||
1 | #include <iostream> | ||
2 | #include "datasource.h" | ||
3 | #include <QtCharts/QXYSeries> | ||
4 | #include <QtCharts/QAreaSeries> | ||
5 | #include <QtQuick/QQuickView> | ||
6 | #include <QtQuick/QQuickItem> | ||
7 | #include <QtCore/QDebug> | ||
8 | |||
9 | #include <QtCore/QtMath> | ||
10 | |||
11 | QT_CHARTS_USE_NAMESPACE | ||
12 | |||
13 | Q_DECLARE_METATYPE(QAbstractSeries *) | ||
14 | Q_DECLARE_METATYPE(QAbstractAxis *) | ||
15 | |||
16 | DataSource::DataSource(QQuickView *appViewer, QObject *parent) : | ||
17 | QObject(parent), | ||
18 | m_appViewer(appViewer) | ||
19 | { | ||
20 | qRegisterMetaType<QAbstractSeries*>(); | ||
21 | qRegisterMetaType<QAbstractAxis*>(); | ||
22 | |||
23 | for (int i=0; i<DISPLAY_VECTOR; i++) | ||
24 | { | ||
25 | generateData(i, 0, 256); | ||
26 | } | ||
27 | |||
28 | setPdmStage("Waiting for sensor data......"); | ||
29 | setAnomalyDetection(""); | ||
30 | } | ||
31 | |||
32 | void DataSource::update(int sel_waveform, QAbstractSeries *series) | ||
33 | { | ||
34 | if (series && (m_last_read > 0)) { | ||
35 | QXYSeries *xySeries = static_cast<QXYSeries *>(series); | ||
36 | QVector<QPointF> points = m_data[sel_waveform]; | ||
37 | // Use replace instead of clear + append, it's optimized for performance | ||
38 | qreal x(0); | ||
39 | qreal y(0); | ||
40 | for (int j = 0; j < m_sampleCount; j ++) | ||
41 | { | ||
42 | y = m_samplesArray[DISPLAY_VECTOR*j + sel_waveform]; | ||
43 | x = j; | ||
44 | if (sel_waveform==4) { | ||
45 | if (y > 100) {y = y - 100; setPdmStage("Calibration......");} | ||
46 | if (y < -50) {y = y + 100; setPdmStage("Looking for anomaly......");} | ||
47 | } | ||
48 | points.replace(j, QPointF(x, y)); | ||
49 | if ((sel_waveform==5) && (y>0)) setAnomalyDetection("Anomaly detected!!!"); | ||
50 | if ((sel_waveform==5) && (y<=0)) setAnomalyDetection(""); | ||
51 | } | ||
52 | xySeries->replace(points); | ||
53 | } | ||
54 | } | ||
55 | |||
56 | int motor_pdm_get_data(int samples_count, double *samples_array); | ||
57 | void DataSource::getdata(void) | ||
58 | { | ||
59 | m_last_read = motor_pdm_get_data(m_sampleCount, m_samplesArray); | ||
60 | } | ||
61 | |||
62 | void DataSource::generateData(int sel_waveform, int type, int colCount) | ||
63 | { | ||
64 | // Remove previous data | ||
65 | m_data[sel_waveform].clear(); | ||
66 | |||
67 | std::cout << std::endl << "__generating (" << sel_waveform << ":" << colCount << " type:" << type << ")____" << std::endl; | ||
68 | |||
69 | m_sampleCount = colCount; | ||
70 | // Append the new data depending on the type | ||
71 | QVector<QPointF> points; | ||
72 | points.reserve(colCount); | ||
73 | m_overlapWindow = type; | ||
74 | for (int j(0); j < colCount; j++) { | ||
75 | qreal x(0); | ||
76 | qreal y(0); | ||
77 | points.append(QPointF(x, y)); | ||
78 | } | ||
79 | m_data[sel_waveform] = points; | ||
80 | } | ||
diff --git a/datasource.h b/datasource.h new file mode 100644 index 0000000..ae88321 --- /dev/null +++ b/datasource.h | |||
@@ -0,0 +1,67 @@ | |||
1 | #ifndef DATASOURCE_H | ||
2 | #define DATASOURCE_H | ||
3 | |||
4 | #include <QtCore/QObject> | ||
5 | #include <QtCharts/QAbstractSeries> | ||
6 | |||
7 | #define DISPLAY_VECTOR 6 | ||
8 | |||
9 | class QQuickView; | ||
10 | |||
11 | QT_CHARTS_USE_NAMESPACE | ||
12 | |||
13 | class DataSource : public QObject | ||
14 | { | ||
15 | Q_OBJECT | ||
16 | Q_PROPERTY(QString pdmStage READ getPdmStage WRITE setPdmStage NOTIFY pdmStageChanged) | ||
17 | Q_PROPERTY(QString anomalyDetection READ getAnomalyDetection WRITE setAnomalyDetection NOTIFY anomalyDetectionChanged) | ||
18 | |||
19 | public: | ||
20 | explicit DataSource(QQuickView *appViewer, QObject *parent = 0); | ||
21 | |||
22 | QString getPdmStage() | ||
23 | { | ||
24 | return pdmStage; | ||
25 | } | ||
26 | |||
27 | void setPdmStage(const QString &text) | ||
28 | { | ||
29 | if (text == pdmStage) return; | ||
30 | pdmStage = text; | ||
31 | emit pdmStageChanged(); | ||
32 | } | ||
33 | |||
34 | QString getAnomalyDetection() | ||
35 | { | ||
36 | return anomalyDetection; | ||
37 | } | ||
38 | |||
39 | void setAnomalyDetection(const QString &text) | ||
40 | { | ||
41 | if (text == anomalyDetection) return; | ||
42 | anomalyDetection = text; | ||
43 | emit anomalyDetectionChanged(); | ||
44 | } | ||
45 | |||
46 | Q_SIGNALS: | ||
47 | void pdmStageChanged(); | ||
48 | void anomalyDetectionChanged(); | ||
49 | |||
50 | public slots: | ||
51 | void generateData(int sel_waveform, int type, int colCount); | ||
52 | void update(int sel_waveform, QAbstractSeries *series); | ||
53 | void getdata(void); | ||
54 | |||
55 | private: | ||
56 | QQuickView *m_appViewer; | ||
57 | QVector<QPointF> m_data[DISPLAY_VECTOR]; | ||
58 | |||
59 | int m_sampleCount, m_last_read; | ||
60 | int m_overlapWindow; | ||
61 | |||
62 | QString pdmStage, anomalyDetection; | ||
63 | |||
64 | double m_samplesArray[4096*DISPLAY_VECTOR]; // Keep in sync with max sample count selection in qml file | ||
65 | }; | ||
66 | |||
67 | #endif // DATASOURCE_H | ||
diff --git a/logs/normal100-anomaly150-normal100.log b/logs/normal100-anomaly150-normal100.log new file mode 100644 index 0000000..47973b2 --- /dev/null +++ b/logs/normal100-anomaly150-normal100.log | |||
Binary files differ | |||
diff --git a/logs/normal270-anomaly170-normal270.log b/logs/normal270-anomaly170-normal270.log new file mode 100644 index 0000000..a110ad7 --- /dev/null +++ b/logs/normal270-anomaly170-normal270.log | |||
Binary files differ | |||
diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..2988119 --- /dev/null +++ b/main.cpp | |||
@@ -0,0 +1,60 @@ | |||
1 | #include <QtWidgets/QApplication> | ||
2 | #include <QtQml/QQmlContext> | ||
3 | #include <QtQuick/QQuickView> | ||
4 | #include <QtQml/QQmlEngine> | ||
5 | #include <QtCore/QDir> | ||
6 | #include "datasource.h" | ||
7 | #include <iostream> // std::cout | ||
8 | #include <thread> // std::thread | ||
9 | |||
10 | int motor_pdm_process(void); | ||
11 | |||
12 | double mu1, mu2, sig1, sig2; | ||
13 | double relative_threshold = 1.5; | ||
14 | int main(int argc, char *argv[]) | ||
15 | { | ||
16 | |||
17 | //Get normalization parameters and relative threshold | ||
18 | if(argc < 5) { | ||
19 | printf("\nUsage:%s mu1 mu2 sig1 sig2 elative-threshold\n", argv[0]); | ||
20 | printf("\nTo set the relative threshold value, use an addtional positive value >= 1\n"); | ||
21 | return -1; | ||
22 | } | ||
23 | mu1 = atof(argv[1]); | ||
24 | mu2 = atof(argv[2]); | ||
25 | sig1 = atof(argv[3]); | ||
26 | sig2 = atof(argv[4]); | ||
27 | |||
28 | if(argc > 5) | ||
29 | relative_threshold = atof(argv[5]); | ||
30 | |||
31 | // Qt Charts uses Qt Graphics View Framework for drawing, therefore QApplication must be used. | ||
32 | QApplication app(argc, argv); | ||
33 | |||
34 | QQuickView viewer; | ||
35 | |||
36 | // The following are needed to make examples run without having to install the module | ||
37 | // in desktop environments. | ||
38 | #ifdef Q_OS_WIN | ||
39 | QString extraImportPath(QStringLiteral("%1/../../../../%2")); | ||
40 | #else | ||
41 | QString extraImportPath(QStringLiteral("%1/../../../%2")); | ||
42 | #endif | ||
43 | viewer.engine()->addImportPath(extraImportPath.arg(QGuiApplication::applicationDirPath(), | ||
44 | QString::fromLatin1("qml"))); | ||
45 | QObject::connect(viewer.engine(), &QQmlEngine::quit, &viewer, &QWindow::close); | ||
46 | |||
47 | viewer.setTitle(QStringLiteral("Deep Learning based Predictive Maintenance Demo on Sitara Edge Devices")); | ||
48 | |||
49 | std::thread motor_pdm_process_thread (motor_pdm_process); | ||
50 | |||
51 | DataSource dataSource(&viewer); | ||
52 | viewer.rootContext()->setContextProperty("dataSource", &dataSource); | ||
53 | |||
54 | viewer.setSource(QUrl("qrc:/qml/oscilloscope/main.qml")); | ||
55 | viewer.setResizeMode(QQuickView::SizeRootObjectToView); | ||
56 | viewer.setColor(QColor("#f5f5dc")); | ||
57 | viewer.show(); | ||
58 | |||
59 | return app.exec(); | ||
60 | } | ||
@@ -1 +0,0 @@ | |||
1 | g++ --std=c++11 -Wall -O3 motor-pdm.cpp psensors.cpp lstm_infer.cpp onnx_model.cpp config.cpp -lpthread -o motpdm | ||
diff --git a/motor-pdm.cpp b/motor-pdm.cpp index 5b30193..9e60df5 100644 --- a/motor-pdm.cpp +++ b/motor-pdm.cpp | |||
@@ -26,21 +26,19 @@ | |||
26 | // delay line to hold input samples | 26 | // delay line to hold input samples |
27 | double insampPH1[ BUFFER_LEN ]; | 27 | double insampPH1[ BUFFER_LEN ]; |
28 | double insampPH2[ BUFFER_LEN ]; | 28 | double insampPH2[ BUFFER_LEN ]; |
29 | double insampPH3[ BUFFER_LEN ]; | ||
29 | // LP filter coefficients | 30 | // LP filter coefficients |
30 | #define DRATIO 200 | 31 | #define DRATIO 200 |
31 | double coeffs[ FIRFLT_LEN ]; | 32 | double coeffs[ FIRFLT_LEN ]; |
32 | 33 | ||
33 | // Relative prediction error threshold | ||
34 | #define RELATIVE_THRESHOLD 2.0 | ||
35 | |||
36 | // Each sample tick is 20ms long | 34 | // Each sample tick is 20ms long |
37 | #define START_FIND_THRESHOLD 150 | 35 | #define START_FIND_THRESHOLD 150 |
38 | #define STOP_FIND_THRESHOLD 1500 | 36 | #define STOP_FIND_THRESHOLD 1500 |
39 | #define HANGOVER_TIME 100 | 37 | #define HANGOVER_TIME 20 |
40 | 38 | ||
41 | #define max(a,b) (a > b ? a : b) | 39 | #define max(a,b) (a > b ? a : b) |
42 | 40 | ||
43 | int uart_get_data(int sample_count, int op_mode, float *samples_array); | 41 | int uart_get_data(int sample_count, float *samples_array); |
44 | void uart_stream_parser(int mode_op); | 42 | void uart_stream_parser(int mode_op); |
45 | void lstmSetup(void); | 43 | void lstmSetup(void); |
46 | void runLstm(double lstm_in1, double lstm_in2, double *lstm_out1, double *lstm_out2); | 44 | void runLstm(double lstm_in1, double lstm_in2, double *lstm_out1, double *lstm_out2); |
@@ -80,24 +78,54 @@ void firFloat( double *coeffs, double *insamp, int filterLength, | |||
80 | // shift input samples back in time for next time | 78 | // shift input samples back in time for next time |
81 | memmove( &insamp[0], &insamp[length], (filterLength - 1) * sizeof(double) ); | 79 | memmove( &insamp[0], &insamp[length], (filterLength - 1) * sizeof(double) ); |
82 | } | 80 | } |
83 | ////////////////////////////////////////////////////////////// | 81 | |
84 | // Test program | 82 | #define DISPLAY_VECTOR 6 |
85 | ////////////////////////////////////////////////////////////// | 83 | #define DISPLAY_MAX_BUFFER (DISPLAY_VECTOR*64*4096) |
86 | // number of samples to read per loop | 84 | static int display_rd_data_cnt = 0; |
87 | int main( int argc, char *argv[]) | 85 | static int display_wr_data_cnt = 0; |
86 | static double data_ready_to_display[DISPLAY_MAX_BUFFER]; | ||
87 | |||
88 | int display_wr_idx = 0; | ||
89 | |||
90 | int motor_pdm_get_data(int samples_count, double *samples_array) | ||
91 | { | ||
92 | int rd_idx; | ||
93 | if(display_wr_data_cnt >= (display_rd_data_cnt + samples_count*DISPLAY_VECTOR)) | ||
94 | { | ||
95 | for (int i = 0; i < samples_count; i ++) | ||
96 | { | ||
97 | rd_idx = display_rd_data_cnt % DISPLAY_MAX_BUFFER; | ||
98 | for (int j=0; j<DISPLAY_VECTOR; j++) | ||
99 | { | ||
100 | samples_array[DISPLAY_VECTOR*i + j] = data_ready_to_display[rd_idx + j]; | ||
101 | } | ||
102 | display_rd_data_cnt += DISPLAY_VECTOR; | ||
103 | } | ||
104 | |||
105 | display_rd_data_cnt -= (127 * samples_count*DISPLAY_VECTOR) / 128; | ||
106 | return samples_count; | ||
107 | } else return 0; | ||
108 | } | ||
109 | |||
110 | extern double mu1, mu2, sig1, sig2; | ||
111 | extern double relative_threshold; | ||
112 | int motor_pdm_process(void) | ||
88 | { | 113 | { |
89 | int size; | 114 | int size; |
90 | double floatInput1[SAMPLES]; | 115 | double floatInput1[SAMPLES]; |
91 | double floatInput2[SAMPLES]; | 116 | double floatInput2[SAMPLES]; |
117 | double floatInput3[SAMPLES]; | ||
92 | double floatOutput1[SAMPLES/DRATIO]; | 118 | double floatOutput1[SAMPLES/DRATIO]; |
93 | double floatOutput2[SAMPLES/DRATIO]; | 119 | double floatOutput2[SAMPLES/DRATIO]; |
120 | double floatOutput3[SAMPLES/DRATIO]; | ||
94 | double predict_threshold_err = 0.0; | 121 | double predict_threshold_err = 0.0; |
95 | double predict1 = 0.0; | 122 | double predict1 = 0.0; |
96 | double predict2 = 0.0; | 123 | double predict2 = 0.0; |
97 | double mu1, mu2, sig1, sig2; | ||
98 | FILE *coeff_fid; | 124 | FILE *coeff_fid; |
99 | int samples_counter = 0; | 125 | int samples_counter = 0; |
100 | int restart_hangover_timer = 0; | 126 | int restart_hangover_timer = 0; |
127 | int ad_detected = 0; | ||
128 | int pdmState = 0; | ||
101 | float samples_array[3*SAMPLES]; | 129 | float samples_array[3*SAMPLES]; |
102 | 130 | ||
103 | //Pick up decimating FIR filter coefficients | 131 | //Pick up decimating FIR filter coefficients |
@@ -109,16 +137,6 @@ int main( int argc, char *argv[]) | |||
109 | fread(coeffs, sizeof(double), FIRFLT_LEN, coeff_fid ); | 137 | fread(coeffs, sizeof(double), FIRFLT_LEN, coeff_fid ); |
110 | fclose( coeff_fid ); | 138 | fclose( coeff_fid ); |
111 | 139 | ||
112 | //Get normalization parameters | ||
113 | if(argc < 5) { | ||
114 | printf("\nUsage:%s mu1 mu2 sig1 sig2\n", argv[0]); | ||
115 | return -1; | ||
116 | } | ||
117 | mu1 = atof(argv[1]); | ||
118 | mu2 = atof(argv[2]); | ||
119 | sig1 = atof(argv[3]); | ||
120 | sig2 = atof(argv[4]); | ||
121 | |||
122 | lstmSetup(); | 140 | lstmSetup(); |
123 | // initialize the filter delay lines | 141 | // initialize the filter delay lines |
124 | firFloatInit(insampPH1, BUFFER_LEN); | 142 | firFloatInit(insampPH1, BUFFER_LEN); |
@@ -130,43 +148,67 @@ int main( int argc, char *argv[]) | |||
130 | while(1) | 148 | while(1) |
131 | { | 149 | { |
132 | // read samples from UART pipe | 150 | // read samples from UART pipe |
133 | size = uart_get_data(SAMPLES, 0, samples_array); | 151 | size = uart_get_data(SAMPLES, samples_array); |
134 | if(size == SAMPLES) | 152 | if(size == SAMPLES) |
135 | { // It means, at least this many SAMPLES are available | 153 | { // It means, at least this many SAMPLES are available |
136 | for(int i = 0; i < SAMPLES; i ++) { | 154 | for(int i = 0; i < SAMPLES; i ++) { |
137 | floatInput1[i] = (double)samples_array[3*i + 0]; | 155 | floatInput1[i] = (double)samples_array[3*i + 0]; |
138 | floatInput2[i] = (double)samples_array[3*i + 1]; | 156 | floatInput2[i] = (double)samples_array[3*i + 1]; |
139 | // position is samples_array[3*i + 2] | 157 | floatInput3[i] = (double)samples_array[3*i + 2]; |
140 | } | 158 | } |
141 | // perform the filtering | 159 | // perform the filtering |
142 | // One decimated sample generated | 160 | // One decimated sample generated |
143 | firFloat( coeffs, insampPH1, FIRFLT_LEN, floatInput1, SAMPLES, floatOutput1 ); | 161 | firFloat( coeffs, insampPH1, FIRFLT_LEN, floatInput1, SAMPLES, floatOutput1 ); |
144 | firFloat( coeffs, insampPH2, FIRFLT_LEN, floatInput2, SAMPLES, floatOutput2 ); | 162 | firFloat( coeffs, insampPH2, FIRFLT_LEN, floatInput2, SAMPLES, floatOutput2 ); |
163 | firFloat( coeffs, insampPH3, FIRFLT_LEN, floatInput3, SAMPLES, floatOutput3 ); | ||
164 | |||
165 | // Writting too fast, do write side throttling | ||
166 | if(display_wr_data_cnt > (display_rd_data_cnt + DISPLAY_MAX_BUFFER - 16)) | ||
167 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); | ||
168 | |||
169 | for (int i = 0; i < SAMPLES/DRATIO; i ++) | ||
170 | { | ||
171 | display_wr_idx = display_wr_data_cnt % DISPLAY_MAX_BUFFER; | ||
172 | data_ready_to_display[display_wr_idx+0] = floatOutput1[i]; | ||
173 | data_ready_to_display[display_wr_idx+1] = floatOutput2[i]; | ||
174 | data_ready_to_display[display_wr_idx+2] = floatOutput3[i]; | ||
175 | display_wr_data_cnt += 3; | ||
176 | } | ||
177 | |||
145 | //Calculate prediction error (using previously predicted samples) | 178 | //Calculate prediction error (using previously predicted samples) |
146 | double predict_error = (floatOutput1[0] - predict1) * (floatOutput1[0] - predict1); | 179 | double predict_error = (floatOutput1[0] - predict1) * (floatOutput1[0] - predict1); |
147 | predict_error += (floatOutput2[0] - predict2) * (floatOutput2[0] - predict2); | 180 | predict_error += (floatOutput2[0] - predict2) * (floatOutput2[0] - predict2); |
181 | |||
182 | ad_detected = 0; | ||
183 | |||
148 | if(samples_counter > START_FIND_THRESHOLD) | 184 | if(samples_counter > START_FIND_THRESHOLD) |
149 | { | 185 | { |
150 | if(samples_counter < STOP_FIND_THRESHOLD) | 186 | if(samples_counter < STOP_FIND_THRESHOLD) |
151 | { // Find error threshold | 187 | { // Find error threshold |
188 | pdmState = 100; | ||
152 | predict_threshold_err = max(predict_error, predict_threshold_err); | 189 | predict_threshold_err = max(predict_error, predict_threshold_err); |
153 | std::cout << "Find error threshold:" << samples_counter << std::endl; | ||
154 | restart_hangover_timer = 0; | 190 | restart_hangover_timer = 0; |
155 | } else { | 191 | } else { |
156 | std::cout << "Look for anomaly:" << samples_counter << " err:" << predict_error << " thre:" << (RELATIVE_THRESHOLD * predict_threshold_err); | 192 | pdmState = -100; |
157 | // Anomaly detection mode! | 193 | // Anomaly detection mode! |
158 | // ...implemented hangover | 194 | // ...implemented hangover |
159 | if(predict_error > RELATIVE_THRESHOLD * predict_threshold_err) | 195 | if(predict_error > relative_threshold * predict_threshold_err) |
160 | { | 196 | { |
161 | restart_hangover_timer = samples_counter; | 197 | restart_hangover_timer = samples_counter; |
162 | } | 198 | } |
163 | std::cout << std::endl; | ||
164 | if((samples_counter - restart_hangover_timer) < HANGOVER_TIME) | 199 | if((samples_counter - restart_hangover_timer) < HANGOVER_TIME) |
165 | { | 200 | { |
166 | std::cout << " [AD] "; | 201 | ad_detected = 1; |
167 | } | 202 | } |
168 | } | 203 | } |
169 | } | 204 | } |
205 | |||
206 | display_wr_idx = display_wr_data_cnt % DISPLAY_MAX_BUFFER; | ||
207 | data_ready_to_display[display_wr_idx+0] = predict_error; | ||
208 | data_ready_to_display[display_wr_idx+1] = predict_threshold_err*relative_threshold + pdmState; | ||
209 | data_ready_to_display[display_wr_idx+2] = ad_detected * predict_threshold_err *relative_threshold * 1.25; | ||
210 | display_wr_data_cnt += 3; | ||
211 | |||
170 | //Normalize | 212 | //Normalize |
171 | double lstm_out1, lstm_in1 = (floatOutput1[0] - mu1) / sig1; | 213 | double lstm_out1, lstm_in1 = (floatOutput1[0] - mu1) / sig1; |
172 | double lstm_out2, lstm_in2 = (floatOutput2[0] - mu2) / sig2; | 214 | double lstm_out2, lstm_in2 = (floatOutput2[0] - mu2) / sig2; |
@@ -179,7 +221,6 @@ int main( int argc, char *argv[]) | |||
179 | samples_counter ++; // Increamented at decimated rate!!! | 221 | samples_counter ++; // Increamented at decimated rate!!! |
180 | } else { | 222 | } else { |
181 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); | 223 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); |
182 | std::cout << "sleep" << std::endl; | ||
183 | } | 224 | } |
184 | } | 225 | } |
185 | 226 | ||
diff --git a/oscilloscope.pro b/oscilloscope.pro new file mode 100644 index 0000000..effdd51 --- /dev/null +++ b/oscilloscope.pro | |||
@@ -0,0 +1,28 @@ | |||
1 | QT += charts qml quick | ||
2 | |||
3 | DEFINES += QRANDOMGENERATOR_MISSING | ||
4 | |||
5 | HEADERS += \ | ||
6 | datasource.h\ | ||
7 | config.h \ | ||
8 | LSTM_model.h \ | ||
9 | onnx_model.h | ||
10 | |||
11 | SOURCES += \ | ||
12 | main.cpp \ | ||
13 | datasource.cpp \ | ||
14 | psensors.cpp \ | ||
15 | motor-pdm.cpp \ | ||
16 | lstm_infer.cpp \ | ||
17 | onnx_model.cpp \ | ||
18 | config.cpp \ | ||
19 | |||
20 | RESOURCES += \ | ||
21 | resources.qrc | ||
22 | |||
23 | DISTFILES += \ | ||
24 | qml/oscilloscope/* | ||
25 | |||
26 | target.path = $$[QT_INSTALL_EXAMPLES]/charts/oscilloscope | ||
27 | INSTALLS += target | ||
28 | TARGET = RnnPdmAnomalyDetection | ||
diff --git a/psensors.cpp b/psensors.cpp index 8b333c9..004dfdb 100644 --- a/psensors.cpp +++ b/psensors.cpp | |||
@@ -18,14 +18,13 @@ static int rd_data_cnt = 0; | |||
18 | static int wr_data_cnt = 0; | 18 | static int wr_data_cnt = 0; |
19 | static float data_ready_to_use[MAX_BUFFER]; | 19 | static float data_ready_to_use[MAX_BUFFER]; |
20 | 20 | ||
21 | int uart_get_data(int samples_count, int overlap_mode, float *samples_array) | 21 | int uart_get_data(int samples_count, float *samples_array) |
22 | { | 22 | { |
23 | int rd_idx; | 23 | int rd_idx; |
24 | #ifdef VERBOSE | 24 | #ifdef VERBOSE |
25 | printf ("\nwr=%d rd=%d cnt=%d ", wr_data_cnt, rd_data_cnt, samples_count); | 25 | printf ("\nwr=%d rd=%d cnt=%d ", wr_data_cnt, rd_data_cnt, samples_count); |
26 | printf ("\noverlap_mode=%d ", overlap_mode); | ||
27 | #endif | 26 | #endif |
28 | if(wr_data_cnt >= (rd_data_cnt + samples_count)) | 27 | if(wr_data_cnt >= (rd_data_cnt + samples_count*3)) |
29 | { | 28 | { |
30 | for (int i = 0; i < samples_count; i ++) | 29 | for (int i = 0; i < samples_count; i ++) |
31 | { | 30 | { |
@@ -124,6 +123,13 @@ const char * myfifo = "/tmp/myfifo"; | |||
124 | phase1 = (float)int_phase1 / 32767.0; | 123 | phase1 = (float)int_phase1 / 32767.0; |
125 | phase2 = (float)int_phase2 / 32767.0; | 124 | phase2 = (float)int_phase2 / 32767.0; |
126 | position = (float)val_position / 33554432.0f; | 125 | position = (float)val_position / 33554432.0f; |
126 | |||
127 | /* throttling when there is no incoming sensor data */ | ||
128 | if(phase1 == 0) { | ||
129 | std::this_thread::sleep_for(std::chrono::milliseconds(20)); | ||
130 | continue; | ||
131 | } | ||
132 | |||
127 | if(log_filename) fprintf(fout, "%8.5f %8.5f %8.5f\n", phase1, phase2, position); | 133 | if(log_filename) fprintf(fout, "%8.5f %8.5f %8.5f\n", phase1, phase2, position); |
128 | #ifdef VERBOSE | 134 | #ifdef VERBOSE |
129 | printf("... %8.5f %8.5f %8.5f", phase1, phase2, position); | 135 | printf("... %8.5f %8.5f %8.5f", phase1, phase2, position); |
diff --git a/qml/oscilloscope/ControlPanel.qml b/qml/oscilloscope/ControlPanel.qml new file mode 100644 index 0000000..c9554de --- /dev/null +++ b/qml/oscilloscope/ControlPanel.qml | |||
@@ -0,0 +1,17 @@ | |||
1 | import QtQuick 2.1 | ||
2 | import QtQuick.Layouts 1.0 | ||
3 | |||
4 | ColumnLayout { | ||
5 | property alias imageDisplayButton: imageDisplayButton | ||
6 | spacing: 8 | ||
7 | Layout.fillHeight: true | ||
8 | signal imageDisplayChanged(bool enabled) | ||
9 | |||
10 | MultiButton { | ||
11 | id: imageDisplayButton | ||
12 | text: " Setup Image and Diagram" | ||
13 | items: ["Show", "Hide"] | ||
14 | currentSelection: 0 | ||
15 | onSelectionChanged: imageDisplayChanged(currentSelection == 1); | ||
16 | } | ||
17 | } | ||
diff --git a/qml/oscilloscope/MultiButton.qml b/qml/oscilloscope/MultiButton.qml new file mode 100644 index 0000000..b06f319 --- /dev/null +++ b/qml/oscilloscope/MultiButton.qml | |||
@@ -0,0 +1,40 @@ | |||
1 | import QtQuick 2.0 | ||
2 | import QtQuick.Controls 1.0 | ||
3 | import QtQuick.Controls.Styles 1.0 | ||
4 | |||
5 | Item { | ||
6 | id: button | ||
7 | |||
8 | property string text: "Option: " | ||
9 | property variant items: ["first"] | ||
10 | property int currentSelection: 0 | ||
11 | signal selectionChanged(variant selection) | ||
12 | |||
13 | signal clicked | ||
14 | |||
15 | implicitWidth: buttonText.implicitWidth + 5 | ||
16 | implicitHeight: buttonText.implicitHeight + 10 | ||
17 | |||
18 | Button { | ||
19 | id: buttonText | ||
20 | width: parent.width | ||
21 | height: parent.height | ||
22 | |||
23 | style: ButtonStyle { | ||
24 | label: Component { | ||
25 | Text { | ||
26 | text: button.items[currentSelection] + button.text | ||
27 | clip: true | ||
28 | wrapMode: Text.WordWrap | ||
29 | verticalAlignment: Text.AlignVCenter | ||
30 | horizontalAlignment: Text.AlignHCenter | ||
31 | anchors.fill: parent | ||
32 | } | ||
33 | } | ||
34 | } | ||
35 | onClicked: { | ||
36 | currentSelection = (currentSelection + 1) % items.length; | ||
37 | selectionChanged(button.items[currentSelection]); | ||
38 | } | ||
39 | } | ||
40 | } | ||
diff --git a/qml/oscilloscope/PdmView.qml b/qml/oscilloscope/PdmView.qml new file mode 100644 index 0000000..dfbe9ec --- /dev/null +++ b/qml/oscilloscope/PdmView.qml | |||
@@ -0,0 +1,74 @@ | |||
1 | import QtQuick 2.0 | ||
2 | import QtCharts 2.1 | ||
3 | |||
4 | ChartView { | ||
5 | id: chartViewPdm | ||
6 | animationOptions: ChartView.NoAnimation | ||
7 | theme: ChartView.ChartThemeBlueCerulean | ||
8 | property bool openGL: true | ||
9 | property bool openGLSupported: true | ||
10 | |||
11 | legend.font.pointSize: 16 | ||
12 | |||
13 | ValueAxis { | ||
14 | id: axisY1 | ||
15 | min: 0 | ||
16 | max: 0.003 | ||
17 | } | ||
18 | |||
19 | ValueAxis { | ||
20 | id: axisX | ||
21 | min: 0 | ||
22 | max: 256 | ||
23 | } | ||
24 | |||
25 | LineSeries { | ||
26 | id: lineSeries1Pdm | ||
27 | name: "Prediction Error" | ||
28 | width: 3 | ||
29 | color: "sandybrown" | ||
30 | axisX: axisX | ||
31 | axisY: axisY1 | ||
32 | useOpenGL: chartViewPdm.openGL | ||
33 | } | ||
34 | LineSeries { | ||
35 | id: lineSeries2Pdm | ||
36 | name: "Threshold" | ||
37 | width: 3 | ||
38 | color: "#1e90ff" | ||
39 | axisX: axisX | ||
40 | axisY: axisY1 | ||
41 | useOpenGL: chartViewPdm.openGL | ||
42 | } | ||
43 | LineSeries { | ||
44 | id: lineSeries3Pdm | ||
45 | name: "Anomaly" | ||
46 | width: 3 | ||
47 | color: "#ff0000" | ||
48 | axisX: axisX | ||
49 | axisY: axisY1 | ||
50 | useOpenGL: chartViewPdm.openGL | ||
51 | } | ||
52 | |||
53 | Timer { | ||
54 | id: refreshTimer | ||
55 | interval: 1 / 30 * 1000 // 30 Hz | ||
56 | running: true | ||
57 | repeat: true | ||
58 | onTriggered: { | ||
59 | dataSource.update(3, chartViewPdm.series(0)); | ||
60 | dataSource.update(4, chartViewPdm.series(1)); | ||
61 | dataSource.update(5, chartViewPdm.series(2)); | ||
62 | } | ||
63 | } | ||
64 | |||
65 | function createAxis(min, max) { | ||
66 | // The following creates a ValueAxis object that can be then set as a x or y axis for a series | ||
67 | return Qt.createQmlObject("import QtQuick 2.0; import QtCharts 2.0; ValueAxis { min: " | ||
68 | + min + "; max: " + max + " }", chartViewPdm); | ||
69 | } | ||
70 | |||
71 | function changeRefreshRate(rate) { | ||
72 | refreshTimer.interval = 1 / Number(rate) * 1000; | ||
73 | } | ||
74 | } | ||
diff --git a/qml/oscilloscope/ScopeView.qml b/qml/oscilloscope/ScopeView.qml new file mode 100644 index 0000000..72191d0 --- /dev/null +++ b/qml/oscilloscope/ScopeView.qml | |||
@@ -0,0 +1,81 @@ | |||
1 | import QtQuick 2.0 | ||
2 | import QtCharts 2.1 | ||
3 | |||
4 | ChartView { | ||
5 | id: chartView | ||
6 | animationOptions: ChartView.NoAnimation | ||
7 | theme: ChartView.ChartThemeBlueCerulean | ||
8 | property bool openGL: true | ||
9 | property bool openGLSupported: true | ||
10 | |||
11 | legend.font.pointSize: 16 | ||
12 | |||
13 | ValueAxis { | ||
14 | id: axisY1 | ||
15 | min: -0.12 | ||
16 | max: 0.12 | ||
17 | } | ||
18 | |||
19 | ValueAxis { | ||
20 | id: axisY3 | ||
21 | min: 0 | ||
22 | max: 1.1 | ||
23 | } | ||
24 | |||
25 | ValueAxis { | ||
26 | id: axisX | ||
27 | min: 0 | ||
28 | max: 256 | ||
29 | } | ||
30 | |||
31 | LineSeries { | ||
32 | id: lineSeries1 | ||
33 | name: "Phase Current 1" | ||
34 | color: "darkturquoise" | ||
35 | width: 3 | ||
36 | axisX: axisX | ||
37 | axisY: axisY1 | ||
38 | useOpenGL: chartView.openGL | ||
39 | } | ||
40 | LineSeries { | ||
41 | id: lineSeries2 | ||
42 | name: "Phase Current 2" | ||
43 | color: "forestgreen" | ||
44 | width: 3 | ||
45 | axisX: axisX | ||
46 | axisYRight: axisY1 | ||
47 | useOpenGL: chartView.openGL | ||
48 | } | ||
49 | LineSeries { | ||
50 | id: lineSeries3 | ||
51 | name: "Position" | ||
52 | width: 3 | ||
53 | color: "orange" | ||
54 | axisX: axisX | ||
55 | axisYRight: axisY3 | ||
56 | useOpenGL: chartView.openGL | ||
57 | } | ||
58 | |||
59 | Timer { | ||
60 | id: refreshTimer | ||
61 | interval: 1 / 30 * 1000 // 30 Hz | ||
62 | running: true | ||
63 | repeat: true | ||
64 | onTriggered: { | ||
65 | dataSource.getdata(); | ||
66 | dataSource.update(0, chartView.series(0)); | ||
67 | dataSource.update(1, chartView.series(1)); | ||
68 | dataSource.update(2, chartView.series(2)); | ||
69 | } | ||
70 | } | ||
71 | |||
72 | function createAxis(min, max) { | ||
73 | // The following creates a ValueAxis object that can be then set as a x or y axis for a series | ||
74 | return Qt.createQmlObject("import QtQuick 2.0; import QtCharts 2.0; ValueAxis { min: " | ||
75 | + min + "; max: " + max + " }", chartView); | ||
76 | } | ||
77 | |||
78 | function changeRefreshRate(rate) { | ||
79 | refreshTimer.interval = 1 / Number(rate) * 1000; | ||
80 | } | ||
81 | } | ||
diff --git a/qml/oscilloscope/anomaly-detection.png b/qml/oscilloscope/anomaly-detection.png new file mode 100644 index 0000000..9428300 --- /dev/null +++ b/qml/oscilloscope/anomaly-detection.png | |||
Binary files differ | |||
diff --git a/qml/oscilloscope/main.qml b/qml/oscilloscope/main.qml new file mode 100644 index 0000000..d5972c7 --- /dev/null +++ b/qml/oscilloscope/main.qml | |||
@@ -0,0 +1,142 @@ | |||
1 | import QtQuick 2.0 | ||
2 | |||
3 | Item { | ||
4 | id: main | ||
5 | width: 1920 | ||
6 | height: 1080 | ||
7 | |||
8 | Text { | ||
9 | anchors.top: parent.top | ||
10 | anchors.topMargin: 50 | ||
11 | anchors.left: parent.left | ||
12 | anchors.leftMargin: 50 | ||
13 | text: "Deep Learning based Predictive Maintenance (PdM) on Sitara Edge Devices" | ||
14 | font.family: "Helvetica" | ||
15 | font.pointSize: 36 | ||
16 | font.bold: true | ||
17 | color: "red" | ||
18 | } | ||
19 | |||
20 | Image { | ||
21 | source: "./ti-logo.png" | ||
22 | scale: 0.35 | ||
23 | anchors.bottom: parent.bottom | ||
24 | anchors.bottomMargin: 35 | ||
25 | anchors.right: parent.right | ||
26 | anchors.rightMargin: -80 | ||
27 | } | ||
28 | |||
29 | ControlPanel { | ||
30 | id: controlPanel | ||
31 | anchors.bottom: parent.bottom | ||
32 | anchors.bottomMargin: 75 | ||
33 | anchors.left: parent.left | ||
34 | anchors.leftMargin: 60 | ||
35 | onImageDisplayChanged: { | ||
36 | picmotordrive.visible = (!picmotordrive.visible); | ||
37 | picanomalydetection.visible = (!picanomalydetection.visible); | ||
38 | picsystemmodel.visible = (!picsystemmodel.visible); | ||
39 | } | ||
40 | } | ||
41 | |||
42 | ScopeView { | ||
43 | id: scopeView | ||
44 | anchors.top: parent.top | ||
45 | anchors.topMargin: 200 | ||
46 | anchors.bottom: parent.bottom | ||
47 | anchors.bottomMargin: 100 | ||
48 | anchors.left: parent.left | ||
49 | anchors.leftMargin: 50 | ||
50 | anchors.right: parent.right | ||
51 | anchors.rightMargin: 950 | ||
52 | } | ||
53 | |||
54 | Image { | ||
55 | id: picmotordrive | ||
56 | source: "./motor-drive.png" | ||
57 | scale: 0.83 | ||
58 | visible: false | ||
59 | anchors.top: scopeView.top | ||
60 | anchors.topMargin: 80 | ||
61 | anchors.right: scopeView.right | ||
62 | anchors.rightMargin: 200 | ||
63 | } | ||
64 | |||
65 | |||
66 | PdmView { | ||
67 | id: pdmView | ||
68 | anchors.top: parent.top | ||
69 | anchors.topMargin: 200 | ||
70 | anchors.bottom: parent.bottom | ||
71 | anchors.bottomMargin: 100 | ||
72 | anchors.left: scopeView.right | ||
73 | anchors.right: parent.right | ||
74 | anchors.rightMargin: 50 | ||
75 | } | ||
76 | |||
77 | Image { | ||
78 | id: picanomalydetection | ||
79 | source: "./anomaly-detection.png" | ||
80 | scale: 0.85 | ||
81 | visible: false | ||
82 | anchors.top: pdmView.top | ||
83 | anchors.topMargin: 70 | ||
84 | anchors.right: pdmView.right | ||
85 | anchors.rightMargin: 370 | ||
86 | } | ||
87 | |||
88 | Image { | ||
89 | id: picsystemmodel | ||
90 | source: "./system-model.png" | ||
91 | scale: 0.8 | ||
92 | visible: false | ||
93 | anchors.top: pdmView.top | ||
94 | anchors.topMargin:160 | ||
95 | anchors.right: pdmView.right | ||
96 | anchors.rightMargin: 15 | ||
97 | } | ||
98 | |||
99 | Text { | ||
100 | text: "Real-time Sensor Input" | ||
101 | font.family: "Helvetica" | ||
102 | font.pointSize: 22 | ||
103 | color: "red" | ||
104 | anchors.top: parent.top | ||
105 | anchors.topMargin: 150 | ||
106 | anchors.left: parent.left | ||
107 | anchors.leftMargin: 280 | ||
108 | } | ||
109 | |||
110 | Text { | ||
111 | text: "Real-time Anomaly Detection" | ||
112 | font.family: "Helvetica" | ||
113 | font.pointSize: 22 | ||
114 | color: "red" | ||
115 | anchors.top: parent.top | ||
116 | anchors.topMargin: 150 | ||
117 | anchors.right: parent.right | ||
118 | anchors.rightMargin: 280 | ||
119 | } | ||
120 | |||
121 | Text { | ||
122 | text: dataSource.pdmStage | ||
123 | font.family: "Helvetica" | ||
124 | font.pointSize: 22 | ||
125 | color: "green" | ||
126 | anchors.top: parent.top | ||
127 | anchors.topMargin: 650 | ||
128 | anchors.right: parent.right | ||
129 | anchors.rightMargin: 460 | ||
130 | } | ||
131 | |||
132 | Text { | ||
133 | text: dataSource.anomalyDetection | ||
134 | font.family: "Helvetica" | ||
135 | font.pointSize: 24 | ||
136 | color: "red" | ||
137 | anchors.top: parent.top | ||
138 | anchors.topMargin: 650 | ||
139 | anchors.right: parent.right | ||
140 | anchors.rightMargin: 125 | ||
141 | } | ||
142 | } | ||
diff --git a/qml/oscilloscope/motor-drive.png b/qml/oscilloscope/motor-drive.png new file mode 100644 index 0000000..13f6393 --- /dev/null +++ b/qml/oscilloscope/motor-drive.png | |||
Binary files differ | |||
diff --git a/qml/oscilloscope/system-model.png b/qml/oscilloscope/system-model.png new file mode 100644 index 0000000..983827f --- /dev/null +++ b/qml/oscilloscope/system-model.png | |||
Binary files differ | |||
diff --git a/qml/oscilloscope/ti-logo.png b/qml/oscilloscope/ti-logo.png new file mode 100644 index 0000000..9763773 --- /dev/null +++ b/qml/oscilloscope/ti-logo.png | |||
Binary files differ | |||
@@ -1,17 +1,20 @@ | |||
1 | ============================== | 1 | ============================== |
2 | Native compilation on target | 2 | Cross compilation on Ubunut |
3 | ============================== | 3 | ============================== |
4 | Copy the folder pdm-anomaly-detection to target, and then run "./mk.sh" | 4 | With Qt build environment and qtcharts available, run "qmake", and then "make". |
5 | After the compilation is completed successfully, motpdm binary will be created. | ||
6 | 5 | ||
7 | 6 | ||
8 | ============================== | 7 | ============================== |
9 | Run motpdm on target | 8 | Run RnnPdmAnomalyDetection on target |
10 | ============================== | 9 | ============================== |
11 | Terminal 1: ./run.sh > log.txt | 10 | Stop Matrix GUI if needed: /etc/init.d/matrix-gui stop |
12 | Terminal 2: cat ./logs/normal45-270-v100-with-friction2-iter10-15.log > /tmp/myfifo | ||
13 | 11 | ||
14 | After the cat command in Terminal 2 is done, stop the process (CTRL-C) in Terminal 1. | 12 | Terminal 1: ./RnnPdmAnomalyDetection -0.0001841035315 0.004191935886 0.01918238488 0.01933241767 1.5 |
13 | This will open a Qt GUI for the demo. | ||
15 | 14 | ||
16 | Then, check reports in log.txt, with time units being in 20ms ticks (decimated sample | 15 | Terminal 2: direct sensor data input to /tmp/myfifo, e.g., |
17 | rate of 50Hz). AD (Anomaly detection) is reported along with the sample counter. | 16 | cat ./logs/normal45-270-v100-with-friction2-iter10-15.log > /tmp/myfifo |
17 | cat ./logs/normal100-anomaly150-normal100.log > /tmp/myfifo | ||
18 | cat ./logs/normal270-anomaly170-normal270.log > /tmp/myfifo | ||
19 | |||
20 | View the sensor data and anomaly detection results from the Qt GUI. | ||
diff --git a/resources.qrc b/resources.qrc new file mode 100644 index 0000000..4a7ebaf --- /dev/null +++ b/resources.qrc | |||
@@ -0,0 +1,13 @@ | |||
1 | <RCC> | ||
2 | <qresource prefix="/"> | ||
3 | <file>qml/oscilloscope/main.qml</file> | ||
4 | <file>qml/oscilloscope/ControlPanel.qml</file> | ||
5 | <file>qml/oscilloscope/ScopeView.qml</file> | ||
6 | <file>qml/oscilloscope/PdmView.qml</file> | ||
7 | <file>qml/oscilloscope/MultiButton.qml</file> | ||
8 | <file>qml/oscilloscope/ti-logo.png</file> | ||
9 | <file>qml/oscilloscope/motor-drive.png</file> | ||
10 | <file>qml/oscilloscope/anomaly-detection.png</file> | ||
11 | <file>qml/oscilloscope/system-model.png</file> | ||
12 | </qresource> | ||
13 | </RCC> | ||
@@ -1 +0,0 @@ | |||
1 | ./motpdm -0.0001841035315 0.004191935886 0.01918238488 0.01933241767 | ||