+
+
+
+
+
+ Accelerometer Data (m/s2)
+ Live visualization of raw accelerometer data (X, Y, Z axes) from the board's Movement module. Horizontal axis represents time; vertical axis shows acceleration in m/s2
+
+
+
+
+
+
+
+ X
+
+
+
+ Y
+
+
+
+ Z
+
+
+
+
+
+ No data
+
+
-#include
+#include
// Create a ModulinoMovement object
ModulinoMovement movement;
diff --git a/examples/vibration-anomaly-detection/sketch/sketch.yaml b/examples/vibration-anomaly-detection/sketch/sketch.yaml
index fc88d11..a3d6ef3 100644
--- a/examples/vibration-anomaly-detection/sketch/sketch.yaml
+++ b/examples/vibration-anomaly-detection/sketch/sketch.yaml
@@ -8,7 +8,7 @@ profiles:
- DebugLog (0.8.4)
- ArxContainer (0.7.0)
- ArxTypeTraits (0.3.1)
- - Modulino (0.5.0)
+ - Arduino_Modulino (0.6.1)
- Arduino_HS300x (1.0.0)
- Arduino_LPS22HB (1.0.2)
- Arduino_LSM6DSOX (1.1.2)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ It quantifies how far the current vibration is from the model's 'normal' clusters.
+ + Scores above your threshold are flagged as an anomaly.
+ + + Scores above your threshold are flagged as an anomaly.
+
-
+
+ Feedback
+ +
-
-
diff --git a/examples/vibration-anomaly-detection/assets/style.css b/examples/vibration-anomaly-detection/assets/style.css
index 9d37cdc..761b1f8 100644
--- a/examples/vibration-anomaly-detection/assets/style.css
+++ b/examples/vibration-anomaly-detection/assets/style.css
@@ -4,20 +4,22 @@
* SPDX-License-Identifier: MPL-2.0
*/
-@import url("fonts/roboto-mono.css");
+@import url("fonts/fonts.css");
/*
* This CSS is used to center the various elements on the screen
*/
* {
+ box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
- background-color: #ECF1F1;
- color: #2C353A;
+ background-color: #DAE3E3;
+ line-height: 1.6;
+ color: #343a40;
padding: 24px 40px;
}
@@ -25,18 +27,19 @@ body {
display: flex;
justify-content: space-between;
align-items: center;
- margin-bottom: 32px;
+ margin-bottom: 24px;
+ padding: 12px 0;
}
.arduino-text {
color: #008184;
font-family: "Roboto Mono", monospace;
font-size: 20px;
- font-weight: 600;
+ font-weight: 700;
margin: 0;
font-style: normal;
line-height: 170%;
- letter-spacing: 0.28px;
+ letter-spacing: 2.4px;
}
.arduino-logo {
@@ -44,84 +47,431 @@ body {
width: auto;
}
-.container {
- text-align: center;
+.main-content {
+ display: flex;
+ gap: 30px;
+ align-items: flex-start;
}
/*
- * LED Button styling
+ * Styles for specific components required by Anomaly Detection
*/
-.led-container {
+
+.legend {
display: flex;
- justify-content: center;
- margin-bottom: 32px;
- padding-top: 40px;
+ gap: 16px;
+ margin-top: 8px;
+ font-size: 14px;
}
-#fan-led {
- width: 128px;
- height: 128px;
- border-radius: 50%;
- cursor: pointer;
- transition: all 0.3s ease;
+.legend-item {
display: flex;
align-items: center;
- justify-content: center;
- font-family: inherit;
- font-weight: 600;
- font-size: 14px;
- text-align: center;
- line-height: 1.2;
- outline: none;
- position: relative;
- border: 2px solid #C9D2D2;
+ gap: 6px;
}
-#fan-led.led-off {
- background: #008184;
- color: #ffffff;
- box-shadow: 0 0 20px #008184, 0 0 40px #008184, 0 0 60px #008184;
- border-color: #008184;
+.legend-color {
+ width: 12px;
+ height: 4px;
+ border-radius: 1px;
}
-#fan-led.led-on {
- background: #e00d0d;
- color: #ffffff;
- box-shadow: 0 0 20px #e00d0d, 0 0 40px #e00d0d, 0 0 60px #e00d0d;
- border-color: #e00d0d;
+.controls-section-right {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ width: 100%;
}
-#fan-led:hover {
- transform: scale(1.05);
+.box-title {
+ color: #2C353A;
+ font-family: "Roboto Mono";
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 700;
+ line-height: 170%;
+ letter-spacing: 1.2px;
+ margin-bottom: 16px;
}
-#fan-led:active {
- transform: scale(0.95);
+.controls-section-left {
+ background: #ECF1F1;
+ padding: 16px;
+ border-radius: 8px;
+ min-width: 750px;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
}
-.instruction-text {
- font-size: 14px;
- font-style: normal;
- font-weight: 400;
- line-height: 160%;
- letter-spacing: 0.12px;
+.right-column {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ width: 100%;
+ max-width: 550px;
+}
+
+.right-column .container:last-child {
+ flex-grow: 1;
+}
+
+.container-right {
+ background: #ECF1F1;
+ padding: 16px;
+ border-radius: 8px;
+}
+
+.error-message {
+ margin-top: 20px;
+ padding: 10px;
+ border-radius: 5px;
+ background-color: #f8d7da;
+ color: #721c24;
+ border: 1px solid #f5c6cb;
+}
+
+.recent-scans-title-container {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ justify-content: space-between;
+}
+
+.recent-scans-title {
color: #2C353A;
+ font-family: "Roboto Mono", monospace;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 700;
+ line-height: 170%;
+ letter-spacing: 1.2px;
+ margin: 0;
+}
+
+#recentClassifications {
+ list-style-type: none;
+ padding: 0;
+ flex: 1;
}
/*
* Responsive design
*/
@media (max-width: 768px) {
- body {
- padding: 12px 20px;
+ .main-content {
+ flex-direction: column;
+ }
+
+ .right-column {
+ max-width: 100%;
}
.arduino-text {
font-size: 14px;
}
+ .container {
+ padding: 15px;
+ }
+
+ .controls-section-left {
+ min-width: 330px;
+ }
+
.arduino-logo {
- height: 20px;
+ height: 16px;
width: auto;
}
-}
\ No newline at end of file
+
+}
+
+@media (max-width: 1024px) and (min-width: 769px) {
+ .controls-section-left {
+ min-width: 490px;
+ }
+}
+
+@media (max-width: 480px) {
+ .controls-section-left {
+ min-width: 170px;
+ }
+}
+
+.info-btn {
+ width: 14px;
+ height: 14px;
+ cursor: pointer;
+ border-radius: 50%;
+ background-color: #C9D2D2;
+ padding: 2px;
+ transition: background 0.2s;
+ position: relative;
+}
+
+.popover {
+ position: absolute;
+ left: 5%;
+ top: 70%;
+ margin-left: 8px;
+ display: none;
+ background: #fff;
+ padding: 16px 24px;
+ border-radius: 6px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ z-index: 10;
+ width: 300px;
+ color: #2C353A;
+ font-weight: 100;
+ font-family: "Open Sans";
+ font-size: 12px;
+ line-height: 170%;
+ letter-spacing: 0.12px;
+}
+
+.popover.active {
+ display: block;
+}
+
+.no-recent-anomalies {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ color: #5D6A6B;
+ gap: 8px;
+ margin: auto;
+}
+
+.no-recent-anomalies p {
+ font-family: "Open Sans";
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%; /* 19.2px */
+ letter-spacing: 0.12px;
+}
+
+.feedback-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ min-height: 64px;
+}
+
+.feedback-text {
+ font-family: "Open Sans";
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%; /* 19.2px */
+ letter-spacing: 0.12px;
+}
+
+.no-data-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ color: #5D6A6B;
+ gap: 8px;
+ margin: auto;
+}
+
+.no-data-placeholder p {
+ font-family: "Open Sans";
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%; /* 19.2px */
+ letter-spacing: 0.12px;
+}
+
+.control-group {
+ position: relative;
+}
+
+.slider-box {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.control-confidence {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 16px;
+}
+
+#confidenceSlider {
+ width: 100%;
+ height: 6px;
+ border-radius: 3px;
+ background: #DAE3E3;
+ outline: none;
+ -webkit-appearance: none;
+ appearance: none;
+ position: relative;
+ margin: 20px 0 10px 0;
+}
+
+#confidenceSlider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: #008184;
+ cursor: pointer;
+ position: relative;
+ bottom: 3px;
+ z-index: 2;
+}
+
+#confidenceSlider::-webkit-slider-runnable-track {
+ width: 100%;
+ height: 6px;
+ border-radius: 3px;
+ background: #DAE3E3;
+}
+
+#confidenceSlider::-moz-range-track {
+ width: 100%;
+ height: 6px;
+ border-radius: 3px;
+ background: #DAE3E3;
+ border: none;
+}
+
+.slider-container {
+ position: relative;
+ width: 100%;
+}
+
+.slider-progress {
+ position: absolute;
+ top: 20px;
+ left: 0;
+ height: 6px;
+ background: #008184;
+ border-radius: 3px;
+ pointer-events: none;
+ z-index: 1;
+ transition: width 0.1s ease;
+}
+
+.confidence-value-display {
+ position: absolute;
+ top: -3px;
+ transform: translateX(-50%);
+ color: #008184;
+ padding: 2px 6px;
+ pointer-events: none;
+ z-index: 3;
+ white-space: nowrap;
+ transition: left 0.1s ease;
+ font-family: "Open Sans";
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%;
+ letter-spacing: 0.12px;
+}
+
+.confidence-limits {
+ color: #2C353A;
+ font-family: "Open Sans";
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 160%;
+ letter-spacing: 0.12px;
+ margin-top: 10px;
+}
+
+.btn-tertiary {
+ border-radius: 6px;
+ border: 1px solid #C9D2D2;
+ background: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ padding: 4px 8px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-size: 12px;
+ min-width: 50px;
+ height: 36px;
+}
+
+.confidence-input {
+ border: none;
+ background: transparent;
+ font-size: 12px;
+ font-weight: inherit;
+ color: inherit;
+ text-align: center;
+ width: 40px;
+ padding: 0;
+ margin: 0;
+ outline: none;
+ cursor: text;
+}
+
+.confidence-input:focus {
+ background: rgba(0, 129, 132, 0.1);
+ border-radius: 2px;
+}
+
+.reset-icon {
+ width: 18px;
+ height: 18px;
+ opacity: 0.7;
+ transition: opacity 0.3s ease;
+ cursor: pointer;
+}
+
+.reset-icon svg path {
+ fill: black;
+}
+
+.btn-tertiary:hover .reset-icon {
+ opacity: 1;
+}
+
+.anomaly-list-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 0;
+ border-bottom: 1px solid #DAE3E3;
+}
+.anomaly-score {
+ font-weight: normal;
+ color: #2C353A;
+ font-size: 12px;
+}
+.anomaly-text {
+ color: #2C353A;
+ font-size: 12px;
+}
+.anomaly-time {
+ color: #2C353A;
+ font-size: 12px;
+}
+#accelerometer-data-display {
+ margin-top: 20px;
+}
+
+#plot {
+ width: 100%;
+ height: 250px;
+}
diff --git a/examples/vibration-anomaly-detection/python/main.py b/examples/vibration-anomaly-detection/python/main.py
index f79b877..4c41a85 100644
--- a/examples/vibration-anomaly-detection/python/main.py
+++ b/examples/vibration-anomaly-detection/python/main.py
@@ -2,52 +2,53 @@
#
# SPDX-License-Identifier: MPL-2.0
+import json
+from datetime import datetime
from arduino.app_utils import *
from arduino.app_bricks.web_ui import WebUI
from arduino.app_bricks.vibration_anomaly_detection import VibrationAnomalyDetection
logger = Logger("vibration-detector")
-ui = WebUI()
-
vibration_detection = VibrationAnomalyDetection(anomaly_detection_threshold=1.0)
+def on_override_th(value: float):
+ logger.info(f"Setting new anomaly threshold: {value}")
+ vibration_detection.anomaly_detection_threshold = value
+
+ui = WebUI()
+ui.on_message("override_th", lambda sid, threshold: on_override_th(threshold))
+
def get_fan_status(anomaly_detected: bool):
return {
"anomaly": anomaly_detected,
"status_text": "Anomaly detected!" if anomaly_detected else "No anomaly"
}
-
# Register action to take after successful detection
def on_detected_anomaly(anomaly_score: float, classification: dict):
- print(f"Detected anomaly. Score: {anomaly_score}")
+ anomaly_payload = {
+ "score": anomaly_score,
+ "timestamp": datetime.now().isoformat()
+ }
+ ui.send_message('anomaly_detected', json.dumps(anomaly_payload))
ui.send_message('fan_status_update', get_fan_status(True))
vibration_detection.on_anomaly(on_detected_anomaly)
def record_sensor_movement(x: float, y: float, z: float):
- logger.debug(f"record_sensor_movement called with raw g-values: x={x}, y={y}, z={z}")
- try:
- # Convert g -> m/s^2 for the detector
- x_ms2 = x * 9.81
- y_ms2 = y * 9.81
- z_ms2 = z * 9.81
+ # Convert g -> m/s^2 for the detector
+ x_ms2 = x * 9.81
+ y_ms2 = y * 9.81
+ z_ms2 = z * 9.81
- # Forward samples to the vibration_detection brick
- vibration_detection.accumulate_samples((x_ms2, y_ms2, z_ms2))
+ # Forward raw data to UI for plotting
+ ui.send_message('sample', {'x': x_ms2, 'y': y_ms2, 'z': z_ms2})
- except Exception as e:
- logger.exception(f"record_sensor_movement: Error: {e}")
- print(f"record_sensor_movement: Error: {e}")
+ # Forward samples to the vibration_detection brick
+ vibration_detection.accumulate_samples((x_ms2, y_ms2, z_ms2))
# Register the Bridge RPC provider so the sketch can call into Python
-try:
- logger.debug("Registering 'record_sensor_movement' Bridge provider")
- Bridge.provide("record_sensor_movement", record_sensor_movement)
- logger.debug("'record_sensor_movement' registered successfully")
-except RuntimeError:
- logger.debug("'record_sensor_movement' already registered")
-
-# Let the App runtime manage bricks and run the web server
+Bridge.provide("record_sensor_movement", record_sensor_movement)
+
App.run()
diff --git a/examples/vibration-anomaly-detection/sketch/sketch.ino b/examples/vibration-anomaly-detection/sketch/sketch.ino
index 47d24b5..a65cc2c 100644
--- a/examples/vibration-anomaly-detection/sketch/sketch.ino
+++ b/examples/vibration-anomaly-detection/sketch/sketch.ino
@@ -3,7 +3,7 @@
// SPDX-License-Identifier: MPL-2.0
#include
-
-
+
-
- No anomaly
+
+
+
+
+