├── .gitignore ├── images ├── device.jpg ├── module.jpg ├── relay-board.jpg ├── schematics.jpg ├── controller-top.jpg ├── module-apart.jpg ├── module-concept.jpg ├── settings-form.jpg ├── controller-open.jpg ├── settings-helper.jpg ├── spring-in-print.jpg ├── relay-board-coils.jpg ├── settings-helper.svg └── module-concept.svg ├── schematics └── sketch.fzz ├── models ├── module │ ├── module.blend │ ├── module.blend1 │ └── stl │ │ └── 1.0.0 │ │ ├── vm-base_2.4.0.stl │ │ ├── vm-latch_2.4.0.stl │ │ ├── vm-shell_2.4.0.stl │ │ └── vm-spring_2.4.0.stl └── controller │ ├── controller.blend │ └── controller.blend1 ├── LICENSE ├── README.md └── code └── nodemcu └── nodemcu.ino /.gitignore: -------------------------------------------------------------------------------- 1 | /local -------------------------------------------------------------------------------- /images/device.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/images/device.jpg -------------------------------------------------------------------------------- /images/module.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/images/module.jpg -------------------------------------------------------------------------------- /images/relay-board.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/images/relay-board.jpg -------------------------------------------------------------------------------- /images/schematics.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/images/schematics.jpg -------------------------------------------------------------------------------- /schematics/sketch.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/schematics/sketch.fzz -------------------------------------------------------------------------------- /images/controller-top.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/images/controller-top.jpg -------------------------------------------------------------------------------- /images/module-apart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/images/module-apart.jpg -------------------------------------------------------------------------------- /images/module-concept.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/images/module-concept.jpg -------------------------------------------------------------------------------- /images/settings-form.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/images/settings-form.jpg -------------------------------------------------------------------------------- /images/controller-open.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/images/controller-open.jpg -------------------------------------------------------------------------------- /images/settings-helper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/images/settings-helper.jpg -------------------------------------------------------------------------------- /images/spring-in-print.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/images/spring-in-print.jpg -------------------------------------------------------------------------------- /models/module/module.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/models/module/module.blend -------------------------------------------------------------------------------- /models/module/module.blend1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/models/module/module.blend1 -------------------------------------------------------------------------------- /images/relay-board-coils.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/images/relay-board-coils.jpg -------------------------------------------------------------------------------- /models/controller/controller.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/models/controller/controller.blend -------------------------------------------------------------------------------- /models/controller/controller.blend1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/models/controller/controller.blend1 -------------------------------------------------------------------------------- /models/module/stl/1.0.0/vm-base_2.4.0.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/models/module/stl/1.0.0/vm-base_2.4.0.stl -------------------------------------------------------------------------------- /models/module/stl/1.0.0/vm-latch_2.4.0.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/models/module/stl/1.0.0/vm-latch_2.4.0.stl -------------------------------------------------------------------------------- /models/module/stl/1.0.0/vm-shell_2.4.0.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/models/module/stl/1.0.0/vm-shell_2.4.0.stl -------------------------------------------------------------------------------- /models/module/stl/1.0.0/vm-spring_2.4.0.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackyDev/vibrotactile-stimulator/HEAD/models/module/stl/1.0.0/vm-spring_2.4.0.stl -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Евгений Епифанов 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vibrotactile-stimulator 2 | 3 | This DIY device is for vibrotactile stimulation and consists of 8 vibration modules and a NodeMCU controller. The frequency, vibration duration, and other settings can be adjusted using a web browser through WIFI. It can be built for less than $20 (access to a 3D printer required). 4 | 5 | This device does not have a specific designated use, so it can be used for any purpose of your choice. The initial settings are set to zero, and you will need to adjust them through a graphical interface to your own values. 6 | 7 | Please note that this device was built by a hobbyist for personal use only and has not been tested to meet any safety standards or requirements. The code running on the controller may also contain bugs. 8 | 9 | The software is likely to be more valuable since it can work with a variety of similar devices. In contrast, the hardware was assembled using whatever materials were available and may require a significant amount of effort to put together, including soldering and gluing. Additionally, the parts used may not be ideal, and I provide some suggestions for improvement throughout the document. 10 | 11 | The repository will be updated with more information in the future. If you find it helpful for your own projects, please consider starring the repository. You can contact me at hackydev@gmail.com 12 | 13 | ![vibrotactile stimulation device](/images/device.jpg?raw=true) 14 | 15 | ## Settings 16 | 17 | The device generates a WIFI network named "vtstim". The default password is 12345678. Upon connecting to this network, you can access the device's settings page by entering "192.168.1.1" in your browser's address bar. From there, you can make various adjustments and update the device in real-time. 18 | 19 | ![vibrotactile stimulation settings form](/images/settings-form.jpg?raw=true) 20 | 21 | ![vibrotactile stimulation settings helper](/images/settings-helper.jpg?raw=true) 22 | 23 | ## Controller 24 | 25 | The controller utilizes a [NodeMCU](https://www.amazon.com/OLatus-OL-nodeMCU-CH340-Wireless-Internet-development/dp/B07BM58B9K), which is compatible with Arduino and allows for the reuse of code in your Arduino projects with some modifications. The board in Arduino IDE is [NodeMCU 1.0 (ESP-12E Module)](https://docs.platformio.org/en/stable/boards/espressif8266/nodemcuv2.html). 26 | 27 | The system is composed of a NodeMCU unit, 8 220 Ohm resistors, 8 DIY vibrotactile modules, 8 1N4007 diodes, and 8 IRF1406 MOSFET transistors, as shown in the schematics below. 28 | 29 | I initially utilized BJT transistors, but they resulted in the NodeMCU being unable to boot up. This was due to these transistors grounding certain controller pins, which prevents the NodeMCU from starting up. To address this issue, MOSFET transistors are required. 30 | 31 | ![vibration module concept](/images/schematics.jpg?raw=true) 32 | 33 | I haven't included *.stl files for the controller cover because it's unlikely that they will fit your specific controller hardware. However, I have provided a Blender file, so you can make edits and customize it according to your requirements. 34 | 35 | ![vibration stimulator controller open](/images/controller-open.jpg?raw=true) 36 | ![vibration stimulator controller top](/images/controller-top.jpg?raw=true) 37 | 38 | ## Vibration module 39 | 40 | The vibration module consists of 3D printed parts, a small 10x10x1 permanent neodymium magnet, and a magnetic coil extracted from a power relay. Assembly can be quite time-consuming. For future versions, I would consider using an audio exciter such as [this one](https://www.tectonicaudiolabs.com/product/teax09c005-8/) 41 | 42 | ![vibration module concept](/images/module-concept.jpg?raw=true) 43 | 44 | ![vibration module concept](/images/relay-board.jpg?raw=true) 45 | 46 | ![Relay coils](/images/relay-board-coils.jpg?raw=true) 47 | 48 | ![vibration module apart](/images/module-apart.jpg?raw=true) 49 | 50 | When you print a spring, the printer will pause in the middle of the print to allow you to insert the magnet into the designated hole. Once you have done this, you can resume printing, and the magnet will be securely held inside the spring, eliminating the need for any gluing 51 | 52 | ![vibration module spring with magnet](/images/spring-in-print.jpg?raw=true) 53 | 54 | The latch on top of the finger module is designed for relatively small fingers. You may need to adjust its height in the provided Blender file. Alternatively, you can place a shim below this part to achieve the desired fit 55 | 56 | ![vibration module](/images/module.jpg?raw=true) 57 | -------------------------------------------------------------------------------- /images/settings-helper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | ON PERIODON-period amount of stepsOFF-period amount of steps*jitter is a variation of vibration onset time*jitter is applied to each step with a random sign (+-)- jitter percent JITTERMIRROR MODESTEPPAUSEOFF PERIODVIBRATION - vibration frequency (HZ)- vibration duration (MS) - pause duration (MS)*when enabled the vibration is applied to corresponding fingers on each hand. *when disabled the order is reversed, e.g., when an index finger is stumulated on the left hand the little finger is stimulated on the right hand. 4 | -------------------------------------------------------------------------------- /images/module-concept.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 51 | 56 | 57 | 70 | 75 | 76 | 89 | 94 | 95 | 108 | 113 | 114 | 127 | 132 | 133 | 134 | 138 | 148 | 155 | 162 | 169 | 176 | 183 | 190 | 197 | 204 | 211 | 218 | 225 | 232 | 239 | 246 | 253 | 260 | 267 | 274 | 282 | 290 | 297 | 300 | 307 | 314 | 321 | 328 | 335 | 342 | 349 | 356 | 357 | 362 | 367 | 372 | 377 | 382 | PlASTIC CASE 393 | CONTACTOR POINT 404 | PERMANENT MAGNET 415 | WINDINGS 426 | STATOR 442 | 443 | 444 | -------------------------------------------------------------------------------- /code/nodemcu/nodemcu.ino: -------------------------------------------------------------------------------- 1 | // https://lastminuteengineers.com/creating-esp8266-web-server-arduino-ide/ 2 | #include 3 | #include 4 | #include 5 | 6 | const char* ssid = "vtstim"; // Enter SSID here 7 | const char* password = "12345678"; // Enter Password here 8 | 9 | enum FingerPin { 10 | RIGHT_INDEX_FINGER_PIN = D3, 11 | RIGHT_MIDDLE_FINGER_PIN = D2, 12 | RIGHT_RING_FINGER_PIN = D1, 13 | RIGHT_LITTLE_FINGER_PIN = D0, 14 | LEFT_INDEX_FINGER_PIN = D7, 15 | LEFT_MIDDLE_FINGER_PIN = D6, 16 | LEFT_RING_FINGER_PIN = D5, 17 | LEFT_LITTLE_FINGER_PIN = D4 18 | }; 19 | 20 | enum State { 21 | NONE, 22 | STEP, 23 | VIBRATION, 24 | PULSE_ON, 25 | PULSE_OFF, 26 | PAUSE 27 | }; 28 | 29 | struct UserSettings { 30 | int vibrationFrequencyHz; 31 | int vibrationDurationMs; 32 | int pauseDurationMs; 33 | int onPeriodAmountOfSteps; 34 | int offPeriodAmountOfSteps; 35 | int jitterPercent; 36 | bool mirrorModeEnabled; 37 | }; 38 | 39 | class RandomProvider { 40 | private: 41 | int _sequencePosition = 0; 42 | long _sequenceStorage[3] = { 0, 0, 0 }; 43 | long _previousRand = 0; 44 | long _currentRand = 0; 45 | 46 | public: 47 | long getRandomNumber() { 48 | _previousRand = _currentRand; 49 | _currentRand = generateUniqueRandom(); 50 | _sequenceStorage[_sequencePosition] = _currentRand; 51 | if (_sequencePosition++ > 2) { 52 | reset(); 53 | } 54 | return _currentRand - 1; 55 | } 56 | void reset() { 57 | _sequenceStorage[0] = 0; 58 | _sequenceStorage[1] = 0; 59 | _sequenceStorage[2] = 0; 60 | _sequencePosition = 0; 61 | } 62 | private: 63 | long generateUniqueRandom() { 64 | long rand = random(1, 5); 65 | while (isDuplicate(rand)) { 66 | rand = random(1, 5); 67 | } 68 | return rand; 69 | } 70 | 71 | bool isDuplicate(long num) { 72 | return _sequenceStorage[0] == num || _sequenceStorage[1] == num || _sequenceStorage[2] == num || _previousRand == num; 73 | } 74 | }; 75 | 76 | class Timer { 77 | private: 78 | unsigned long _startedAt; 79 | unsigned long _endsAt; 80 | unsigned long _duration; 81 | bool _hasOverflowed; 82 | bool _isMillis; 83 | unsigned long getTime () { 84 | if (_isMillis) { 85 | return millis(); 86 | } else { 87 | return micros(); 88 | } 89 | } 90 | public: 91 | Timer (bool isMillis) { 92 | _isMillis = isMillis; 93 | } 94 | const unsigned long& startedAt = _startedAt; 95 | void start(unsigned long durationMicroseconds) { 96 | _startedAt = getTime(); 97 | _endsAt = _startedAt + durationMicroseconds; 98 | _duration = durationMicroseconds; 99 | _hasOverflowed = _endsAt < _startedAt; 100 | } 101 | bool isStarted () { 102 | return _duration > 0; 103 | } 104 | unsigned long getRunTime () { 105 | if (isStarted()) { 106 | return getTime() - _startedAt; 107 | } else { 108 | return 0; 109 | } 110 | } 111 | unsigned long isDone () { 112 | if (_duration == 0) { 113 | return false; 114 | } 115 | unsigned long now = getTime(); 116 | if (_hasOverflowed) { 117 | return now + _duration > _startedAt; 118 | } else { 119 | return now > _endsAt; 120 | } 121 | } 122 | void reset () { 123 | _startedAt = 0; 124 | _endsAt = 0; 125 | _duration = 0; 126 | _hasOverflowed = false; 127 | } 128 | }; 129 | 130 | class UserSettingsStorage { 131 | private: 132 | UserSettings _defaultUserSettings; 133 | public: 134 | int checkValue = 11111; 135 | int checkAddress = 100; 136 | int dataAddress = 200; 137 | 138 | UserSettingsStorage (UserSettings defaultUserSettings) { 139 | _defaultUserSettings = defaultUserSettings; 140 | } 141 | bool put(UserSettings userSettings) { 142 | EEPROM.put(dataAddress, userSettings); 143 | EEPROM.put(checkAddress, checkValue); 144 | return EEPROM.commit(); 145 | } 146 | UserSettings get () { 147 | UserSettings userSettings; 148 | int checkValueStored; 149 | EEPROM.get(checkAddress, checkValueStored); 150 | if (checkValue == checkValueStored) { 151 | EEPROM.get(dataAddress, userSettings); 152 | return userSettings; 153 | } else { 154 | return getDefaultSettings(); 155 | } 156 | } 157 | UserSettings getDefaultSettings () { 158 | return _defaultUserSettings; 159 | } 160 | void printUserSettings (UserSettings userSettings) { 161 | Serial.println(""); 162 | Serial.print("vibrationFrequencyHz:"); 163 | Serial.println(userSettings.vibrationFrequencyHz); 164 | Serial.print("vibrationDurationMs:"); 165 | Serial.println(userSettings.vibrationDurationMs); 166 | Serial.print("mirrorModeEnabled:"); 167 | Serial.println(userSettings.mirrorModeEnabled); 168 | Serial.print("onPeriodAmountOfSteps:"); 169 | Serial.println(userSettings.onPeriodAmountOfSteps); 170 | Serial.print("offPeriodAmountOfSteps:"); 171 | Serial.println(userSettings.offPeriodAmountOfSteps); 172 | Serial.print("jitterPercent:"); 173 | Serial.println(userSettings.jitterPercent); 174 | Serial.print("mirrorModeEnabled:"); 175 | Serial.println(userSettings.mirrorModeEnabled); 176 | } 177 | }; 178 | 179 | class App { 180 | private: 181 | State _state = State::NONE; 182 | // TODO ADD TYPE CHECK to UserSettingsStorage 183 | UserSettingsStorage _userSettingsStorage = UserSettingsStorage({ 0, 0, 0, 0, 0, 0, false }); 184 | UserSettings _userSettings = _userSettingsStorage.getDefaultSettings(); 185 | unsigned long _pulseOnDurationUs = 0; 186 | unsigned long _pulseOffDurationUs = 0; 187 | int _leftHandActivePin = 0; 188 | int _rightHandActivePin = 0; 189 | int _step = 0; 190 | bool _isOnPeriod = true; 191 | long _jitterValueMu = 0; 192 | long _currentJitterValueMu = 0; 193 | Timer _muTimer = Timer(false); 194 | Timer _muTimer2 = Timer(false); 195 | RandomProvider _randomProvider = RandomProvider(); 196 | constexpr static int _fingerPins[2][4] = { 197 | { 198 | LEFT_INDEX_FINGER_PIN, 199 | LEFT_MIDDLE_FINGER_PIN, 200 | LEFT_RING_FINGER_PIN, 201 | LEFT_LITTLE_FINGER_PIN 202 | }, 203 | { 204 | RIGHT_INDEX_FINGER_PIN, 205 | RIGHT_MIDDLE_FINGER_PIN, 206 | RIGHT_RING_FINGER_PIN, 207 | RIGHT_LITTLE_FINGER_PIN 208 | } 209 | }; 210 | constexpr static int _fingerPinsReversed[2][4] = { 211 | { 212 | LEFT_LITTLE_FINGER_PIN, 213 | LEFT_RING_FINGER_PIN, 214 | LEFT_MIDDLE_FINGER_PIN, 215 | LEFT_INDEX_FINGER_PIN 216 | }, 217 | { 218 | RIGHT_LITTLE_FINGER_PIN, 219 | RIGHT_RING_FINGER_PIN, 220 | RIGHT_MIDDLE_FINGER_PIN, 221 | RIGHT_INDEX_FINGER_PIN 222 | } 223 | }; 224 | String _stateStr[6] = { 225 | "NONE", 226 | "STEP", 227 | "VIBRATION", 228 | "PULSE_ON", 229 | "PULSE_OFF", 230 | "PAUSE" 231 | }; 232 | void applyUserSettings (UserSettings userSettings) { 233 | if (userSettings.vibrationFrequencyHz > 0) { 234 | _pulseOnDurationUs = 1000000 / (userSettings.vibrationFrequencyHz * 2); 235 | _pulseOffDurationUs = 1000000 / (userSettings.vibrationFrequencyHz * 2); 236 | } else { 237 | _pulseOnDurationUs = 0; 238 | _pulseOffDurationUs = 0; 239 | } 240 | } 241 | unsigned long getPauseDuration () { 242 | return (_userSettings.pauseDurationMs * 1000) + _currentJitterValueMu; 243 | } 244 | unsigned long getVibrationDuration () { 245 | return _userSettings.vibrationDurationMs * 1000; 246 | } 247 | void setJitterValue () { 248 | _jitterValueMu = ((_userSettings.pauseDurationMs * 1000) / 2) * (_userSettings.jitterPercent / 100); 249 | } 250 | void updateCurrentJitterValue () { 251 | if (_step % 2 > 0) { 252 | long rand = random(0, 2); 253 | if (rand > 0) { 254 | _currentJitterValueMu = _jitterValueMu; 255 | } else { 256 | _currentJitterValueMu = -_jitterValueMu; 257 | } 258 | } else { 259 | if (_currentJitterValueMu > 0) { 260 | _currentJitterValueMu = -_jitterValueMu; 261 | } else { 262 | _currentJitterValueMu = _jitterValueMu; 263 | } 264 | } 265 | } 266 | bool updateState () { 267 | State state = _state; 268 | switch (_state) { 269 | case State::NONE: 270 | _state = State::STEP; 271 | break; 272 | case State::STEP: 273 | _state = State::VIBRATION; 274 | _muTimer2.start(getVibrationDuration()); 275 | break; 276 | case State::VIBRATION: 277 | _state = State::PULSE_ON; 278 | _muTimer.start(_pulseOnDurationUs); 279 | break; 280 | case State::PULSE_ON: 281 | if (_muTimer2.isDone()) { 282 | _state = State::PAUSE; 283 | _muTimer2.reset(); 284 | _muTimer.reset(); 285 | } else if (_muTimer.isDone()) { 286 | _state = State::PULSE_OFF; 287 | _muTimer.reset(); 288 | _muTimer.start(_pulseOffDurationUs); 289 | } 290 | break; 291 | case State::PULSE_OFF: 292 | if (_muTimer2.isDone()) { 293 | _state = State::PAUSE; 294 | _muTimer2.reset(); 295 | _muTimer.reset(); 296 | } else if (_muTimer.isDone()) { 297 | _state = State::PULSE_ON; 298 | _muTimer.reset(); 299 | _muTimer.start(_pulseOnDurationUs); 300 | } 301 | break; 302 | case State::PAUSE: 303 | if (_muTimer2.isStarted()) { 304 | if (_muTimer2.isDone()) { 305 | _state = State::STEP; 306 | _muTimer2.reset(); 307 | } 308 | } else { 309 | _muTimer2.start(getPauseDuration()); 310 | } 311 | break; 312 | } 313 | return state != _state; 314 | } 315 | void updateActivePins () { 316 | int num = _randomProvider.getRandomNumber(); 317 | _leftHandActivePin = _fingerPins[0][num]; 318 | if (_userSettings.mirrorModeEnabled) { 319 | _rightHandActivePin = _fingerPins[1][num]; 320 | } else { 321 | _rightHandActivePin = _fingerPinsReversed[1][num]; 322 | } 323 | } 324 | void newStepHandler () { 325 | if (_isOnPeriod) { 326 | if (_step >= _userSettings.onPeriodAmountOfSteps) { 327 | _isOnPeriod = false; 328 | _step = 0; 329 | } 330 | } else { 331 | if (_step >= _userSettings.offPeriodAmountOfSteps) { 332 | _isOnPeriod = true; 333 | _step = 0; 334 | _randomProvider.reset(); 335 | } 336 | } 337 | updateActivePins(); 338 | updateCurrentJitterValue(); 339 | _step++; 340 | } 341 | void initPins () { 342 | for (int i = 0; i < 4; i++) { 343 | pinMode(_fingerPins[0][i], OUTPUT); 344 | pinMode(_fingerPins[1][i], OUTPUT); 345 | } 346 | } 347 | void printState (State state) { 348 | Serial.print("STATE: "); 349 | Serial.println(_stateStr[state]); 350 | } 351 | public: 352 | void setup () { 353 | randomSeed(analogRead(A0)); 354 | EEPROM.begin(512); 355 | _userSettings = _userSettingsStorage.get(); 356 | _userSettingsStorage.printUserSettings(_userSettings); 357 | applyUserSettings(_userSettings); 358 | setJitterValue(); 359 | initPins(); 360 | } 361 | void update () { 362 | bool isStateChanged = updateState(); 363 | if (isStateChanged) { 364 | switch (_state) { 365 | case State::STEP: 366 | newStepHandler(); 367 | break; 368 | case State::PAUSE: 369 | digitalWrite(_leftHandActivePin, LOW); 370 | digitalWrite(_rightHandActivePin, LOW); 371 | break; 372 | case State::PULSE_ON: 373 | if (_isOnPeriod) { 374 | digitalWrite(_leftHandActivePin, HIGH); 375 | digitalWrite(_rightHandActivePin, HIGH); 376 | } 377 | break; 378 | case State::PULSE_OFF: 379 | digitalWrite(_leftHandActivePin, LOW); 380 | digitalWrite(_rightHandActivePin, LOW); 381 | break; 382 | } 383 | } 384 | } 385 | bool setUserSettings (UserSettings userSettings) { 386 | bool success = _userSettingsStorage.put(userSettings); 387 | if (success) { 388 | _userSettings = userSettings; 389 | applyUserSettings(_userSettings); 390 | } 391 | return success; 392 | } 393 | UserSettings getUserSettings () { 394 | return _userSettings; 395 | } 396 | 397 | bool setUserSettingsFromHttpRequest (ESP8266WebServer &server) { 398 | UserSettings userSettings = getUserSettings(); 399 | for (int i = 0; i < server.args(); i++) { 400 | String argValue = server.arg(i); 401 | String argName = server.argName(i); 402 | if (argName == "vibrationFrequencyHz") { 403 | userSettings.vibrationFrequencyHz = argValue.toInt(); 404 | } else if (argName == "vibrationDurationMs") { 405 | userSettings.vibrationDurationMs = argValue.toInt(); 406 | } else if (argName == "pauseDurationMs") { 407 | userSettings.pauseDurationMs = argValue.toInt(); 408 | } else if (argName == "onPeriodAmountOfSteps") { 409 | userSettings.onPeriodAmountOfSteps = argValue.toInt(); 410 | } else if (argName == "offPeriodAmountOfSteps") { 411 | userSettings.offPeriodAmountOfSteps = argValue.toInt(); 412 | } else if (argName == "jitterPercent") { 413 | userSettings.jitterPercent = argValue.toInt(); 414 | } else if (argName == "mirrorModeEnabled") { 415 | userSettings.mirrorModeEnabled = argValue == "true"; 416 | } 417 | } 418 | return setUserSettings(userSettings); 419 | } 420 | }; 421 | 422 | App app = App(); 423 | 424 | // SERVER 425 | IPAddress local_ip(192,168,1,1); 426 | IPAddress gateway(192,168,1,1); 427 | IPAddress subnet(255,255,255,0); 428 | ESP8266WebServer server(80); 429 | 430 | void setup() { 431 | Serial.begin(115200); 432 | while (!Serial) { 433 | ; // wait for serial port to connect. Needed for native USB port only 434 | } 435 | // SERVER 436 | WiFi.softAP(ssid, password); 437 | WiFi.softAPConfig(local_ip, gateway, subnet); 438 | delay(100); 439 | server.on("/", serveIndex); 440 | server.on("/update", serveUpdate); 441 | server.onNotFound(serveIndex); 442 | server.begin(); 443 | 444 | // APP 445 | app.setup(); // return mode and disable/prevent init wifi and server if needed (for performance) 446 | } 447 | 448 | void loop() { 449 | server.handleClient(); 450 | app.update(); 451 | } 452 | 453 | // SERVER 454 | 455 | void serveIndex() { 456 | server.send(200, "text/html", getUpdateUserSettingsFormHtml(app.getUserSettings())); 457 | } 458 | void serveUpdate() { 459 | bool success = app.setUserSettingsFromHttpRequest(server); 460 | if (success) { 461 | server.send(200, "text/html", getUpdateUserSettingsSuccessHtml()); 462 | } else { 463 | server.send(200, "text/html", getUpdateUserSettingsFailHtml()); 464 | } 465 | } 466 | 467 | // https://flems.io/#0=N4IgtglgJlA2CmIBcBmArAOgGwHYA0IAzvAgMYAu8UyIGphhIBpA9gHaEsI2uywCGAB2LUChUgCcusZAG0ADHnkBdAgDMICRklmg2-MIiS0AFuTAzm7Sm3I0APIQBuAcwAE0ALwAdEBDZqLAC0EGAuvm4AHhYcPiBm5IJIAPTJAO4ZGGkoGCwSLskATPIlyc7hIFExhEiRsP4A1nEJSakZaVk5eQUAjACcA8l1jRFp0OQmcSgALCURJvAQLmZTs-IRThDwaQBCLJFx8m5HABwDGGgnbmd9Fye+AHzebM9u9u5evvyPr-aC-BM3FA4gBZHooW5YNBuaZoQoYHAnExoW7TPqFJyFFDyDAoAAS4MhaAAXhFyBJ+BxAhIwHFyZTCAJKAAKII9HAYYrTNxBCHw6YASl8ySebDe-0BGj4cQAxGp5RFgb4wbNsNDYTlEciOWiMYVCidcQTVVDSZV6VS8rTfBbGQD4Kz2Zz5NzeeiMILhaLxQCTG4pbBZTB4KQAEaFRWg9mYHrcjUIpFYjAlaaYg1G6MYWNmty26nWkC2pkOtkcrk8vkeoUgEW-ZIub3vDxKkCkH5iv6+oFRiFqmFwhPI1HozHYjO901kimWml06d2lngrAYdE8s7YeSFau1jsSv0BoNQEPhyPK2M4qH9zVItA6kf6w3489qnN5q1zhnF5lLleFNeQzdt0betG3cQhyRYBp4CCeo2BDIQ4kIABHABXfgJHgCJwKkKCgjGKAJjieEsFhe4QFA-1rCCQgIGJeAiMwaZ2zeShInIKI4hOTAcBweQUQiABPVZlxOCEeh6CJAlsPDFmWcg4lDLgoAiB57HIQh-jFA5fC4hFeP4yohN8GYRLEiTKgeAB5AA5NwAAUAFEACUAElLIAEXsZJ1M01TvPgNjG1Y9jtJAZ9ET6FAI0MuIsHTHAUGjSTNEDXwwHQlh2DJALyCCSlSBMPI4kgGAEEeNSNMpDjfHCs4osE2L4sSnA0EeGygkEeAJAgFgoDebzKrYVSfKq0Lasi6K3CMkAsCwHFphOJLyIMFgUNsNwWDUNxwPgYQvJGob9pyoKcuqkAIUfE5ikm6a4sNBKlv9FLioyrLzRyvK2AKoq0ugOBMPIirNLOi7cSuzcGt8O6EWa1ryMsgAxBGOq6nq+v2wbhsGkG+ku67IZmuaPUWlrHhWtb2M27bKD2gbfKOwK6wbX53Ck3KaLooj4VvMiTrYs7bxxM40FhAnLhyE40Swcy3Cx4HQsFjBhdFmLfHFpWpfMh4ACoACsIHISgJA8Qg3H4NwnHQiAAR6sUqc2UMKXIW3+oOuXRriRXlbhqbYp6eF5BwHpJceDaOHgdjncMDH6YOgXeKVvoRZ927VUKPppnBR49YNo2TbNwRBHqKhcxYV3MaBj21YT72Cdmw00B6LAUCwUP4H4Arqd2twxkBc3pygFgwHL2PsYVmuk5V32ob6ZcEq4vpQ5olwxWZABqIIBRjyk-OCvmQs9tBMBQSXplb1WQCPnIsVEpjKjZmSlhWXxFNgZSLMrrTD+P0-z+ny+0DXxPjMSSVEOb0V8IURioDpJpFks-EAbArT8FgOVN2QQR470-k9aUaVXovHIvrQ2XU3CdQkKQeAtht5DUwYdfyjMOzBQFtMPSGck4ExOPITU2Im4qWwQrFhPE2Ep04lwmG8heHkW1mkBYYpKH8FDAgPqEx4AWwgI7G27B85CCLlsZRZdWASAwhpdgUB-Cs3MV1U2Wj26dxMJSKAGBaHuy-mrQROBhEcMSiuOEEJs4yMoUCCAhAFFKNzAsDaEgjzG2CW4DCTgrFUDwG4eAGAXAYGSQEsUVV-BHkiE9NgLgSGxPAihMAKFix9S0dQlxzDWFohETpIm7Iz683CaohAah2L2LYMoiJ9RDYIAKUUmJptwKhAqfaKpYoVFxKft0hxGAan8M9u4zxF8TibNxLPPo5kRQM3IA8NwTMKIP3AQxD0MDcpwPmQpJSzE1KnVCjgdMwcEo3U9k6cEi1oq1Oea8k47yxZfJPv7bKbFPrfQkMVP6ZVyIACkXIABUkXOWWfQw5vwmGhVhII6YD4CYtRyE3Fqd9cwfXyoVaFv1SoAz+XEXFCJ8UGkJYArMuARaPBBC5JyTlLJOTcCCDyDl0V7yxU8uI-scQogWovC+Z8Vx9E3DgPhccxrFAuBnM4BMFUDGVSpAAyqiuyorjriv5qFQosZcSZ3YRfcS8I9WIlVWPIi1qZj9EaWFf2irA5kVlnZAAggAVQNSKumWCMX7xBlahEEjCh-2miZJWZkXXyymLG3i-tE3CRTbsmWVkkb2Wcm5TyEa6Fio7CBE5vw9w4NSog9gANqY4QgSAGUKAWo4CgJNbCkFoKwXgoIRCqF0LNr7bhfChFIHYFIqecA7IcRQLcD0eQy4URuBQDkTcPRcSbhAZUHcPpASfBAKGZKuD22hjWHMSoLYwRJmmNydkhp04mBmMTE4ThbzJhwNMAkj7pg5iPU2CdA7-BDpHWhDCWEIKTvGJMGdJFLgPOxURF0Fx5C4wJla-k4JpgqvvmA2ibam4ri3Vcx+ck7lvzTVXEAXJMPYYvrhj0+HCOy2wWckjFy74PAAGouR2E5QNSK3LWRqc4g50bLU9EwHFK1cr-6FFVE+3ik1uOc0gdAj+aq3Xyf1P0HDqmCMQ3Ihgrjz08FSDeg8B2TsXZqAwqhShpABJuGZHiAAWlvctu8K56cgXJ7AhmlPTUKC8rMOy5UPAs3HA81nMoELs+ohzWioAoTS6vEEBqBRSb89J81B8gu3C4kfMLkqT4XGbuySj5ztOXKI7A+B8kX73N066krSsj4GX-t86rzUL0NvSjZghlFmu3N8EgmkKDHgYP+ChYgQJMuaOy7l01DD+rM13F2BLja4KwdbbKTtt4e2Hf7TBCDpAEK+GQtB8dcHoJTsQwx2dKG73FWfFgPobhigciwFcH9Ca+hBDjTgAHEQQOLdUcMNgDQkAmAwmoWU3xzTznzB+DgX4gi7IRCpzdm4ER9CAr8aHURYLw8R-AZHvgZTnrRwyDHNp5zY9xy87k2IcQuhJx2MnsPKdI5R1ORn75mefntKyMcCaUAE-hB4nnbw+cU4R4L2nqPmxxDUMLmcBYiwS+8f0K42I5fE69KTpb-OVfU9lPT3M6PReFhZxL3kS7m6-fxSuHACv7BK8aFbmn7b1dvlnGLrHEv3RQh+8bon3vfdw-9zb7XhAmeO-FyyA3uNN0-vErHi3yuqcB7p0nlPeuWQR43cHTAZxc8w-z6r9tbZKinvfnbkXIfU9h5ZNLD00sa-k79wXoMERT3NuD7rp3Xfd1nx6H3y3g-aej-t+30vDpu-T9n3X63tOtcM515jhcDpYS3H6DyS4LDRJ5fECgh0u6PF9C92b3neeB-17lMXh3K-mToBYfjoIZ+U2X7XYIDfhE737e51qnoVAtr9qyjfZwFKZgZBBQD8CED2JGL8DTQsKrriTJKvo7ocaIHIGoGbRqDECtYzTVbBzna4SDrXbDq3ajowaVCIHPYMpZgSIyz3rgychYBuAJxn5OCZxCx-xQ7P7x7z7tpQFj777Y6roxg9A8jJglB8Qb4v5b7tomDv7L4T4lhyFZgKFsiBy-iqHiGv4QBaHj5p4liiQrhnw8irocjojgFdinqaH3xWbtryj3735DZBAsD-CkAGzTQXCxjpzzoghBwxjQido5BwgmB6HiROAOG-h4iRFZgkiQ6NgQEtjmHuGXp043rrAfbKgvLLi7KboJQXCFAmBxTJjfZJFGHoipEJorg9DAaiheTlAdH5hmwUC2xxDJAoSCDIGUDMRvBmJODehuDTGOC+T8aparb+jOYoSubuaeY+adG+SvDTFvD+CCAoTsT6CGBxD2arYIzLGrF4g5jLEQAYR9RFwdzwCFRvxdRxAOS2AkKWywArERCQBsCHC-H8ChRRTg4cbkACSdRxBsBlKhhdSwa7SSqP4zHJATFTH2ConbEzGYzzEaLOzpYrZ4lra+aYyYm7FsD7GHEGBtqnGEk7CZbgTuQEm2wgiEARA3F3GkICAULPHRJvEfHGxfE-GVB-EAnClAkZqglkgQltrQlgCwkSDwn0FhRIn9QYlijjEQCTGkmzE7x2T8Bk4ZZZYeY5bElbHqkzF7EHFuBHFtoLbECMlZYslsnwCoS3ElwPHclKSvG+DvF5yCnNoim+BFFuDpTAkRa4BSmQlTYwlwnME0yIk1hokomalTHbHokpnamYztRkJoxmxgCrTrRUw7S0wknmlkkUnWlUlxDsB2Soy9SBr5kUyWRqAGo0ysmVDsnulclPFenUogC+mfEoJCkhn+CikhninGThlgnSlQkxkKlxkIk1QqleRqnTFpmrk7E6lDSIzIw5m9R5kFmUxbTFmECbE7zamWmUnHG+AkG1ndT1mNm2DNmtm7TtlxIukoRun3Hdk8nen9n8kWxDkBmjlBmAlhmSnmgznRlymxldxKl7JJkbnrkZlllbkPDwq5wkJkIUJUJlBmk7HllWk2lxBEJGx3k4XkDOmukckek9kvF9kDkClAW-EgUgDBmhmSq3q5hQWIJzmKkJlHqqkoVrnqnplamoWYwgi3FSDGz5lHhnlDSZkkAhhXltqQBGJ5Agi9TwDvGhJUBjEEX2D+GEmAXfFtrkhCkPC6WKJUBeTGW2xTGGX2VaL+ma4oLECPDuTBJ6VQB2WCCEmOX9TEBkCYqiXJniUiUWnklWnglRlEAoShiQCUWVCuW+DBrDH2jtheT5gPBMBEDKV9HsDaBsVID9BBDyBIA4AgAAC+eAegVJNAGAusjAVgHxtgNANVdViCDVxgdADAeVrAbVdgxgikUA7m2sbgwA45+Q-gSAxwuIGEYAAA3COWwHhAhnNauvIIIJECtdVc8KNe5lNWYhpAIAJHNWoAgLtf6FdXhBSEkD3PdStW4LrIts7GoAJEEINTYOQHNRRV1CtSgksGtQbPAGADUDdQFNROQOhOQCtQ-GoAYJoOdW4EivwIVOlEtftQEFaJNUEqdRgRdVdStaGetQRCYHNWgCUDtStQsPMnNfqeQCwPDbdWYhhIVWwH9VwGUmwCtaGB3A0C4FIGtFAHNTIqDYQPmVBHzXkNEptTtdtFwNAG4IogLSteLZQNRAEfAHNUgmkE9djT0UchMZNSdQ8SjZdQFCta9eMh9V9dYJQr9W4P9RICzVDfrUIGLQbc8EbUEk4LLG4NrMkj0cFSpXjc9nNcUNtddYpJENRLROYnNYpFEl1EELHXtc8DKP4IECEGEOHRtSuiUAAKS00tYM0HHM3jlx0R1uBU3R180C1C0Fmi09wmAS1S3wAy0p0SDy35KcD1B9Sq2kANB7V5Wh0c3FWFBICbI1XKDVVAA 468 | 469 | String getUpdateUserSettingsFormHtml (UserSettings userSettings) { 470 | String ptr = " "; 471 | ptr += ""; 472 | ptr += "Vibrotactile stimulator settings"; 473 | 474 | ptr += ""; 482 | 483 | ptr += "ON PERIODON-period amount of stepsOFF-period amount of steps*jitter is a variation of vibration onset time*jitter is applied to each step with a random sign (+-)- jitter percent *when enabled the vibration is applied to corresponding fingers on each hand. *when disabled the order is reversed, e.g., when an index finger is stumulated on the left hand the little finger is stimulated on the right hand.JITTERMIRROR MODESTEPPAUSEOFF PERIODVIBRATION - vibration frequency (HZ)- vibration duration (MS) - pause duration (MS)"; 484 | 485 | ptr += ""; 486 | ptr += ""; 487 | ptr += ""; 488 | 489 | ptr += "
"; 490 | ptr += "
"; 491 | ptr += "Vibration frequency (HZ)"; 492 | ptr += ""; 493 | ptr += "
"; 494 | ptr += "
"; 495 | ptr += "Vibration duration (MS)"; 496 | ptr += ""; 497 | ptr += "
"; 498 | 499 | ptr += "
"; 500 | ptr += "Pause duration (MS)"; 501 | ptr += ""; 502 | ptr += "
"; 503 | 504 | ptr += "
"; 505 | ptr += "ON-period amount of steps"; 506 | ptr += ""; 507 | ptr += "
"; 508 | 509 | ptr += "
"; 510 | ptr += "OFF-period amount of steps"; 511 | ptr += ""; 512 | ptr += "
"; 513 | 514 | ptr += "
"; 515 | ptr += "Jitter percent"; 516 | ptr += ""; 517 | ptr += "
"; 518 | 519 | ptr += "
"; 520 | ptr += "Mirror mode"; 521 | ptr += ""; 530 | ptr += "
"; 531 | 532 | ptr += ""; 533 | ptr += "
"; 534 | 535 | ptr += ""; 536 | ptr += ""; 537 | return ptr; 538 | } 539 | // https://flems.io/#0=N4IgtglgJlA2CmIBcA2ALAOgMwE4A0IAzvAgMYAu8UyIGphhIBhpATgPayzIDaADHj4BdAgDMICRkh6gAdgEMwiJLQAW5MNwKl2synpoAeVQEYAfAGV45chFkBzQgAIArgAco8ylEMB6U2YAOrKG8k6qrPCiALyBIL5xZgBC8qQA1k7k7E7ENnaOfvJmTEQk8BQQulIgJkhYAEwAtHxIfCAAvnhyisq0AFaM2rr65DQdXSAKSjR0DCU6evAGKgBG7FAAnk7ATlAQhG6w8htITqIIAB4A3E59LoS2ohuNCyOnpEuUrDfysBD2skaEEoYEI70+8G+4Xg-3UpxMfD4ADdVDdzvALo09pEKrp3pwXGBZDd2lcSsQyLYqjQWgicM1Wh0hO0gA 540 | String getUpdateUserSettingsSuccessHtml () { 541 | String ptr = " "; 542 | ptr += ""; 543 | ptr += "Pd-device settings"; 544 | ptr += ""; 547 | ptr += ""; 548 | ptr += ""; 549 | ptr += "

Settings updated!

"; 550 | ptr += "Back to settings"; 551 | ptr += ""; 552 | ptr += ""; 553 | return ptr; 554 | } 555 | 556 | String getUpdateUserSettingsFailHtml () { 557 | String ptr = " "; 558 | ptr += ""; 559 | ptr += "Pd-device settings"; 560 | ptr += ""; 563 | ptr += ""; 564 | ptr += ""; 565 | ptr += "

Error

"; 566 | ptr += "Back to settings"; 567 | ptr += ""; 568 | ptr += ""; 569 | return ptr; 570 | } --------------------------------------------------------------------------------