Components and supplies
Arduino Mega 2560
Resistor 10k ohm
RGB LCD Shield Kit, 16x2 Character Display
Photo resistor
6 DOF Sensor - MPU6050
Capacitive Touch Sensor Breakout - MPR121
Resistor 221 ohm
Perma-Proto Breadboard Half Size
Arduino UNO
nRF24 Module (Generic)
LED (generic)
Rotary Encoder with Push-Button
Tools and machines
Glove
a small box
Project description
Code
MPU6050_data_func.h
c_cpp
1uint8_t mpuIntStatus; // holds actual interrupt status byte from MPU 2uint16_t packetSize; // expected DMP packet size (default is 42 bytes) 3uint16_t fifoCount; // count of all bytes currently in FIFO 4uint8_t fifoBuffer[64]; // FIFO storage buffer 5Quaternion q; // [w, x, y, z] quaternion container 6VectorFloat gravity; // [x, y, z] gravity vector 7uint8_t teapotPacket[14] = { '$', 0x02, 0,0, 0,0, 0,0, 0,0, 0x00, 0x00, '\r', '\n' }; 8volatile bool mpuInterrupt = false; // indicates whether MPU interrupt pin has gone high 9 10void dmpDataReady(){ 11 12 mpuInterrupt=true; 13 14} 15 16void get_data(MPU6050 mpu,float ypr[3]){ 17 18 //ypr[0]=0.0f; ypr[1]=0.0f; ypr[2]=0.0f; 19 mpuIntStatus=mpu.getIntStatus(); 20 fifoCount=mpu.getFIFOCount(); 21 if(mpuIntStatus&0x02){ 22 while(fifoCount<packetSize) fifoCount=mpu.getFIFOCount(); 23 mpu.getFIFOBytes(fifoBuffer,packetSize); 24 fifoCount-=packetSize; 25 mpu.dmpGetQuaternion(&q,fifoBuffer); 26 mpu.dmpGetGravity(&gravity,&q); 27 mpu.dmpGetYawPitchRoll(ypr,&q,&gravity); 28 } 29 30}
CalibratingBox.ino
c_cpp
1#include<LiquidCrystal.h> 2 3int valore; 4String testo; 5bool calibrated=false,calibrating=false; 6uint8_t red_led=3,green_led=2,photo_resistor=A4; 7 8LiquidCrystal lcd(7,8,9,10,11,12); 9 10void setup(){ 11 12 Serial.begin(115200); 13 lcd.begin(16,2); 14 pinMode(photo_resistor,INPUT); 15 pinMode(green_led,OUTPUT); 16 pinMode(red_led,OUTPUT); 17 digitalWrite(green_led,LOW); 18 digitalWrite(red_led,LOW); 19 20} 21void loop(){ 22 23 valore=analogRead(photo_resistor); 24 if(!calibrating&&!calibrated&&valore<50){ 25 lampeggio(red_led,10,500); 26 digitalWrite(red_led,HIGH); 27 Serial.println("S"); 28 calibrating=true; 29 }else if(calibrated&&valore>300){ 30 digitalWrite(green_led,LOW); 31 digitalWrite(red_led,LOW); 32 calibrating=false; 33 }if(calibrating){ 34 testo=""; 35 while(Serial.available()) testo+=(char)Serial.read(); 36 if(testo!="") lcd.clear(); 37 if(testo=="FINISHED"){ 38 digitalWrite(green_led,HIGH); 39 digitalWrite(red_led,LOW); 40 calibrated=true; 41 } 42 lcd.setCursor(0,0); 43 lcd.print(testo); 44 } 45 46} 47 48void lampeggio(uint8_t led,int n,int millisec){ 49 50 for(int i=0;i<n;++i){ 51 digitalWrite(led,HIGH); 52 delay(millisec); 53 digitalWrite(led,LOW); 54 delay(millisec); 55 } 56} 57
MPU6050_data_func.h
c_cpp
1uint8_t mpuIntStatus; // holds actual interrupt status byte from MPU 2uint16_t 3 packetSize; // expected DMP packet size (default is 42 bytes) 4uint16_t fifoCount; 5 // count of all bytes currently in FIFO 6uint8_t fifoBuffer[64]; // FIFO 7 storage buffer 8Quaternion q; // [w, x, y, z] quaternion container 9VectorFloat 10 gravity; // [x, y, z] gravity vector 11uint8_t teapotPacket[14] = 12 { '$', 0x02, 0,0, 0,0, 0,0, 0,0, 0x00, 0x00, '\ ', '\ 13' }; 14volatile bool mpuInterrupt 15 = false; // indicates whether MPU interrupt pin has gone high 16 17void dmpDataReady(){ 18 19 20 mpuInterrupt=true; 21 22} 23 24void get_data(MPU6050 mpu,float ypr[3]){ 25 26 //ypr[0]=0.0f; 27 ypr[1]=0.0f; ypr[2]=0.0f; 28 mpuIntStatus=mpu.getIntStatus(); 29 fifoCount=mpu.getFIFOCount(); 30 31 if(mpuIntStatus&0x02){ 32 while(fifoCount<packetSize) fifoCount=mpu.getFIFOCount(); 33 34 mpu.getFIFOBytes(fifoBuffer,packetSize); 35 fifoCount-=packetSize; 36 37 mpu.dmpGetQuaternion(&q,fifoBuffer); 38 mpu.dmpGetGravity(&gravity,&q); 39 40 mpu.dmpGetYawPitchRoll(ypr,&q,&gravity); 41 } 42 43}
GestureGlove.ino
c_cpp
1#include "Wire.h" 2#include "I2Cdev.h" 3#include "MPU6050_6Axis_MotionApps20.h" 4#include 5 "Calibration_func.h" 6#include "MPU6050_data_func.h" 7 8float ypr[3]; 9String 10 testo; 11bool calibrate=false; 12int lastMSB=0,lastLSB=0; 13long lastencoderValue=0; 14const 15 uint8_t encoderPin1=2,encoderPin2=3,touch_pin=8; 16 17volatile int lastEncoded=0; 18volatile 19 long int encoderValue=0; 20 21MPU6050 mpu(0x68); 22 23void setup(){ 24 25 26 Serial.begin(115200); 27 Wire.begin(); 28 TWBR=24; 29 /* 30 mpu.initialize(); 31 32 mpu6050_calibration(mpu); 33 mpu.dmpInitialize(); 34 mpu.setDMPEnabled(true); 35 36 attachInterrupt(0,dmpDataReady,RISING); 37 packetSize=mpu.dmpGetFIFOPacketSize(); 38 39 */ 40 pinMode(touch_pin,INPUT); 41 pinMode(encoderPin1,INPUT_PULLUP); 42 43 pinMode(encoderPin2,INPUT_PULLUP); 44 attachInterrupt(0,updateEncoder,CHANGE); 45 46 attachInterrupt(1,updateEncoder,CHANGE); 47 48} 49 50void loop(){ 51 52 53 if(!calibrate){ 54 testo=""; 55 while(Serial.available()) 56 testo+=(char)Serial.read(); 57 if(testo!=""){ 58 mpu6050_calibration(mpu); 59 60 mpu.dmpInitialize(); 61 mpu.setDMPEnabled(true); 62 attachInterrupt(0,dmpDataReady,RISING); 63 64 packetSize=mpu.dmpGetFIFOPacketSize(); 65 calibrate=true; 66 67 } 68 }else{ 69 get_data(mpu,ypr); 70 Serial.print(ypr[0]*180/M_PI); 71 72 Serial.print(";"); 73 Serial.print(ypr[1]*180/M_PI); 74 Serial.print(";"); 75 76 Serial.print(ypr[2]*180/M_PI); 77 Serial.print(";"); 78 Serial.print(digitalRead(touch_pin)); 79 80 Serial.print(";"); 81 Serial.println(encoderValue); 82 } 83 84} 85 86void 87 updateEncoder(){ 88 89 int MSB=digitalRead(encoderPin1),LSB=digitalRead(encoderPin2),encoded=(MSB<<1)|LSB,sum=(lastEncoded<<2)|encoded; 90 91 92 if(sum==0b1101||sum==0b0100||sum==0b0010||sum==0b1011) encoderValue++; 93 94 if(sum==0b1110||sum==0b0111||sum==0b0001||sum==0b1000) encoderValue--; 95 lastEncoded=encoded; 96 97 98} 99
Calibration_func.h
c_cpp
1#define BUFFERSIZE 1000 //Amount of readings used to average, make 2 it higher to get more precision but sketch will be slower (default:1000) 3#define 4 ACEL_DEADZONE 8 //Acelerometer error allowed, make it lower to get more precision, 5 but sketch may not converge (default:8) 6#define GIRO_DEADZONE 1 //Giro error 7 allowed, make it lower to get more precision, but sketch may not converge (default:1) 8 9void 10 calibration(MPU6050); 11void meansensors(MPU6050); 12 13int16_t ax,ay,az,gx,gy,gz; 14int 15 mean_ax,mean_ay,mean_az,mean_gx,mean_gy,mean_gz,ax_offset,ay_offset,az_offset,gx_offset,gy_offset,gz_offset; 16 17void 18 mpu6050_calibration(MPU6050 accelgyro){ 19 20 accelgyro.initialize(); 21 Serial.println("Calibration"); 22 23 delay(3000); 24 Serial.println(accelgyro.testConnection()?"Success":"Epic 25 Fail?"); 26 delay(1000); 27 accelgyro.setXAccelOffset(0); 28 accelgyro.setYAccelOffset(0); 29 30 accelgyro.setZAccelOffset(0); 31 accelgyro.setXGyroOffset(0); 32 accelgyro.setYGyroOffset(0); 33 34 accelgyro.setZGyroOffset(0); 35 delay(1000); 36 Serial.println("Reading 37 sensors"); 38 meansensors(accelgyro); 39 delay(1000); 40 Serial.println("Calculating 41 offsets?"); 42 calibration(accelgyro); 43 delay(1000); 44 meansensors(accelgyro); 45 46 Serial.print("Just a minute?"); 47 accelgyro.setXGyroOffset(gx_offset); 48 49 accelgyro.setYGyroOffset(gy_offset); 50 accelgyro.setZGyroOffset(gz_offset); 51 52 accelgyro.setZAccelOffset(az_offset); 53 54} 55 56void meansensors(MPU6050 57 accelgyro){ 58 59 long i=0,buff_ax=0,buff_ay=0,buff_az=0,buff_gx=0,buff_gy=0,buff_gz=0; 60 61 62 do{ 63 accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz); // read raw 64 accel/gyro measurements from device 65 if (i>100 && i<=BUFFERSIZE+100){ //First 66 100 measures are discarded 67 buff_ax=buff_ax+ax; 68 buff_ay=buff_ay+ay; 69 70 buff_az=buff_az+az; 71 buff_gx=buff_gx+gx; 72 buff_gy=buff_gy+gy; 73 74 buff_gz=buff_gz+gz; 75 }if (i==BUFFERSIZE+100){ 76 mean_ax=buff_ax/BUFFERSIZE; 77 78 mean_ay=buff_ay/BUFFERSIZE; 79 mean_az=buff_az/BUFFERSIZE; 80 mean_gx=buff_gx/BUFFERSIZE; 81 82 mean_gy=buff_gy/BUFFERSIZE; 83 mean_gz=buff_gz/BUFFERSIZE; 84 } 85 86 delay(2); //Needed so we don't get repeated measures 87 }while(++i<=BUFFERSIZE+100); 88 89} 90 91void 92 calibration(MPU6050 accelgyro){ 93 94 int ready; 95 96 ax_offset=-mean_ax/8; 97 98 ay_offset=-mean_ay/8; 99 az_offset=(16384-mean_az)/8; 100 gx_offset=-mean_gx/4; 101 102 gy_offset=-mean_gy/4; 103 gz_offset=-mean_gz/4; 104 do{ 105 ready=0; 106 107 accelgyro.setXAccelOffset(ax_offset); 108 accelgyro.setYAccelOffset(ay_offset); 109 110 accelgyro.setZAccelOffset(az_offset); 111 accelgyro.setXGyroOffset(gx_offset); 112 113 accelgyro.setYGyroOffset(gy_offset); 114 accelgyro.setZGyroOffset(gz_offset); 115 116 meansensors(accelgyro); 117 Serial.println("..."); 118 if (abs(mean_ax)<=ACEL_DEADZONE) 119 ready++; 120 else ax_offset=ax_offset-mean_ax/ACEL_DEADZONE; 121 if (abs(mean_ay)<=ACEL_DEADZONE) 122 ready++; 123 else ay_offset=ay_offset-mean_ay/ACEL_DEADZONE; 124 if (abs(16384-mean_az)<=ACEL_DEADZONE) 125 ready++; 126 else az_offset=az_offset+(16384-mean_az)/ACEL_DEADZONE; 127 if 128 (abs(mean_gx)<=GIRO_DEADZONE) ready++; 129 else gx_offset=gx_offset-mean_gx/(GIRO_DEADZONE+1); 130 131 if (abs(mean_gy)<=GIRO_DEADZONE) ready++; 132 else gy_offset=gy_offset-mean_gy/(GIRO_DEADZONE+1); 133 134 if (abs(mean_gz)<=GIRO_DEADZONE) ready++; 135 else gz_offset=gz_offset-mean_gz/(GIRO_DEADZONE+1); 136 137 }while(ready!=6); 138 139}
serial_mouse.py
python
1import pyautogui 2import serial 3import time 4 5vel_sens=0.1 6 7def strTofloat(data): 8 9 try: 10 num=data.split('.') 11 return int(num[0])+int(num[1])/10**len(num[1]) 12 except IndexError: 13 return int(data) 14 15arduino=serial.Serial("COM4",115200,timeout=0.05) 16arduino2=serial.Serial("COM3",115200,timeout=0.05) 17pyautogui.PAUSE=0 18pyautogui.FAILSAFE=False 19data=0; k=0; 20x=0; y=0; z=0; t=0; s=0; x0=0; y0=0; z0=0; t0=0; s0=0; 21calibrated=False 22calibrating=False 23 24arduino.flushInput() 25arduino2.flushOutput() 26while True: 27 if not calibrating and not calibrated: 28 data2=arduino2.readline() 29 if data2: 30 print(data2) 31 arduino.write(data2) 32 calibrating=True 33 else: 34 try: 35 data=arduino.readline() 36 x,y,z,t,s=[strTofloat(i) for i in str(data)[2:-5].split(';')] 37 except ValueError: 38 if(data): 39 arduino2.write(data) 40 print(data) 41 continue 42 try: 43 vy=y-y0 44 vz=z-z0 45 vx=x-x0 46 vs=s-s0 47 if calibrated: 48 if(t): pyautogui.mouseDown() 49 elif(t<t0): pyautogui.mouseUp() 50 pyautogui.scroll(vs*20) 51 pyautogui.moveRel(-vy*vel_sens,-vz*vel_sens,duration=0) 52 elif k>0 and abs(vx)<0.5 and abs(vy)<0.5 and abs(vz)<0.5: 53 k+=1 54 if k==50: 55 x0,y0,z0=x,y,z 56 calibrated=True 57 calibrating=False 58 arduino2.write(b"FINISHED") 59 elif k==0: 60 x0,y0,z0=x,y,z 61 k=1 62 else: k=0 63 except Exception as i: 64 print(i) 65 t0=t 66 s0=s 67arduino.close() 68arduino2.close() 69
Calibration_func.h
c_cpp
1#define BUFFERSIZE 1000 //Amount of readings used to average, make it higher to get more precision but sketch will be slower (default:1000) 2#define ACEL_DEADZONE 8 //Acelerometer error allowed, make it lower to get more precision, but sketch may not converge (default:8) 3#define GIRO_DEADZONE 1 //Giro error allowed, make it lower to get more precision, but sketch may not converge (default:1) 4 5void calibration(MPU6050); 6void meansensors(MPU6050); 7 8int16_t ax,ay,az,gx,gy,gz; 9int mean_ax,mean_ay,mean_az,mean_gx,mean_gy,mean_gz,ax_offset,ay_offset,az_offset,gx_offset,gy_offset,gz_offset; 10 11void mpu6050_calibration(MPU6050 accelgyro){ 12 13 accelgyro.initialize(); 14 Serial.println("Calibration"); 15 delay(3000); 16 Serial.println(accelgyro.testConnection()?"Success":"Epic Fail?"); 17 delay(1000); 18 accelgyro.setXAccelOffset(0); 19 accelgyro.setYAccelOffset(0); 20 accelgyro.setZAccelOffset(0); 21 accelgyro.setXGyroOffset(0); 22 accelgyro.setYGyroOffset(0); 23 accelgyro.setZGyroOffset(0); 24 delay(1000); 25 Serial.println("Reading sensors"); 26 meansensors(accelgyro); 27 delay(1000); 28 Serial.println("Calculating offsets?"); 29 calibration(accelgyro); 30 delay(1000); 31 meansensors(accelgyro); 32 Serial.print("Just a minute?"); 33 accelgyro.setXGyroOffset(gx_offset); 34 accelgyro.setYGyroOffset(gy_offset); 35 accelgyro.setZGyroOffset(gz_offset); 36 accelgyro.setZAccelOffset(az_offset); 37 38} 39 40void meansensors(MPU6050 accelgyro){ 41 42 long i=0,buff_ax=0,buff_ay=0,buff_az=0,buff_gx=0,buff_gy=0,buff_gz=0; 43 44 do{ 45 accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz); // read raw accel/gyro measurements from device 46 if (i>100 && i<=BUFFERSIZE+100){ //First 100 measures are discarded 47 buff_ax=buff_ax+ax; 48 buff_ay=buff_ay+ay; 49 buff_az=buff_az+az; 50 buff_gx=buff_gx+gx; 51 buff_gy=buff_gy+gy; 52 buff_gz=buff_gz+gz; 53 }if (i==BUFFERSIZE+100){ 54 mean_ax=buff_ax/BUFFERSIZE; 55 mean_ay=buff_ay/BUFFERSIZE; 56 mean_az=buff_az/BUFFERSIZE; 57 mean_gx=buff_gx/BUFFERSIZE; 58 mean_gy=buff_gy/BUFFERSIZE; 59 mean_gz=buff_gz/BUFFERSIZE; 60 } 61 delay(2); //Needed so we don't get repeated measures 62 }while(++i<=BUFFERSIZE+100); 63 64} 65 66void calibration(MPU6050 accelgyro){ 67 68 int ready; 69 70 ax_offset=-mean_ax/8; 71 ay_offset=-mean_ay/8; 72 az_offset=(16384-mean_az)/8; 73 gx_offset=-mean_gx/4; 74 gy_offset=-mean_gy/4; 75 gz_offset=-mean_gz/4; 76 do{ 77 ready=0; 78 accelgyro.setXAccelOffset(ax_offset); 79 accelgyro.setYAccelOffset(ay_offset); 80 accelgyro.setZAccelOffset(az_offset); 81 accelgyro.setXGyroOffset(gx_offset); 82 accelgyro.setYGyroOffset(gy_offset); 83 accelgyro.setZGyroOffset(gz_offset); 84 meansensors(accelgyro); 85 Serial.println("..."); 86 if (abs(mean_ax)<=ACEL_DEADZONE) ready++; 87 else ax_offset=ax_offset-mean_ax/ACEL_DEADZONE; 88 if (abs(mean_ay)<=ACEL_DEADZONE) ready++; 89 else ay_offset=ay_offset-mean_ay/ACEL_DEADZONE; 90 if (abs(16384-mean_az)<=ACEL_DEADZONE) ready++; 91 else az_offset=az_offset+(16384-mean_az)/ACEL_DEADZONE; 92 if (abs(mean_gx)<=GIRO_DEADZONE) ready++; 93 else gx_offset=gx_offset-mean_gx/(GIRO_DEADZONE+1); 94 if (abs(mean_gy)<=GIRO_DEADZONE) ready++; 95 else gy_offset=gy_offset-mean_gy/(GIRO_DEADZONE+1); 96 if (abs(mean_gz)<=GIRO_DEADZONE) ready++; 97 else gz_offset=gz_offset-mean_gz/(GIRO_DEADZONE+1); 98 }while(ready!=6); 99 100}
CalibratingBox.ino
c_cpp
1#include<LiquidCrystal.h> 2 3int valore; 4String testo; 5bool calibrated=false,calibrating=false; 6uint8_t red_led=3,green_led=2,photo_resistor=A4; 7 8LiquidCrystal lcd(7,8,9,10,11,12); 9 10void setup(){ 11 12 Serial.begin(115200); 13 lcd.begin(16,2); 14 pinMode(photo_resistor,INPUT); 15 pinMode(green_led,OUTPUT); 16 pinMode(red_led,OUTPUT); 17 digitalWrite(green_led,LOW); 18 digitalWrite(red_led,LOW); 19 20} 21void loop(){ 22 23 valore=analogRead(photo_resistor); 24 if(!calibrating&&!calibrated&&valore<50){ 25 lampeggio(red_led,10,500); 26 digitalWrite(red_led,HIGH); 27 Serial.println("S"); 28 calibrating=true; 29 }else if(calibrated&&valore>300){ 30 digitalWrite(green_led,LOW); 31 digitalWrite(red_led,LOW); 32 calibrating=false; 33 }if(calibrating){ 34 testo=""; 35 while(Serial.available()) testo+=(char)Serial.read(); 36 if(testo!="") lcd.clear(); 37 if(testo=="FINISHED"){ 38 digitalWrite(green_led,HIGH); 39 digitalWrite(red_led,LOW); 40 calibrated=true; 41 } 42 lcd.setCursor(0,0); 43 lcd.print(testo); 44 } 45 46} 47 48void lampeggio(uint8_t led,int n,int millisec){ 49 50 for(int i=0;i<n;++i){ 51 digitalWrite(led,HIGH); 52 delay(millisec); 53 digitalWrite(led,LOW); 54 delay(millisec); 55 } 56} 57
GestureGlove.ino
c_cpp
1#include "Wire.h" 2#include "I2Cdev.h" 3#include "MPU6050_6Axis_MotionApps20.h" 4#include "Calibration_func.h" 5#include "MPU6050_data_func.h" 6 7float ypr[3]; 8String testo; 9bool calibrate=false; 10int lastMSB=0,lastLSB=0; 11long lastencoderValue=0; 12const uint8_t encoderPin1=2,encoderPin2=3,touch_pin=8; 13 14volatile int lastEncoded=0; 15volatile long int encoderValue=0; 16 17MPU6050 mpu(0x68); 18 19void setup(){ 20 21 Serial.begin(115200); 22 Wire.begin(); 23 TWBR=24; 24 /* 25 mpu.initialize(); 26 mpu6050_calibration(mpu); 27 mpu.dmpInitialize(); 28 mpu.setDMPEnabled(true); 29 attachInterrupt(0,dmpDataReady,RISING); 30 packetSize=mpu.dmpGetFIFOPacketSize(); 31 */ 32 pinMode(touch_pin,INPUT); 33 pinMode(encoderPin1,INPUT_PULLUP); 34 pinMode(encoderPin2,INPUT_PULLUP); 35 attachInterrupt(0,updateEncoder,CHANGE); 36 attachInterrupt(1,updateEncoder,CHANGE); 37 38} 39 40void loop(){ 41 42 if(!calibrate){ 43 testo=""; 44 while(Serial.available()) testo+=(char)Serial.read(); 45 if(testo!=""){ 46 mpu6050_calibration(mpu); 47 mpu.dmpInitialize(); 48 mpu.setDMPEnabled(true); 49 attachInterrupt(0,dmpDataReady,RISING); 50 packetSize=mpu.dmpGetFIFOPacketSize(); 51 calibrate=true; 52 } 53 }else{ 54 get_data(mpu,ypr); 55 Serial.print(ypr[0]*180/M_PI); 56 Serial.print(";"); 57 Serial.print(ypr[1]*180/M_PI); 58 Serial.print(";"); 59 Serial.print(ypr[2]*180/M_PI); 60 Serial.print(";"); 61 Serial.print(digitalRead(touch_pin)); 62 Serial.print(";"); 63 Serial.println(encoderValue); 64 } 65 66} 67 68void updateEncoder(){ 69 70 int MSB=digitalRead(encoderPin1),LSB=digitalRead(encoderPin2),encoded=(MSB<<1)|LSB,sum=(lastEncoded<<2)|encoded; 71 72 if(sum==0b1101||sum==0b0100||sum==0b0010||sum==0b1011) encoderValue++; 73 if(sum==0b1110||sum==0b0111||sum==0b0001||sum==0b1000) encoderValue--; 74 lastEncoded=encoded; 75 76} 77
Downloadable files
Fritzing of the project
we didn't put the touch sensor because we can't find it in Fritzing
Fritzing of the project
Fritzing of the project
we didn't put the touch sensor because we can't find it in Fritzing
Fritzing of the project
Comments
Only logged in users can leave comments
lorenzoamaducci
0 Followers
•0 Projects
Table of contents
Intro
3
0