├── .gitignore ├── .travis.yml ├── .vscode ├── .browse.c_cpp.db-shm ├── .browse.c_cpp.db-wal └── settings.json ├── README.md ├── lib ├── MadgwickAHRS │ ├── README.adoc │ ├── examples │ │ └── Visualize101 │ │ │ └── Visualize101.ino │ ├── extras │ │ └── Visualizer │ │ │ └── Visualizer.pde │ ├── keywords.txt │ ├── library.properties │ └── src │ │ ├── MadgwickAHRS.cpp │ │ └── MadgwickAHRS.h └── readme.txt ├── platformio.ini ├── src └── main.cpp └── webapp ├── .gitignore ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── images │ └── redstone.png ├── index.html └── manifest.json ├── src ├── App.css ├── App.js ├── BLEBlock.js ├── MinecraftBlock.js ├── index.js └── registerServiceWorker.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .pioenvs 2 | .piolibdeps 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/*.db 6 | webapp/node_modules 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Continuous Integration (CI) is the practice, in software 2 | # engineering, of merging all developer working copies with a shared mainline 3 | # several times a day < http://docs.platformio.org/page/ci/index.html > 4 | # 5 | # Documentation: 6 | # 7 | # * Travis CI Embedded Builds with PlatformIO 8 | # < https://docs.travis-ci.com/user/integration/platformio/ > 9 | # 10 | # * PlatformIO integration with Travis CI 11 | # < http://docs.platformio.org/page/ci/travis.html > 12 | # 13 | # * User Guide for `platformio ci` command 14 | # < http://docs.platformio.org/page/userguide/cmd_ci.html > 15 | # 16 | # 17 | # Please choice one of the following templates (proposed below) and uncomment 18 | # it (remove "# " before each line) or use own configuration according to the 19 | # Travis CI documentation (see above). 20 | # 21 | 22 | 23 | # 24 | # Template #1: General project. Test it using existing `platformio.ini`. 25 | # 26 | 27 | # language: python 28 | # python: 29 | # - "2.7" 30 | # 31 | # install: 32 | # - pip install -U platformio 33 | # 34 | # script: 35 | # - platformio run 36 | 37 | 38 | # 39 | # Template #2: The project is intended to by used as a library with examples 40 | # 41 | 42 | # language: python 43 | # python: 44 | # - "2.7" 45 | # 46 | # env: 47 | # - PLATFORMIO_CI_SRC=path/to/test/file.c 48 | # - PLATFORMIO_CI_SRC=examples/file.ino 49 | # - PLATFORMIO_CI_SRC=path/to/test/directory 50 | # 51 | # install: 52 | # - pip install -U platformio 53 | # 54 | # script: 55 | # - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N 56 | -------------------------------------------------------------------------------- /.vscode/.browse.c_cpp.db-shm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvarowolfx/physical-cube-imu-web-bluetooth-esp32/5bce851e8bc3438057af28c84ebdce5d5e9bb2bf/.vscode/.browse.c_cpp.db-shm -------------------------------------------------------------------------------- /.vscode/.browse.c_cpp.db-wal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvarowolfx/physical-cube-imu-web-bluetooth-esp32/5bce851e8bc3438057af28c84ebdce5d5e9bb2bf/.vscode/.browse.c_cpp.db-wal -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "terminal.integrated.env.osx": { 3 | "PATH": "/Users/alvaroviebrantz/.platformio/penv/bin:/Users/alvaroviebrantz/.platformio/penv:/Users/alvaroviebrantz/.mos/bin:/Users/alvaroviebrantz/.fastlane/bin:/Users/alvaroviebrantz/.composer/vendor/bin:/usr/local/protoc/bin:/Users/alvaroviebrantz/Documents/Desenvolvimento/flutter/bin:/usr/local/bin:/Users/alvaroviebrantz/google-cloud-sdk/bin:/Users/alvaroviebrantz/.nvm/versions/node/v7.7.1/bin:/Users/alvaroviebrantz/Documents/Desenvolvimento/Go/gocode/bin:/usr/local/go/bin:/usr/local/go_appengine/:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/share/dotnet:/usr/local/go/bin:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/usr/local/bin:/Users/alvaroviebrantz/Documents/Desenvolvimento/Java/Android/sdk/tools:/Users/alvaroviebrantz/Documents/Desenvolvimento/Java/Android/sdk/platform-tools:/Users/alvaroviebrantz/Documents/Desenvolvimento/Java/Android/sdk/ndk-bundle/:/usr/local/elasticsearch-1.5.2/bin:/Applications/Postgres.app/Contents/Versions/9.3/bin:/usr/local/mysql/bin:/Applications/Visual Studio Code.app/Contents/Resources/app/bin" 4 | }, 5 | "files.associations": { 6 | "array": "cpp", 7 | "initializer_list": "cpp", 8 | "limits": "cpp", 9 | "system_error": "cpp", 10 | "type_traits": "cpp", 11 | "cmath": "cpp" 12 | } 13 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Physical Cube using ESP32, Acc+Gyro and Bluetooth LE displayed in a Browser - A WebBluetooth experiment. 2 | 3 | ## Overview 4 | 5 | IoT Project that shows how to connect a microcontroller directly to the Browser using Bluetooth LE. This project uses WebBluetooth specification, an ESP32, a NeoPixel Ring and a MPU6050 that is controlled and displayed by the web interface made with ReactJS and ThreeJS. 6 | 7 | ### Upload firmware to ESP32 8 | 9 | I used Platform.io to write this project. I recommend to use it with the Visual Studio Code IDE. 10 | 11 | * [Visual Studio Code](https://code.visualstudio.com/) 12 | * [Platform.io](https://platformio.org) 13 | * [Platform.io VSCode Plugin](http://docs.platformio.org/en/latest/ide/vscode.html) 14 | 15 | ### BOM 16 | 17 | * ESP32 Microcontroler. 18 | * 16 Leds Neopixel Ring. 19 | * MPU6050 Accelerometer and Gyroscope. 20 | * Jumpers 21 | 22 | ### Schematic 23 | 24 | * TODO 25 | 26 | ## References 27 | 28 | * https://developers.google.com/web/updates/2015/07/interact-with-ble-devices-on-the-web 29 | -------------------------------------------------------------------------------- /lib/MadgwickAHRS/README.adoc: -------------------------------------------------------------------------------- 1 | = Madgwick Library = 2 | 3 | This library wraps the official implementation of MadgwickAHRS algorithm to get orientation of an object based on accelerometer and gyroscope readings 4 | 5 | == License == 6 | 7 | Copyright (c) Arduino LLC. All right reserved. 8 | 9 | This library is free software; you can redistribute it and/or 10 | modify it under the terms of the GNU Lesser General Public 11 | License as published by the Free Software Foundation; either 12 | version 2.1 of the License, or (at your option) any later version. 13 | 14 | This library is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | Lesser General Public License for more details. 18 | 19 | You should have received a copy of the GNU Lesser General Public 20 | License along with this library; if not, write to the Free Software 21 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 22 | 23 | 24 | Implementation of Madgwick's IMU and AHRS algorithms. 25 | See: http://www.x-io.co.uk/node/8#open_source_ahrs_and_imu_algorithms 26 | 27 | Date Author Notes 28 | 29/09/2011 SOH Madgwick Initial release 29 | 02/10/2011 SOH Madgwick Optimised for reduced CPU load 30 | 19/02/2012 SOH Madgwick Magnetometer measurement is normalised 31 | -------------------------------------------------------------------------------- /lib/MadgwickAHRS/examples/Visualize101/Visualize101.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | Madgwick filter; 5 | unsigned long microsPerReading, microsPrevious; 6 | float accelScale, gyroScale; 7 | 8 | void setup() { 9 | Serial.begin(9600); 10 | 11 | // start the IMU and filter 12 | CurieIMU.begin(); 13 | CurieIMU.setGyroRate(25); 14 | CurieIMU.setAccelerometerRate(25); 15 | filter.begin(25); 16 | 17 | // Set the accelerometer range to 2G 18 | CurieIMU.setAccelerometerRange(2); 19 | // Set the gyroscope range to 250 degrees/second 20 | CurieIMU.setGyroRange(250); 21 | 22 | // initialize variables to pace updates to correct rate 23 | microsPerReading = 1000000 / 25; 24 | microsPrevious = micros(); 25 | } 26 | 27 | void loop() { 28 | int aix, aiy, aiz; 29 | int gix, giy, giz; 30 | float ax, ay, az; 31 | float gx, gy, gz; 32 | float roll, pitch, heading; 33 | unsigned long microsNow; 34 | 35 | // check if it's time to read data and update the filter 36 | microsNow = micros(); 37 | if (microsNow - microsPrevious >= microsPerReading) { 38 | 39 | // read raw data from CurieIMU 40 | CurieIMU.readMotionSensor(aix, aiy, aiz, gix, giy, giz); 41 | 42 | // convert from raw data to gravity and degrees/second units 43 | ax = convertRawAcceleration(aix); 44 | ay = convertRawAcceleration(aiy); 45 | az = convertRawAcceleration(aiz); 46 | gx = convertRawGyro(gix); 47 | gy = convertRawGyro(giy); 48 | gz = convertRawGyro(giz); 49 | 50 | // update the filter, which computes orientation 51 | filter.updateIMU(gx, gy, gz, ax, ay, az); 52 | 53 | // print the heading, pitch and roll 54 | roll = filter.getRoll(); 55 | pitch = filter.getPitch(); 56 | heading = filter.getYaw(); 57 | Serial.print("Orientation: "); 58 | Serial.print(heading); 59 | Serial.print(" "); 60 | Serial.print(pitch); 61 | Serial.print(" "); 62 | Serial.println(roll); 63 | 64 | // increment previous time, so we keep proper pace 65 | microsPrevious = microsPrevious + microsPerReading; 66 | } 67 | } 68 | 69 | float convertRawAcceleration(int aRaw) { 70 | // since we are using 2G range 71 | // -2g maps to a raw value of -32768 72 | // +2g maps to a raw value of 32767 73 | 74 | float a = (aRaw * 2.0) / 32768.0; 75 | return a; 76 | } 77 | 78 | float convertRawGyro(int gRaw) { 79 | // since we are using 250 degrees/seconds range 80 | // -250 maps to a raw value of -32768 81 | // +250 maps to a raw value of 32767 82 | 83 | float g = (gRaw * 250.0) / 32768.0; 84 | return g; 85 | } 86 | -------------------------------------------------------------------------------- /lib/MadgwickAHRS/extras/Visualizer/Visualizer.pde: -------------------------------------------------------------------------------- 1 | import processing.serial.*; 2 | Serial myPort; 3 | 4 | float yaw = 0.0; 5 | float pitch = 0.0; 6 | float roll = 0.0; 7 | 8 | void setup() 9 | { 10 | size(600, 500, P3D); 11 | 12 | // if you have only ONE serial port active 13 | myPort = new Serial(this, Serial.list()[0], 9600); // if you have only ONE serial port active 14 | 15 | // if you know the serial port name 16 | //myPort = new Serial(this, "COM5:", 9600); // Windows 17 | //myPort = new Serial(this, "/dev/ttyACM0", 9600); // Linux 18 | //myPort = new Serial(this, "/dev/cu.usbmodem1217321", 9600); // Mac 19 | 20 | textSize(16); // set text size 21 | textMode(SHAPE); // set text mode to shape 22 | } 23 | 24 | void draw() 25 | { 26 | serialEvent(); // read and parse incoming serial message 27 | background(255); // set background to white 28 | lights(); 29 | 30 | translate(width/2, height/2); // set position to centre 31 | 32 | pushMatrix(); // begin object 33 | 34 | float c1 = cos(radians(roll)); 35 | float s1 = sin(radians(roll)); 36 | float c2 = cos(radians(pitch)); 37 | float s2 = sin(radians(pitch)); 38 | float c3 = cos(radians(yaw)); 39 | float s3 = sin(radians(yaw)); 40 | applyMatrix( c2*c3, s1*s3+c1*c3*s2, c3*s1*s2-c1*s3, 0, 41 | -s2, c1*c2, c2*s1, 0, 42 | c2*s3, c1*s2*s3-c3*s1, c1*c3+s1*s2*s3, 0, 43 | 0, 0, 0, 1); 44 | 45 | drawArduino(); 46 | 47 | popMatrix(); // end of object 48 | 49 | // Print values to console 50 | print(roll); 51 | print("\t"); 52 | print(pitch); 53 | print("\t"); 54 | print(yaw); 55 | println(); 56 | } 57 | 58 | void serialEvent() 59 | { 60 | int newLine = 13; // new line character in ASCII 61 | String message; 62 | do { 63 | message = myPort.readStringUntil(newLine); // read from port until new line 64 | if (message != null) { 65 | String[] list = split(trim(message), " "); 66 | if (list.length >= 4 && list[0].equals("Orientation:")) { 67 | yaw = float(list[1]); // convert to float yaw 68 | pitch = float(list[2]); // convert to float pitch 69 | roll = float(list[3]); // convert to float roll 70 | } 71 | } 72 | } while (message != null); 73 | } 74 | 75 | void drawArduino() 76 | { 77 | /* function contains shape(s) that are rotated with the IMU */ 78 | stroke(0, 90, 90); // set outline colour to darker teal 79 | fill(0, 130, 130); // set fill colour to lighter teal 80 | box(300, 10, 200); // draw Arduino board base shape 81 | 82 | stroke(0); // set outline colour to black 83 | fill(80); // set fill colour to dark grey 84 | 85 | translate(60, -10, 90); // set position to edge of Arduino box 86 | box(170, 20, 10); // draw pin header as box 87 | 88 | translate(-20, 0, -180); // set position to other edge of Arduino box 89 | box(210, 20, 10); // draw other pin header as box 90 | } 91 | 92 | -------------------------------------------------------------------------------- /lib/MadgwickAHRS/keywords.txt: -------------------------------------------------------------------------------- 1 | ####################################### 2 | # Syntax Coloring Map For WiFi 3 | ####################################### 4 | 5 | ####################################### 6 | # Datatypes (KEYWORD1) 7 | ####################################### 8 | 9 | Madgwick KEYWORD1 10 | 11 | ####################################### 12 | # Methods and Functions (KEYWORD2) 13 | ####################################### 14 | 15 | update KEYWORD2 16 | updateIMU KEYWORD2 17 | getPitch KEYWORD2 18 | getYaw KEYWORD2 19 | getRoll KEYWORD2 20 | 21 | 22 | ####################################### 23 | # Constants (LITERAL1) 24 | ####################################### 25 | 26 | -------------------------------------------------------------------------------- /lib/MadgwickAHRS/library.properties: -------------------------------------------------------------------------------- 1 | name=Madgwick 2 | version=1.1 3 | author=Arduino 4 | maintainer=Arduino 5 | sentence=Helpers for MadgwickAHRS algorithm 6 | paragraph=This library wraps the official implementation of MadgwickAHRS algorithm to get orientation of an object based on accelerometer and gyroscope readings 7 | category=Data Processing 8 | url=https://github.com/arduino-libraries/MadgwickAHRS 9 | architectures=* 10 | -------------------------------------------------------------------------------- /lib/MadgwickAHRS/src/MadgwickAHRS.cpp: -------------------------------------------------------------------------------- 1 | //============================================================================================= 2 | // MadgwickAHRS.c 3 | //============================================================================================= 4 | // 5 | // Implementation of Madgwick's IMU and AHRS algorithms. 6 | // See: http://www.x-io.co.uk/open-source-imu-and-ahrs-algorithms/ 7 | // 8 | // From the x-io website "Open-source resources available on this website are 9 | // provided under the GNU General Public Licence unless an alternative licence 10 | // is provided in source." 11 | // 12 | // Date Author Notes 13 | // 29/09/2011 SOH Madgwick Initial release 14 | // 02/10/2011 SOH Madgwick Optimised for reduced CPU load 15 | // 19/02/2012 SOH Madgwick Magnetometer measurement is normalised 16 | // 17 | //============================================================================================= 18 | 19 | //------------------------------------------------------------------------------------------- 20 | // Header files 21 | 22 | #include "MadgwickAHRS.h" 23 | #include 24 | 25 | //------------------------------------------------------------------------------------------- 26 | // Definitions 27 | 28 | #define sampleFreqDef 512.0f // sample frequency in Hz 29 | #define betaDef 0.1f // 2 * proportional gain 30 | 31 | 32 | //============================================================================================ 33 | // Functions 34 | 35 | //------------------------------------------------------------------------------------------- 36 | // AHRS algorithm update 37 | 38 | Madgwick::Madgwick() { 39 | beta = betaDef; 40 | q0 = 1.0f; 41 | q1 = 0.0f; 42 | q2 = 0.0f; 43 | q3 = 0.0f; 44 | invSampleFreq = 1.0f / sampleFreqDef; 45 | anglesComputed = 0; 46 | } 47 | 48 | void Madgwick::update(float gx, float gy, float gz, float ax, float ay, float az, float mx, float my, float mz) { 49 | float recipNorm; 50 | float s0, s1, s2, s3; 51 | float qDot1, qDot2, qDot3, qDot4; 52 | float hx, hy; 53 | float _2q0mx, _2q0my, _2q0mz, _2q1mx, _2bx, _2bz, _4bx, _4bz, _2q0, _2q1, _2q2, _2q3, _2q0q2, _2q2q3, q0q0, q0q1, q0q2, q0q3, q1q1, q1q2, q1q3, q2q2, q2q3, q3q3; 54 | 55 | // Use IMU algorithm if magnetometer measurement invalid (avoids NaN in magnetometer normalisation) 56 | if((mx == 0.0f) && (my == 0.0f) && (mz == 0.0f)) { 57 | updateIMU(gx, gy, gz, ax, ay, az); 58 | return; 59 | } 60 | 61 | // Convert gyroscope degrees/sec to radians/sec 62 | gx *= 0.0174533f; 63 | gy *= 0.0174533f; 64 | gz *= 0.0174533f; 65 | 66 | // Rate of change of quaternion from gyroscope 67 | qDot1 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz); 68 | qDot2 = 0.5f * (q0 * gx + q2 * gz - q3 * gy); 69 | qDot3 = 0.5f * (q0 * gy - q1 * gz + q3 * gx); 70 | qDot4 = 0.5f * (q0 * gz + q1 * gy - q2 * gx); 71 | 72 | // Compute feedback only if accelerometer measurement valid (avoids NaN in accelerometer normalisation) 73 | if(!((ax == 0.0f) && (ay == 0.0f) && (az == 0.0f))) { 74 | 75 | // Normalise accelerometer measurement 76 | recipNorm = invSqrt(ax * ax + ay * ay + az * az); 77 | ax *= recipNorm; 78 | ay *= recipNorm; 79 | az *= recipNorm; 80 | 81 | // Normalise magnetometer measurement 82 | recipNorm = invSqrt(mx * mx + my * my + mz * mz); 83 | mx *= recipNorm; 84 | my *= recipNorm; 85 | mz *= recipNorm; 86 | 87 | // Auxiliary variables to avoid repeated arithmetic 88 | _2q0mx = 2.0f * q0 * mx; 89 | _2q0my = 2.0f * q0 * my; 90 | _2q0mz = 2.0f * q0 * mz; 91 | _2q1mx = 2.0f * q1 * mx; 92 | _2q0 = 2.0f * q0; 93 | _2q1 = 2.0f * q1; 94 | _2q2 = 2.0f * q2; 95 | _2q3 = 2.0f * q3; 96 | _2q0q2 = 2.0f * q0 * q2; 97 | _2q2q3 = 2.0f * q2 * q3; 98 | q0q0 = q0 * q0; 99 | q0q1 = q0 * q1; 100 | q0q2 = q0 * q2; 101 | q0q3 = q0 * q3; 102 | q1q1 = q1 * q1; 103 | q1q2 = q1 * q2; 104 | q1q3 = q1 * q3; 105 | q2q2 = q2 * q2; 106 | q2q3 = q2 * q3; 107 | q3q3 = q3 * q3; 108 | 109 | // Reference direction of Earth's magnetic field 110 | hx = mx * q0q0 - _2q0my * q3 + _2q0mz * q2 + mx * q1q1 + _2q1 * my * q2 + _2q1 * mz * q3 - mx * q2q2 - mx * q3q3; 111 | hy = _2q0mx * q3 + my * q0q0 - _2q0mz * q1 + _2q1mx * q2 - my * q1q1 + my * q2q2 + _2q2 * mz * q3 - my * q3q3; 112 | _2bx = sqrtf(hx * hx + hy * hy); 113 | _2bz = -_2q0mx * q2 + _2q0my * q1 + mz * q0q0 + _2q1mx * q3 - mz * q1q1 + _2q2 * my * q3 - mz * q2q2 + mz * q3q3; 114 | _4bx = 2.0f * _2bx; 115 | _4bz = 2.0f * _2bz; 116 | 117 | // Gradient decent algorithm corrective step 118 | s0 = -_2q2 * (2.0f * q1q3 - _2q0q2 - ax) + _2q1 * (2.0f * q0q1 + _2q2q3 - ay) - _2bz * q2 * (_2bx * (0.5f - q2q2 - q3q3) + _2bz * (q1q3 - q0q2) - mx) + (-_2bx * q3 + _2bz * q1) * (_2bx * (q1q2 - q0q3) + _2bz * (q0q1 + q2q3) - my) + _2bx * q2 * (_2bx * (q0q2 + q1q3) + _2bz * (0.5f - q1q1 - q2q2) - mz); 119 | s1 = _2q3 * (2.0f * q1q3 - _2q0q2 - ax) + _2q0 * (2.0f * q0q1 + _2q2q3 - ay) - 4.0f * q1 * (1 - 2.0f * q1q1 - 2.0f * q2q2 - az) + _2bz * q3 * (_2bx * (0.5f - q2q2 - q3q3) + _2bz * (q1q3 - q0q2) - mx) + (_2bx * q2 + _2bz * q0) * (_2bx * (q1q2 - q0q3) + _2bz * (q0q1 + q2q3) - my) + (_2bx * q3 - _4bz * q1) * (_2bx * (q0q2 + q1q3) + _2bz * (0.5f - q1q1 - q2q2) - mz); 120 | s2 = -_2q0 * (2.0f * q1q3 - _2q0q2 - ax) + _2q3 * (2.0f * q0q1 + _2q2q3 - ay) - 4.0f * q2 * (1 - 2.0f * q1q1 - 2.0f * q2q2 - az) + (-_4bx * q2 - _2bz * q0) * (_2bx * (0.5f - q2q2 - q3q3) + _2bz * (q1q3 - q0q2) - mx) + (_2bx * q1 + _2bz * q3) * (_2bx * (q1q2 - q0q3) + _2bz * (q0q1 + q2q3) - my) + (_2bx * q0 - _4bz * q2) * (_2bx * (q0q2 + q1q3) + _2bz * (0.5f - q1q1 - q2q2) - mz); 121 | s3 = _2q1 * (2.0f * q1q3 - _2q0q2 - ax) + _2q2 * (2.0f * q0q1 + _2q2q3 - ay) + (-_4bx * q3 + _2bz * q1) * (_2bx * (0.5f - q2q2 - q3q3) + _2bz * (q1q3 - q0q2) - mx) + (-_2bx * q0 + _2bz * q2) * (_2bx * (q1q2 - q0q3) + _2bz * (q0q1 + q2q3) - my) + _2bx * q1 * (_2bx * (q0q2 + q1q3) + _2bz * (0.5f - q1q1 - q2q2) - mz); 122 | recipNorm = invSqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3); // normalise step magnitude 123 | s0 *= recipNorm; 124 | s1 *= recipNorm; 125 | s2 *= recipNorm; 126 | s3 *= recipNorm; 127 | 128 | // Apply feedback step 129 | qDot1 -= beta * s0; 130 | qDot2 -= beta * s1; 131 | qDot3 -= beta * s2; 132 | qDot4 -= beta * s3; 133 | } 134 | 135 | // Integrate rate of change of quaternion to yield quaternion 136 | q0 += qDot1 * invSampleFreq; 137 | q1 += qDot2 * invSampleFreq; 138 | q2 += qDot3 * invSampleFreq; 139 | q3 += qDot4 * invSampleFreq; 140 | 141 | // Normalise quaternion 142 | recipNorm = invSqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3); 143 | q0 *= recipNorm; 144 | q1 *= recipNorm; 145 | q2 *= recipNorm; 146 | q3 *= recipNorm; 147 | anglesComputed = 0; 148 | } 149 | 150 | //------------------------------------------------------------------------------------------- 151 | // IMU algorithm update 152 | 153 | void Madgwick::updateIMU(float gx, float gy, float gz, float ax, float ay, float az) { 154 | float recipNorm; 155 | float s0, s1, s2, s3; 156 | float qDot1, qDot2, qDot3, qDot4; 157 | float _2q0, _2q1, _2q2, _2q3, _4q0, _4q1, _4q2 ,_8q1, _8q2, q0q0, q1q1, q2q2, q3q3; 158 | 159 | // Convert gyroscope degrees/sec to radians/sec 160 | gx *= 0.0174533f; 161 | gy *= 0.0174533f; 162 | gz *= 0.0174533f; 163 | 164 | // Rate of change of quaternion from gyroscope 165 | qDot1 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz); 166 | qDot2 = 0.5f * (q0 * gx + q2 * gz - q3 * gy); 167 | qDot3 = 0.5f * (q0 * gy - q1 * gz + q3 * gx); 168 | qDot4 = 0.5f * (q0 * gz + q1 * gy - q2 * gx); 169 | 170 | // Compute feedback only if accelerometer measurement valid (avoids NaN in accelerometer normalisation) 171 | if(!((ax == 0.0f) && (ay == 0.0f) && (az == 0.0f))) { 172 | 173 | // Normalise accelerometer measurement 174 | recipNorm = invSqrt(ax * ax + ay * ay + az * az); 175 | ax *= recipNorm; 176 | ay *= recipNorm; 177 | az *= recipNorm; 178 | 179 | // Auxiliary variables to avoid repeated arithmetic 180 | _2q0 = 2.0f * q0; 181 | _2q1 = 2.0f * q1; 182 | _2q2 = 2.0f * q2; 183 | _2q3 = 2.0f * q3; 184 | _4q0 = 4.0f * q0; 185 | _4q1 = 4.0f * q1; 186 | _4q2 = 4.0f * q2; 187 | _8q1 = 8.0f * q1; 188 | _8q2 = 8.0f * q2; 189 | q0q0 = q0 * q0; 190 | q1q1 = q1 * q1; 191 | q2q2 = q2 * q2; 192 | q3q3 = q3 * q3; 193 | 194 | // Gradient decent algorithm corrective step 195 | s0 = _4q0 * q2q2 + _2q2 * ax + _4q0 * q1q1 - _2q1 * ay; 196 | s1 = _4q1 * q3q3 - _2q3 * ax + 4.0f * q0q0 * q1 - _2q0 * ay - _4q1 + _8q1 * q1q1 + _8q1 * q2q2 + _4q1 * az; 197 | s2 = 4.0f * q0q0 * q2 + _2q0 * ax + _4q2 * q3q3 - _2q3 * ay - _4q2 + _8q2 * q1q1 + _8q2 * q2q2 + _4q2 * az; 198 | s3 = 4.0f * q1q1 * q3 - _2q1 * ax + 4.0f * q2q2 * q3 - _2q2 * ay; 199 | recipNorm = invSqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3); // normalise step magnitude 200 | s0 *= recipNorm; 201 | s1 *= recipNorm; 202 | s2 *= recipNorm; 203 | s3 *= recipNorm; 204 | 205 | // Apply feedback step 206 | qDot1 -= beta * s0; 207 | qDot2 -= beta * s1; 208 | qDot3 -= beta * s2; 209 | qDot4 -= beta * s3; 210 | } 211 | 212 | // Integrate rate of change of quaternion to yield quaternion 213 | q0 += qDot1 * invSampleFreq; 214 | q1 += qDot2 * invSampleFreq; 215 | q2 += qDot3 * invSampleFreq; 216 | q3 += qDot4 * invSampleFreq; 217 | 218 | // Normalise quaternion 219 | recipNorm = invSqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3); 220 | q0 *= recipNorm; 221 | q1 *= recipNorm; 222 | q2 *= recipNorm; 223 | q3 *= recipNorm; 224 | anglesComputed = 0; 225 | } 226 | 227 | //------------------------------------------------------------------------------------------- 228 | // Fast inverse square-root 229 | // See: http://en.wikipedia.org/wiki/Fast_inverse_square_root 230 | 231 | float Madgwick::invSqrt(float x) { 232 | float halfx = 0.5f * x; 233 | float y = x; 234 | long i = *(long*)&y; 235 | i = 0x5f3759df - (i>>1); 236 | y = *(float*)&i; 237 | y = y * (1.5f - (halfx * y * y)); 238 | y = y * (1.5f - (halfx * y * y)); 239 | return y; 240 | } 241 | 242 | //------------------------------------------------------------------------------------------- 243 | 244 | void Madgwick::computeAngles() 245 | { 246 | roll = atan2f(q0*q1 + q2*q3, 0.5f - q1*q1 - q2*q2); 247 | pitch = asinf(-2.0f * (q1*q3 - q0*q2)); 248 | yaw = atan2f(q1*q2 + q0*q3, 0.5f - q2*q2 - q3*q3); 249 | anglesComputed = 1; 250 | } 251 | 252 | -------------------------------------------------------------------------------- /lib/MadgwickAHRS/src/MadgwickAHRS.h: -------------------------------------------------------------------------------- 1 | //============================================================================================= 2 | // MadgwickAHRS.h 3 | //============================================================================================= 4 | // 5 | // Implementation of Madgwick's IMU and AHRS algorithms. 6 | // See: http://www.x-io.co.uk/open-source-imu-and-ahrs-algorithms/ 7 | // 8 | // From the x-io website "Open-source resources available on this website are 9 | // provided under the GNU General Public Licence unless an alternative licence 10 | // is provided in source." 11 | // 12 | // Date Author Notes 13 | // 29/09/2011 SOH Madgwick Initial release 14 | // 02/10/2011 SOH Madgwick Optimised for reduced CPU load 15 | // 16 | //============================================================================================= 17 | #ifndef MadgwickAHRS_h 18 | #define MadgwickAHRS_h 19 | #include 20 | 21 | //-------------------------------------------------------------------------------------------- 22 | // Variable declaration 23 | class Madgwick{ 24 | private: 25 | static float invSqrt(float x); 26 | float beta; // algorithm gain 27 | float q0; 28 | float q1; 29 | float q2; 30 | float q3; // quaternion of sensor frame relative to auxiliary frame 31 | float invSampleFreq; 32 | float roll; 33 | float pitch; 34 | float yaw; 35 | char anglesComputed; 36 | void computeAngles(); 37 | 38 | //------------------------------------------------------------------------------------------- 39 | // Function declarations 40 | public: 41 | Madgwick(void); 42 | void begin(float sampleFrequency) { invSampleFreq = 1.0f / sampleFrequency; } 43 | void update(float gx, float gy, float gz, float ax, float ay, float az, float mx, float my, float mz); 44 | void updateIMU(float gx, float gy, float gz, float ax, float ay, float az); 45 | //float getPitch(){return atan2f(2.0f * q2 * q3 - 2.0f * q0 * q1, 2.0f * q0 * q0 + 2.0f * q3 * q3 - 1.0f);}; 46 | //float getRoll(){return -1.0f * asinf(2.0f * q1 * q3 + 2.0f * q0 * q2);}; 47 | //float getYaw(){return atan2f(2.0f * q1 * q2 - 2.0f * q0 * q3, 2.0f * q0 * q0 + 2.0f * q1 * q1 - 1.0f);}; 48 | float getRoll() { 49 | if (!anglesComputed) computeAngles(); 50 | return roll * 57.29578f; 51 | } 52 | float getPitch() { 53 | if (!anglesComputed) computeAngles(); 54 | return pitch * 57.29578f; 55 | } 56 | float getYaw() { 57 | if (!anglesComputed) computeAngles(); 58 | return yaw * 57.29578f + 180.0f; 59 | } 60 | float getRollRadians() { 61 | if (!anglesComputed) computeAngles(); 62 | return roll; 63 | } 64 | float getPitchRadians() { 65 | if (!anglesComputed) computeAngles(); 66 | return pitch; 67 | } 68 | float getYawRadians() { 69 | if (!anglesComputed) computeAngles(); 70 | return yaw; 71 | } 72 | }; 73 | #endif 74 | 75 | -------------------------------------------------------------------------------- /lib/readme.txt: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for the project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link to executable file. 4 | 5 | The source code of each library should be placed in separate directory, like 6 | "lib/private_lib/[here are source files]". 7 | 8 | For example, see how can be organized `Foo` and `Bar` libraries: 9 | 10 | |--lib 11 | | |--Bar 12 | | | |--docs 13 | | | |--examples 14 | | | |--src 15 | | | |- Bar.c 16 | | | |- Bar.h 17 | | |--Foo 18 | | | |- Foo.c 19 | | | |- Foo.h 20 | | |- readme.txt --> THIS FILE 21 | |- platformio.ini 22 | |--src 23 | |- main.c 24 | 25 | Then in `src/main.c` you should use: 26 | 27 | #include 28 | #include 29 | 30 | // rest H/C/CPP code 31 | 32 | PlatformIO will find your libraries automatically, configure preprocessor's 33 | include paths and build them. 34 | 35 | More information about PlatformIO Library Dependency Finder 36 | - http://docs.platformio.org/page/librarymanager/ldf.html 37 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; http://docs.platformio.org/page/projectconf.html 10 | 11 | [env:featheresp32] 12 | ;platform = atmelavr 13 | platform = espressif32 14 | ;platform = https://github.com/platformio/platform-espressif32.git#feature/stage 15 | board = featheresp32 16 | ;board = nanoatmega328 17 | framework = arduino 18 | build_flags= -fexceptions 19 | 20 | lib_deps= 21 | ESP32 BLE Arduino 22 | WS2812FX 23 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #define SAMPLE_RATE 20 12 | #define REPORTING_PERIOD_MS 20 13 | #define DEBUG false 14 | #define IMU_SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" 15 | #define ACC_CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26b8" 16 | #define GYRO_CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" 17 | #define ANGLE_CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26c8" 18 | 19 | #define COLOR_SERVICE_UUID "5051269d-7791-4332-b534-c6caab0ae14b" 20 | #define COLOR_CHARACTERISTIC_UUID "3bac26a0-e902-44d5-8820-d8698543147b" 21 | 22 | #define LED_PIN 18 23 | #define NUMPIXELS 16 24 | #define FX_SPEED 220 25 | #define FX_BRIGHTNESS 100 26 | 27 | WS2812FX ws2812fx = WS2812FX(NUMPIXELS, LED_PIN, NEO_GRB + NEO_KHZ800); 28 | 29 | class ColorCallbacks : public BLECharacteristicCallbacks 30 | { 31 | void onWrite(BLECharacteristic *pCharacteristic) 32 | { 33 | std::string value = pCharacteristic->getValue(); 34 | if (value.length() == 3) 35 | { 36 | int red = value[0]; 37 | int green = value[1]; 38 | int blue = value[2]; 39 | 40 | if (DEBUG) 41 | { 42 | Serial.println("Valid"); 43 | Serial.print("Value "); 44 | Serial.print(red); 45 | Serial.print(", "); 46 | Serial.print(green); 47 | Serial.print(", "); 48 | Serial.println(blue); 49 | } 50 | 51 | ws2812fx.setColor(red, green, blue); 52 | } 53 | } 54 | }; 55 | 56 | BLEServer *pServer = NULL; 57 | BLEService *imuService = NULL; 58 | BLECharacteristic *accelerometerDataCharacteristic = NULL; 59 | BLECharacteristic *gyroscopeDataCharacteristic = NULL; 60 | BLECharacteristic *angleDataCharacteristic = NULL; 61 | BLEService *colorService = NULL; 62 | BLECharacteristic *colorDataCharacteristic = NULL; 63 | BLEAdvertising *pAdvertising = NULL; 64 | 65 | //MPU6050 I2C Address 66 | #define MPU 0x68 67 | #define PWR_MGMT_1 0x6B 68 | #define GYRO_CONFIG 0x1B 69 | #define ACCEL_CONFIG 0x1C 70 | #define ACCEL_XOUT 0x3B 71 | 72 | //Sensors Data 73 | #define SAMPLE_SIZE 30 74 | int16_t AcX[SAMPLE_SIZE], AcY[SAMPLE_SIZE], AcZ[SAMPLE_SIZE], Tmp[SAMPLE_SIZE], GyX[SAMPLE_SIZE], GyY[SAMPLE_SIZE], GyZ[SAMPLE_SIZE]; 75 | int currentSampleIndex = 0; 76 | Madgwick filter; 77 | 78 | const int ACC_MODE = 0; 79 | const int GYRO_MODE = 0; 80 | float gyroScales[4] = {131.0f, 65.5f, 32.8f, 16.4f}; 81 | float accScales[4] = {16384.0f, 8192.0f, 4096.0f, 2048.0f}; 82 | /* SCALING FACTORS 83 | 84 | Angular Velocity Limit | Sensitivity 85 | ---------------------------------------- 86 | 250º/s | 131 87 | 500º/s | 65.5 88 | 1000º/s | 32.8 89 | 2000º/s | 16.4 90 | 91 | Acceleration Limit | Sensitivity 92 | ---------------------------------------- 93 | 2g | 16,384 94 | 4g | 8,192 95 | 8g | 4,096 96 | 16g | 2,048 97 | */ 98 | 99 | long lastReport = 0; 100 | long lastSample = 0; 101 | 102 | typedef union { 103 | float value; 104 | byte bytes[4]; 105 | } Reading; 106 | 107 | void writeRegMPU(int reg, int val) 108 | { 109 | Wire.beginTransmission(MPU); 110 | Wire.write(reg); 111 | Wire.write(val); 112 | Wire.endTransmission(false); 113 | } 114 | 115 | void setupMPU() 116 | { 117 | Wire.begin(21, 22); 118 | writeRegMPU(PWR_MGMT_1, 0); 119 | writeRegMPU(GYRO_CONFIG, GYRO_MODE); // Gyro config 120 | writeRegMPU(ACCEL_CONFIG, ACC_MODE); // Acc config 121 | } 122 | 123 | void readMPU() 124 | { 125 | Wire.beginTransmission(MPU); 126 | Wire.write(ACCEL_XOUT); // starting with register 0x3B (ACCEL_XOUT_H) 127 | Wire.endTransmission(false); 128 | // Request sensor data 129 | Wire.requestFrom(MPU, 14); 130 | // Store sensors values 131 | AcX[currentSampleIndex] = Wire.read() << 8 | Wire.read(); //0x3B (ACCEL_XOUT_H) & 0x3C (ACCEL_XOUT_L) 132 | AcY[currentSampleIndex] = Wire.read() << 8 | Wire.read(); //0x3D (ACCEL_YOUT_H) & 0x3E (ACCEL_YOUT_L) 133 | AcZ[currentSampleIndex] = Wire.read() << 8 | Wire.read(); //0x3F (ACCEL_ZOUT_H) & 0x40 (ACCEL_ZOUT_L) 134 | Tmp[currentSampleIndex] = Wire.read() << 8 | Wire.read(); //0x41 (TEMP_OUT_H) & 0x42 (TEMP_OUT_L) 135 | GyX[currentSampleIndex] = Wire.read() << 8 | Wire.read(); //0x43 (GYRO_XOUT_H) & 0x44 (GYRO_XOUT_L) 136 | GyY[currentSampleIndex] = Wire.read() << 8 | Wire.read(); //0x45 (GYRO_YOUT_H) & 0x46 (GYRO_YOUT_L) 137 | GyZ[currentSampleIndex] = Wire.read() << 8 | Wire.read(); //0x47 (GYRO_ZOUT_H) & 0x48 (GYRO_ZOUT_L) 138 | currentSampleIndex++; 139 | if (currentSampleIndex >= SAMPLE_SIZE) 140 | { 141 | currentSampleIndex = 0; 142 | } 143 | } 144 | 145 | float avgSample(int16_t values[SAMPLE_SIZE]) 146 | { 147 | float value = 0; 148 | for (int i = 0; i < SAMPLE_SIZE; i++) 149 | { 150 | value += values[i] * 1.0f; 151 | } 152 | return (value / (1.0f * SAMPLE_SIZE)); 153 | } 154 | 155 | void setup() 156 | { 157 | Serial.begin(9600); 158 | 159 | setupMPU(); 160 | filter.begin(SAMPLE_RATE); 161 | 162 | ws2812fx.init(); 163 | ws2812fx.setBrightness(FX_BRIGHTNESS); 164 | ws2812fx.setSpeed(FX_SPEED); 165 | ws2812fx.setColor(0x00FF00); 166 | ws2812fx.setMode(FX_MODE_BREATH); 167 | ws2812fx.start(); 168 | 169 | BLEDevice::init("AwesomeMinecraftBlock"); 170 | 171 | pServer = BLEDevice::createServer(); 172 | 173 | imuService = pServer->createService(IMU_SERVICE_UUID); 174 | accelerometerDataCharacteristic = imuService->createCharacteristic( 175 | ACC_CHARACTERISTIC_UUID, 176 | BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); 177 | gyroscopeDataCharacteristic = imuService->createCharacteristic( 178 | GYRO_CHARACTERISTIC_UUID, 179 | BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); 180 | angleDataCharacteristic = imuService->createCharacteristic( 181 | ANGLE_CHARACTERISTIC_UUID, 182 | BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); 183 | imuService->start(); 184 | 185 | colorService = pServer->createService(COLOR_SERVICE_UUID); 186 | colorDataCharacteristic = colorService->createCharacteristic( 187 | COLOR_CHARACTERISTIC_UUID, 188 | BLECharacteristic::PROPERTY_WRITE); 189 | colorDataCharacteristic->setCallbacks(new ColorCallbacks()); 190 | colorService->start(); 191 | 192 | pAdvertising = pServer->getAdvertising(); 193 | pAdvertising->start(); 194 | 195 | for (int i = 0; i < SAMPLE_SIZE; i++) 196 | { 197 | readMPU(); 198 | } 199 | } 200 | 201 | void loop() 202 | { 203 | // Make sure to call update as fast as possible 204 | ws2812fx.service(); 205 | readMPU(); 206 | 207 | long dT = millis() - lastSample; 208 | if (dT > SAMPLE_RATE) 209 | { 210 | 211 | float ax = avgSample(AcX) / accScales[ACC_MODE] + 0.08f; 212 | float ay = avgSample(AcY) / accScales[ACC_MODE] - 0.04f; 213 | float az = avgSample(AcZ) / accScales[ACC_MODE] + 0.07f; 214 | 215 | float gx = avgSample(GyX) / gyroScales[GYRO_MODE]; 216 | float gy = avgSample(GyY) / gyroScales[GYRO_MODE]; 217 | float gz = avgSample(GyZ) / gyroScales[GYRO_MODE]; 218 | 219 | float gyroScale = 0.5f; 220 | 221 | filter.updateIMU(gx * gyroScale, gy * gyroScale, gz * gyroScale, ax, ay, az); 222 | lastSample = millis(); 223 | } 224 | 225 | dT = millis() - lastReport; 226 | 227 | if (dT > REPORTING_PERIOD_MS) 228 | { 229 | float ax = avgSample(AcX) / accScales[ACC_MODE] + 0.08f; 230 | float ay = avgSample(AcY) / accScales[ACC_MODE] - 0.04f; 231 | float az = avgSample(AcZ) / accScales[ACC_MODE] + 0.07f; 232 | 233 | float gx = avgSample(GyX) / gyroScales[GYRO_MODE]; 234 | float gy = avgSample(GyY) / gyroScales[GYRO_MODE]; 235 | float gz = avgSample(GyZ) / gyroScales[GYRO_MODE]; 236 | 237 | float roll = filter.getRoll(); 238 | float pitch = filter.getPitch(); 239 | float heading = filter.getYaw(); 240 | 241 | if (DEBUG) 242 | { 243 | Serial.print("AcX:"); 244 | Serial.print(ax); 245 | Serial.print(", AcY:"); 246 | Serial.print(ay); 247 | Serial.print(", AcZ:"); 248 | Serial.println(az); 249 | //Serial.print("Tmp:"); 250 | //Serial.println(Tmp); 251 | Serial.print("GyX:"); 252 | Serial.print(gx); 253 | Serial.print(", GyY:"); 254 | Serial.print(gy); 255 | Serial.print(", GyZ:"); 256 | Serial.println(gz); 257 | 258 | Serial.print("Orientation: "); 259 | Serial.print(heading); 260 | Serial.print(" "); 261 | Serial.print(pitch); 262 | Serial.print(" "); 263 | Serial.println(roll); 264 | } 265 | 266 | // Error if all zero 267 | if (ax == 0.0f && ay == 0.0f && az == 0.0f) 268 | { 269 | ws2812fx.setColor(0xFF0000); 270 | ws2812fx.setMode(FX_MODE_STATIC); 271 | } 272 | 273 | Reading axReading, ayReading, azReading; 274 | axReading.value = ax; 275 | ayReading.value = ay; 276 | azReading.value = az; 277 | 278 | Reading gxReading, gyReading, gzReading; 279 | gxReading.value = gx; 280 | gyReading.value = gy; 281 | gzReading.value = gz; 282 | 283 | Reading agxReading, agyReading, agzReading; 284 | agxReading.value = roll; 285 | agyReading.value = pitch; 286 | agzReading.value = heading; 287 | 288 | uint8_t accData[12] = { 289 | (uint8_t)axReading.bytes[0], 290 | (uint8_t)axReading.bytes[1], 291 | (uint8_t)axReading.bytes[2], 292 | (uint8_t)axReading.bytes[3], 293 | (uint8_t)ayReading.bytes[0], 294 | (uint8_t)ayReading.bytes[1], 295 | (uint8_t)ayReading.bytes[2], 296 | (uint8_t)ayReading.bytes[3], 297 | (uint8_t)azReading.bytes[0], 298 | (uint8_t)azReading.bytes[1], 299 | (uint8_t)azReading.bytes[2], 300 | (uint8_t)azReading.bytes[3]}; 301 | 302 | uint8_t gyroData[12] = { 303 | (uint8_t)gxReading.bytes[0], 304 | (uint8_t)gxReading.bytes[1], 305 | (uint8_t)gxReading.bytes[2], 306 | (uint8_t)gxReading.bytes[3], 307 | (uint8_t)gyReading.bytes[0], 308 | (uint8_t)gyReading.bytes[1], 309 | (uint8_t)gyReading.bytes[2], 310 | (uint8_t)gyReading.bytes[3], 311 | (uint8_t)gzReading.bytes[0], 312 | (uint8_t)gzReading.bytes[1], 313 | (uint8_t)gzReading.bytes[2], 314 | (uint8_t)gzReading.bytes[3]}; 315 | 316 | uint8_t angleData[12] = { 317 | (uint8_t)agxReading.bytes[0], 318 | (uint8_t)agxReading.bytes[1], 319 | (uint8_t)agxReading.bytes[2], 320 | (uint8_t)agxReading.bytes[3], 321 | (uint8_t)agyReading.bytes[0], 322 | (uint8_t)agyReading.bytes[1], 323 | (uint8_t)agyReading.bytes[2], 324 | (uint8_t)agyReading.bytes[3], 325 | (uint8_t)agzReading.bytes[0], 326 | (uint8_t)agzReading.bytes[1], 327 | (uint8_t)agzReading.bytes[2], 328 | (uint8_t)agzReading.bytes[3]}; 329 | 330 | accelerometerDataCharacteristic->setValue(accData, 12); 331 | //accelerometerDataCharacteristic->notify(); 332 | 333 | gyroscopeDataCharacteristic->setValue(gyroData, 12); 334 | //gyroscopeDataCharacteristic->notify(); 335 | 336 | angleDataCharacteristic->setValue(angleData, 12); 337 | //angleDataCharacteristic->notify(); 338 | 339 | lastReport = millis(); 340 | } 341 | } -------------------------------------------------------------------------------- /webapp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://alvarowolfx.github.io/physical-cube-web-bluetooth-esp32", 6 | "dependencies": { 7 | "gh-pages": "^1.1.0", 8 | "react": "^15.6.1", 9 | "react-color": "^2.13.8", 10 | "react-dom": "^15.6.1", 11 | "react-three-renderer": "^3.2.4", 12 | "react-throttle": "^0.3.0", 13 | "three": "^0.86.0" 14 | }, 15 | "devDependencies": { 16 | "react-scripts": "1.1.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject", 23 | "predeploy": "npm run build", 24 | "deploy": "gh-pages -d build" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /webapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvarowolfx/physical-cube-imu-web-bluetooth-esp32/5bce851e8bc3438057af28c84ebdce5d5e9bb2bf/webapp/public/favicon.ico -------------------------------------------------------------------------------- /webapp/public/images/redstone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvarowolfx/physical-cube-imu-web-bluetooth-esp32/5bce851e8bc3438057af28c84ebdce5d5e9bb2bf/webapp/public/images/redstone.png -------------------------------------------------------------------------------- /webapp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React + WebUSB 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /webapp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /webapp/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | background-color: #d4d4d4; 6 | } 7 | 8 | .app { 9 | text-align: center; 10 | } 11 | 12 | .logo { 13 | animation: app-logo-spin infinite 20s linear; 14 | height: 80px; 15 | } 16 | 17 | header { 18 | display: flex; 19 | flex-direction: row; 20 | align-items: center; 21 | align-content: center; 22 | align-self: center; 23 | background-color: #222; 24 | height: 60px; 25 | padding: 20px; 26 | color: white; 27 | } 28 | 29 | .title { 30 | font-size: 1.5em; 31 | } 32 | 33 | .container { 34 | margin: 0; 35 | display: flex; 36 | flex-direction: column; 37 | align-items: center; 38 | background-color: #d4d4d4; 39 | } 40 | 41 | .controls { 42 | color: white; 43 | display: flex; 44 | flex-direction: column; 45 | align-items: center; 46 | } 47 | 48 | @keyframes app-logo-spin { 49 | from { 50 | transform: rotate(0deg); 51 | } 52 | to { 53 | transform: rotate(360deg); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /webapp/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import React3 from 'react-three-renderer'; 3 | import * as THREE from 'three'; 4 | import { SketchPicker } from 'react-color'; 5 | import { Throttle } from 'react-throttle'; 6 | import './App.css'; 7 | import MinecraftBlock from './MinecraftBlock'; 8 | import BLEBlock from './BLEBlock'; 9 | 10 | class App extends Component { 11 | state = { 12 | connected: false, 13 | selectedColor: { r: 255, b: 0, g: 0 }, 14 | selectedHex: '#FF0000', 15 | data: { 16 | acc: { x: 0, y: 0, z: 0 }, 17 | gyro: { x: 0, y: 0, z: 0 }, 18 | angle: { x: 0, y: 0, z: 0 } 19 | }, 20 | block: null, 21 | cubeRotation: new THREE.Euler(-50, 100, 100) 22 | }; 23 | 24 | constructor(props) { 25 | super(props); 26 | 27 | this.cameraPosition = new THREE.Vector3(0, 0, 5); 28 | this.directionalLightPosition = new THREE.Vector3(0, 1, 0); 29 | this.scenePosition = new THREE.Vector3(0, 0, 0); 30 | } 31 | 32 | _onAnimate = () => { 33 | const { data, connected } = this.state; 34 | if (connected) { 35 | let { angle: { x, y, z } } = data; 36 | const mapAcc = value => { 37 | return -1 * value * Math.PI / 180; 38 | }; 39 | x = mapAcc(x); 40 | y = mapAcc(y); 41 | z = mapAcc(z); 42 | 43 | this.setState({ 44 | cubeRotation: new THREE.Euler(x, z, y) 45 | }); 46 | } 47 | }; 48 | 49 | connect = async () => { 50 | const block = new BLEBlock(); 51 | await block.connect(); 52 | 53 | block.updateColor(this.state.selectedColor); 54 | 55 | this.setState({ 56 | connected: true, 57 | block 58 | }); 59 | 60 | block.addListener(data => { 61 | this.setState({ 62 | data 63 | }); 64 | }); 65 | }; 66 | 67 | onConnectClick = () => { 68 | if (!this.state.connected) { 69 | this.connect(); 70 | } else { 71 | this.state.block && this.state.block.disconnect(); 72 | this.setState({ 73 | connected: false, 74 | block: null 75 | }); 76 | } 77 | }; 78 | 79 | handleColorChange = color => { 80 | const { rgb, hex } = color; 81 | const { block } = this.state; 82 | if (block) { 83 | block.updateColor(rgb); 84 | } 85 | 86 | this.setState({ 87 | selectedColor: rgb, 88 | selectedHex: hex 89 | }); 90 | }; 91 | 92 | render() { 93 | const width = window.innerWidth; // canvas width 94 | const height = window.innerHeight - 100; // canvas height 95 | 96 | const { 97 | connected, 98 | selectedColor, 99 | data, 100 | selectedHex, 101 | cubeRotation 102 | } = this.state; 103 | return ( 104 |
105 |
106 |

