Modulino Vibro + UNO Q - Haptic Tester
Trigger vibration pulses at any power level and duration straight from the UNO Q.
Devices & Components
1
Arduino® UNO™ Q 2GB
1
Modulino™ Vibro
Software & Tools
Arduino App Lab
Project description
Code
app
js
JS file
1var socket = io(); 2 3var statusEl = document.getElementById("status"); 4var durationSlider = document.getElementById("duration-slider"); 5var durationValue = document.getElementById("duration-value"); 6var buzzBtn = document.getElementById("buzz-btn"); 7var stopBtn = document.getElementById("stop-btn"); 8var lastTriggered = document.getElementById("last-triggered"); 9var powerBtns = document.querySelectorAll(".power-btn"); 10 11var POWER_LABELS = { 12 25: "Gentle", 13 30: "Moderate", 14 35: "Medium", 15 40: "Intense", 16 45: "Powerful", 17 50: "Maximum" 18}; 19 20var selectedPower = 50; 21 22// Connection status 23socket.on("connect", function () { statusEl.className = "status connected"; statusEl.textContent = "● Connected"; }); 24socket.on("disconnect", function () { statusEl.className = "status disconnected"; statusEl.textContent = "● Disconnected"; }); 25socket.on("connect_error", function () { statusEl.className = "status connecting"; statusEl.textContent = "Connecting…"; }); 26 27// State from server 28socket.on("state_update", function (data) { 29 if (data) applyState(data); 30}); 31 32function applyState(state) { 33 // Sync duration slider 34 durationSlider.value = state.last_duration_ms; 35 durationValue.textContent = state.last_duration_ms + " ms"; 36 37 // Sync power buttons 38 selectedPower = state.last_power; 39 powerBtns.forEach(function (btn) { 40 btn.classList.toggle("active", parseInt(btn.dataset.power) === selectedPower); 41 }); 42 43 // Update last triggered readout 44 var label = POWER_LABELS[state.last_power] || state.last_power; 45 lastTriggered.textContent = state.last_duration_ms + " ms — " + label; 46} 47 48// Duration slider live readout (local only, no emit) 49durationSlider.addEventListener("input", function () { 50 durationValue.textContent = durationSlider.value + " ms"; 51}); 52 53// Power level selection 54powerBtns.forEach(function (btn) { 55 btn.addEventListener("click", function () { 56 selectedPower = parseInt(btn.dataset.power); 57 powerBtns.forEach(function (b) { b.classList.remove("active"); }); 58 btn.classList.add("active"); 59 }); 60}); 61 62// Buzz 63buzzBtn.addEventListener("click", function () { 64 socket.emit("buzz", { 65 duration_ms: parseInt(durationSlider.value), 66 power_level: selectedPower 67 }); 68}); 69 70// Stop 71stopBtn.addEventListener("click", function () { 72 socket.emit("stop", {}); 73});
styles
css
Style file
1:root { 2 --bg: #0f1117; 3 --surface: #1c1f2a; 4 --border: #2e3244; 5 --accent: #4fa3e0; 6 --text: #e2e8f0; 7 --muted: #8892a4; 8 --green: #22c55e; 9 --red: #ef4444; 10 --radius: 10px; 11} 12 13* { box-sizing: border-box; margin: 0; padding: 0; } 14 15body { 16 font-family: 'Segoe UI', system-ui, sans-serif; 17 background: var(--bg); 18 color: var(--text); 19 min-height: 100vh; 20 padding: 1rem; 21} 22 23header { 24 display: flex; 25 flex-direction: column; 26 align-items: center; 27 gap: 0.5rem; 28 margin-bottom: 1.5rem; 29 text-align: center; 30} 31 32header h1 { font-size: 1.35rem; letter-spacing: -0.02em; } 33 34.status { 35 font-size: 0.8rem; 36 font-weight: 600; 37 padding: 0.25rem 0.75rem; 38 border-radius: 999px; 39 letter-spacing: 0.04em; 40 text-transform: uppercase; 41} 42 43.status.connected { background: var(--green); color: #fff; } 44.status.disconnected { background: var(--red); color: #fff; } 45.status.connecting { background: var(--muted); color: #fff; } 46 47main { display: flex; flex-direction: column; gap: 1.25rem; } 48 49.card { 50 background: var(--surface); 51 border: 1px solid var(--border); 52 border-radius: var(--radius); 53 padding: 1.25rem 1.5rem; 54} 55 56.card h2 { 57 font-size: 0.95rem; 58 font-weight: 600; 59 color: var(--muted); 60 text-transform: uppercase; 61 letter-spacing: 0.06em; 62 margin-bottom: 1rem; 63} 64 65/* Power level grid */ 66.power-grid { 67 display: grid; 68 grid-template-columns: repeat(3, 1fr); 69 gap: 0.6rem; 70} 71 72/* Buttons */ 73.btn { 74 background: var(--surface); 75 color: var(--text); 76 border: 1px solid var(--border); 77 border-radius: 8px; 78 padding: 0.6rem 1.2rem; 79 font-size: 0.9rem; 80 font-weight: 600; 81 cursor: pointer; 82 transition: opacity 0.15s, background 0.15s, border-color 0.15s; 83} 84 85.btn:hover { opacity: 0.85; } 86.btn:active { opacity: 0.7; } 87.btn:disabled { opacity: 0.4; cursor: not-allowed; } 88 89.btn.active { 90 background: var(--accent); 91 border-color: var(--accent); 92 color: #fff; 93} 94 95.btn.danger { 96 background: var(--red); 97 border-color: var(--red); 98 color: #fff; 99} 100 101/* Actions card */ 102.actions-card { 103 display: flex; 104 gap: 1rem; 105} 106 107.actions-card .btn { 108 flex: 1; 109 font-size: 1rem; 110 padding: 0.75rem; 111} 112 113/* Buzz button */ 114#buzz-btn { 115 background: var(--accent); 116 border-color: var(--accent); 117 color: #fff; 118} 119 120/* Duration slider */ 121.brightness-row { display: flex; align-items: center; gap: 1rem; } 122 123#duration-slider { 124 flex: 1; 125 accent-color: var(--accent); 126 height: 6px; 127 cursor: pointer; 128} 129 130#duration-value { 131 width: 5rem; 132 text-align: right; 133 font-variant-numeric: tabular-nums; 134 font-size: 1rem; 135 white-space: nowrap; 136} 137 138/* Last triggered */ 139.last-triggered { 140 font-size: 1rem; 141 color: var(--text); 142} 143 144@media (min-width: 600px) { 145 main { max-width: 700px; margin: 0 auto; } 146}
main
python
python main
1import json 2import os 3 4from arduino.app_utils import App, Bridge 5from arduino.app_bricks.web_ui import WebUI 6 7_ui_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "ui") 8ui = WebUI(assets_dir_path=_ui_dir) 9 10state = {"last_duration_ms": 500, "last_power": 50} 11 12POWER_LABELS = { 13 25: "Gentle", 14 30: "Moderate", 15 35: "Medium", 16 40: "Intense", 17 45: "Powerful", 18 50: "Maximum", 19} 20 21 22def _call_mcu(method, *args): 23 try: 24 return json.loads(Bridge.call(method, *args)) 25 except Exception as exc: 26 print(f"[bridge] {method} error: {exc}") 27 return None 28 29 30def _broadcast(new_state, room=None): 31 if new_state and "error" not in new_state: 32 state.update(new_state) 33 ui.send_message("state_update", state, room=room) 34 35 36def on_connect(sid): 37 _broadcast(_call_mcu("get_state"), room=sid) 38 39 40def on_buzz(sid, data): 41 result = _call_mcu("buzz", data["duration_ms"], data["power_level"]) 42 _broadcast(result) 43 44 45def on_stop(sid, data): 46 result = _call_mcu("stop_buzz") 47 _broadcast(result) 48 49 50ui.on_connect(on_connect) 51ui.on_message("buzz", on_buzz) 52ui.on_message("stop", on_stop) 53 54App.run()
index
markup
HTML file
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 <title>Modulino Vibro – Haptic Tester</title> 7 <link rel="stylesheet" href="styles.css" /> 8</head> 9<body> 10 <header> 11 <h1>📳 Modulino Vibro – Haptic Tester</h1> 12 <span id="status" class="status connecting">Connecting…</span> 13 </header> 14 15 <main> 16 <section class="card"> 17 <h2>Power Level</h2> 18 <div class="power-grid" id="power-grid"> 19 <button class="btn power-btn" data-power="25">Gentle</button> 20 <button class="btn power-btn" data-power="30">Moderate</button> 21 <button class="btn power-btn" data-power="35">Medium</button> 22 <button class="btn power-btn" data-power="40">Intense</button> 23 <button class="btn power-btn" data-power="45">Powerful</button> 24 <button class="btn power-btn active" data-power="50">Maximum</button> 25 </div> 26 </section> 27 28 <section class="card"> 29 <h2>Duration</h2> 30 <div class="brightness-row"> 31 <input type="range" id="duration-slider" min="50" max="2000" step="50" value="500" /> 32 <span id="duration-value">500 ms</span> 33 </div> 34 </section> 35 36 <section class="card actions-card"> 37 <button class="btn" id="buzz-btn">⚡ Buzz!</button> 38 <button class="btn danger" id="stop-btn">■ Stop</button> 39 </section> 40 41 <section class="card"> 42 <h2>Last Triggered</h2> 43 <p id="last-triggered" class="last-triggered">—</p> 44 </section> 45 </main> 46 47 <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script> 48 <script src="app.js"></script> 49</body> 50</html>
sketch
cpp
Arduino sketch
1#include <Arduino_RouterBridge.h> 2#include <Arduino_Modulino.h> 3 4ModulinoVibro vibro; 5 6int last_duration_ms = 500; 7int last_power = 50; 8 9String buildState() { 10 return "{\"last_duration_ms\": " + String(last_duration_ms) + 11 ", \"last_power\": " + String(last_power) + "}"; 12} 13 14String rpc_get_state() { 15 return buildState(); 16} 17 18String rpc_buzz(int duration_ms, int power_level) { 19 last_duration_ms = constrain(duration_ms, 50, 2000); 20 last_power = constrain(power_level, 25, 50); 21 vibro.on((size_t)last_duration_ms, (VibroPowerLevel)last_power); 22 return buildState(); 23} 24 25String rpc_stop_buzz() { 26 vibro.off(); 27 return buildState(); 28} 29 30void setup() { 31 Bridge.begin(); 32 Modulino.begin(); 33 vibro.begin(); 34 35 Bridge.provide("get_state", rpc_get_state); 36 Bridge.provide("buzz", rpc_buzz); 37 Bridge.provide("stop_buzz", rpc_stop_buzz); 38} 39 40void loop() { 41 // No polling needed — all actions are RPC-driven 42}
Arduino App Lab
Modulino Vibro – Haptic Tester
Trigger and fine-tune vibration patterns from the browser.
📳
Modulino Vibro – Haptic Tester
Comments
Only logged in users can leave comments