ESP32 + Bluetooth LE + WebBluetooth Demo

107 |
108 |
109 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 126 | 134 | 135 | 136 | 137 |
138 |
139 |
140 | 143 |
144 |
145 |
146 | 147 | 151 | 152 |
153 | Accelerometer:
154 | {JSON.stringify(data.acc)} 155 |
156 | Gyroscope:
{JSON.stringify(data.gyro)} 157 |
158 | Angle:
{JSON.stringify(data.angle)} 159 |
160 |
161 |
162 |
163 | ); 164 | } 165 | } 166 | 167 | export default App; 168 | -------------------------------------------------------------------------------- /webapp/src/BLEBlock.js: -------------------------------------------------------------------------------- 1 | const IMU_SERVICE_UUID = '4fafc201-1fb5-459e-8fcc-c5c9c331914b'; 2 | const ACC_CHARACTERISTIC_UUID = 'beb5483e-36e1-4688-b7f5-ea07361b26b8'; 3 | const GYRO_CHARACTERISTIC_UUID = 'beb5483e-36e1-4688-b7f5-ea07361b26a8'; 4 | const ANGLE_CHARACTERISTIC_UUID = 'beb5483e-36e1-4688-b7f5-ea07361b26c8'; 5 | 6 | const COLOR_SERVICE_UUID = '5051269d-7791-4332-b534-c6caab0ae14b'; 7 | const COLOR_CHARACTERISTIC_UUID = '3bac26a0-e902-44d5-8820-d8698543147b'; 8 | 9 | const BLE_MODULE_NAME = 'AwesomeMinecraftBlock'; 10 | 11 | export default class BLEBlock { 12 | async connect() { 13 | this.device = await navigator.bluetooth.requestDevice({ 14 | filters: [ 15 | { 16 | name: BLE_MODULE_NAME 17 | } 18 | ], 19 | optionalServices: [ 20 | IMU_SERVICE_UUID, 21 | COLOR_SERVICE_UUID, 22 | COLOR_CHARACTERISTIC_UUID, 23 | ACC_CHARACTERISTIC_UUID, 24 | GYRO_CHARACTERISTIC_UUID, 25 | ANGLE_CHARACTERISTIC_UUID 26 | ] 27 | }); 28 | 29 | const server = await this.device.gatt.connect(); 30 | 31 | this.imuService = await server.getPrimaryService(IMU_SERVICE_UUID); 32 | 33 | this.accCharacteristic = await this.imuService.getCharacteristic( 34 | ACC_CHARACTERISTIC_UUID 35 | ); 36 | console.log('AccCharacteristic', this.accCharacteristic.properties); 37 | 38 | this.gyroCharacteristic = await this.imuService.getCharacteristic( 39 | GYRO_CHARACTERISTIC_UUID 40 | ); 41 | console.log('GyroCharacteristic', this.gyroCharacteristic.properties); 42 | 43 | this.angleCharacteristic = await this.imuService.getCharacteristic( 44 | ANGLE_CHARACTERISTIC_UUID 45 | ); 46 | console.log('AngleCharacteristic', this.angleCharacteristic.properties); 47 | 48 | this.colorService = await server.getPrimaryService(COLOR_SERVICE_UUID); 49 | 50 | this.colorCharacteristic = await this.colorService.getCharacteristic( 51 | COLOR_CHARACTERISTIC_UUID 52 | ); 53 | } 54 | 55 | disconnect() { 56 | this.device.gatt.disconnect(); 57 | } 58 | 59 | _readValueToPos(value) { 60 | let arr = []; 61 | for (let i = 0; i < value.byteLength; i += 4) { 62 | let buf = new ArrayBuffer(4); 63 | let view = new DataView(buf); 64 | for (let j = 0; j < 4; j++) { 65 | view.setUint8(j, value.getUint8(i + j)); 66 | } 67 | let pos = view.getFloat32(0, true); 68 | arr.push(pos.toFixed(2)); 69 | } 70 | return { 71 | x: arr[0], 72 | y: arr[1], 73 | z: arr[2] 74 | }; 75 | } 76 | 77 | async addListener(callback) { 78 | setInterval(async () => { 79 | if (this.reading) { 80 | return; 81 | } 82 | this.reading = true; 83 | const accValue = await this.accCharacteristic.readValue(); 84 | const gyroValue = await this.gyroCharacteristic.readValue(); 85 | const angleValue = await this.angleCharacteristic.readValue(); 86 | 87 | const acc = this._readValueToPos(accValue); 88 | const gyro = this._readValueToPos(gyroValue); 89 | const angle = this._readValueToPos(angleValue); 90 | 91 | callback({ acc, gyro, angle }); 92 | this.reading = false; 93 | }, 50); 94 | } 95 | 96 | removeListener(callback) {} 97 | 98 | updateColor({ r, g, b }) { 99 | let data = new Uint8Array([r, g, b]); 100 | this.colorCharacteristic.writeValue(data); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /webapp/src/MinecraftBlock.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as THREE from 'three'; 3 | 4 | const MinecraftBlock = ({ rotation }) => { 5 | return ( 6 | 7 | 8 | {/* */} 9 | 10 | 11 | ); 12 | }; 13 | 14 | const MinecraftTexture = () => { 15 | return ( 16 | 23 | ); 24 | }; 25 | 26 | const MinecraftMaterial = () => { 27 | return ( 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | MinecraftBlock.Texture = MinecraftTexture; 35 | MinecraftBlock.Material = MinecraftMaterial; 36 | export default MinecraftBlock; 37 | -------------------------------------------------------------------------------- /webapp/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import registerServiceWorker from './registerServiceWorker'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | registerServiceWorker(); 8 | -------------------------------------------------------------------------------- /webapp/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------