40490| 33
|
[项目] 头戴式肌电鼠标 |
为啥要做个头戴式肌电鼠标?先说说想法来源吧,去年参加了蘑菇云的创客大赛,作品就是给残疾人做的无障碍输入设备,超大号的键盘,还拿了一个一等奖,嘚瑟一下~~ 然而觉得还是对于残疾人来说不够方便,于是在跟一些资深玩家们头脑激荡时,想到了可以用头部输入的方式,简单介绍一下实现方式: 技术实现:用该设备采用运动感应、肌电传感、语音识别等技术,可以实现: 1. 用陀螺仪将头部运动转化为鼠标运动,从而解放双手,帮助双手行动不便及单/双臂缺失的人。 2. 肌肉电传感器检测牙齿咀嚼肌的咬合,实现鼠标单击双击。 3. 语音可选控制/输入模式,控制模式可实现命令控制,如“复制”“粘贴”等;输入模式可将语音转换为文字。从而实现快速控制与输入。 4. 运动感应器,实现坐姿检测、颈椎病预防等功能; 硬件列表:
好了,开工!! 第一步:测试安装肌电传感器 拿到OYMotion的肌电板子,还是有些小激动的,因为这是自己寻找了许久的肌电模块。在这里,情不自禁要为OYMotion打个小广告,自己找遍了市场上的肌电传感器,都是要额外贴一层胶在电极上,完全不是消费级方案,OYMotion就是用的干电极,而且可以实现医疗级的精度。自己有幸认识他们团队,并优先拿到了他们的肌电模块 欧耶~~ 好了,废话不多说,上图!这是原始模块 为了减小体积,我们将耳机线路部分移除,用飞线的方式进行传输 如何接线呢?很简单:VCC对3.3V, GND-GND, 数据线接A0进行AD转换。 只要一句就可以读到数据data = analogRead(A0); 但如何实现牙齿单击双击呢?首先要大致熟悉HID开发宝贝ArduinoLeonardo,这个部分就不多说了,大家自己看资料就好了。用Arduino采集数据后,通过波形分析,就可实现鼠标的单击双击!波形分析的算法,可以详见代码库的部分。先给大家看一张波形图吧~~,波形图均为咬牙一次+长咬的过程,第一张是原始数据,第二章是加了算法后处理出来的数据(红线代表鼠标按下的过程)。 第二步:安装陀螺仪 用陀螺仪来感知头部运动,是个和取巧的方法。我选用的是市场上现有的陀螺模块JY901,可以直接输出陀螺的角度数据。可以通过角度来算鼠标的移动,算法详见代码。注意:模块一定要水平/竖直位置,头顶部是最佳,这是经验! 那我们用什么做为支架呢?既要有弹性,能夹住肌电传感器,又要佩戴舒适!那,只好牺牲我的头戴式耳机了~~ 三滴泪~~ 第三步:语音模块 语音模块听上去会让人眉头一紧,其实并不复杂,看到官网给出的Arduino操作,你就恍然大悟了,其实就是通过拼音进行识别嘛~~ 语音模块与Arduino的连接,是通过SPI,具体请见https://www.waveshare.net/study/article-11-1.html 第四步:其他小件的连接: 还需要蜂鸣器、LED等这类的小器件作为提示信息使用的,LED接线,一根接GND一根接IO 13口,蜂鸣器除了VCC GND之外,数据线接IO7 整体安装的具体方式:1 肌电模块在耳机耳蜗中,佩戴时尽量与脸部贴合;2.IMU模块放在头顶部,来感应头部运动;3. 主控模块leonardo放在耳侧;4.语音模块放在主控模块的前部探出来,可以与讲话者更近;5. 蜂鸣器藏在主控板中,LED灯放在语音模块前段,带上后眼睛也可以看到。 好了,效果图如下,怎么样,还不错吧? 来来来,上视频!! https://v.ku6.com/show/nHQQRFinso0eOYOBXCbszw...html (此视频消失在了茫茫人海中) 再来!上代码! [mw_shl_code=cpp,true]#include <Wire.h> #include <JY901.h> #include <Keyboard.h> #include <Mouse.h> #include <ld3320.h> #define PRINT_RAW_DATA 0 #define PRINT_STD_DATA 0 bool mouse_disabled=false; void setup() { Serial.begin(9600); //配置9600 voice_init(); mouse_move_init(); musle_click_init(); Mouse.begin(); Keyboard.begin(); Serial.print("Initialized\n"); } void loop() { handle_voice(); if(!mouse_disabled){ mouse_move(); handle_musle_click(); } } /*---------------------------------------------------------------mouse movement-------------------------------------------------------------------------*/ float Angles[3]; float xLast,yLast; bool right_clicked=false; bool left_click=false; float view_center; float view_angle=30; int shake_x,shake_y; #define SHAKE_LIMIT 16 void mouse_move_init(){ Serial1.begin(9600); } void mouse_move(){ int x,y; float xDelta,yDelta; float xNow,yNow; float X_RATE=0.06;//0.05; float Y_RATE=0.04;//0.05; get_IMU_data(); xNow=Angles[2]; yNow=Angles[0]; if(xLast==0){ xLast=xNow; } if(yLast==0){ yLast=yNow; } xDelta=xNow-xLast; yDelta=yNow-yLast; x=xDelta/X_RATE; y=yDelta/Y_RATE; if((x!=0 || y!=0)){ if(xLast*xNow<0){ x=xLast/abs(xLast)*(360/X_RATE-abs(x)); } int out_x=x, out_y=y; if(left_click==true){ //do not let mouse move when left button pressed, in case it's during right pressing. shake_x+=x; shake_y+=y; }else{ while(abs(out_x)>120){ out_x=out_x/abs(out_x)*(abs(out_x)-120); Mouse.move(-(out_x/abs(out_x)*120), 0); } while(abs(out_y)>120){ out_y=out_y/abs(out_y)*(abs(out_y)-120); Mouse.move(0,-(out_y/abs(out_y)*120)); } Mouse.move(-out_x, -out_y); Serial.print("xNow=");Serial.print(xNow);Serial.print(" yNow=");Serial.print(yNow); Serial.print(" xDelta=");Serial.print(xDelta);Serial.print(" yDelta=");Serial.print(yDelta);Serial.print(" x=");Serial.print(x);Serial.print(" y=");Serial.println(y); } } xLast=xLast+((int)(xDelta/X_RATE))*X_RATE; yLast=yLast+((int)(yDelta/Y_RATE))*Y_RATE; } void get_IMU_data(){ if(Serial1.available()) { JY901.CopeSerialData(Serial1.read()); //Call JY901 data cope function for(int i=0; i<3; i++){ Angles=(float)JY901.stcAngle.Angle/32768*180; //Angles=(float)JY901.stcGyro.w/32768*2000; } } } /*-------------------------------------------------------voice detect--------------------------------------------------------------------*/ #define buzzerPin 7 #define VOICE_ARRAY 16 bool start_switch = false; int current_mode=0; VoiceRecognition Voice; //声明一个语音识别对象 char *voices[VOICE_ARRAY]= {"mo gu yun", //0 "shu ru mo shi", //1 "ming ling mo shi", //2 "xin jian", //3 ctrl+n "guan bi", //4 alt+f4 "fu zhi", //5 ctr+c "zhan tie", //6 ctr+v "tian qi hen hao", //7 "zhong mei chuang ke da sai",//8 "tou kong shu ru she bei", //9 "ni hao", //10 "hen kai xin", //11 "hen shun li", //12 "da kai shu biao", //13 鼠标打开 "guan bi shu biao", //14 鼠标停止 "zheng chang mo shi" //15 正常模式 }; void voice_init(){ pinMode(buzzerPin, OUTPUT); Voice.init(); //初始化VoiceRecognition模块 for(int i=0; i<VOICE_ARRAY; i++){ Voice.addCommand(voices,i); } Voice.start();//开始识别 } void handle_voice(){ bool start_switch=false; int voice_id = voice_control(); if(current_mode==1){ //输入模式 if(voice_id>2){ Keyboard.print(voices[voice_id]); } } if(current_mode==2){ //命令模式 char ctrlKey = KEY_LEFT_CTRL; char altKey = KEY_LEFT_ALT; switch(voice_id){ case 3: Keyboard.press(ctrlKey); Keyboard.press('n'); delay(10); Keyboard.release('n'); Keyboard.release(ctrlKey); break; case 4: Keyboard.press(altKey); Keyboard.press(KEY_F4); delay(10); Keyboard.release(KEY_F4); Keyboard.release(altKey); break; case 5: Keyboard.press(ctrlKey); Keyboard.press('c'); delay(10); Keyboard.release('c'); Keyboard.release(ctrlKey); break; case 6: Keyboard.press(ctrlKey); Keyboard.press('v'); delay(10); Keyboard.release('v'); Keyboard.release(ctrlKey); break; } } } int voice_control(){ int id = Voice.read(); if(id >= VOICE_ARRAY || id < 0){ return -1; } Serial.println(voices[id]); switch(id) //判断识别 { case 0: //开始指令 start_switch = true; Serial.print("Start command\n"); tone(buzzerPin, 5000, 200); delay(350); tone(buzzerPin, 5000, 200); break; case 1: //输入模式 if(start_switch){ current_mode=1; start_switch = false; Serial.print("input mode\n"); tone(buzzerPin, 5000, 200); } break; case 2: //命令模式 if(start_switch){ current_mode=2; start_switch = false; Serial.print("Command mode\n"); tone(buzzerPin, 5000, 200); } break; case 15: //正常模式 if(start_switch){ current_mode=0; start_switch = false; Serial.print("normal mode\n"); tone(buzzerPin, 5000, 200); } break; case 14: if(start_switch){ //关闭鼠标 mouse_disabled=true; start_switch = false; Serial.print("Mouse disabled\n"); tone(buzzerPin, 5000, 200); } break; case 13: if(start_switch){ //打开鼠标 mouse_disabled=false; start_switch = false; Serial.print("Mouse turn on\n"); tone(buzzerPin, 5000, 200); } break; default: //Serial.print("nothing..."); start_switch = 0; break; } return id; } /*----------------------------------------------------------- musle click---------------------------------------------------------------------------*/ int sensorPin = A0; // select the input pin for the potentiometer int sensorData = 0; // variable to store the value coming from the sensor int errorPin = 13; bool errorHappen = false; int calib_count=0; long calib_data=0; unsigned long errorTime=0; #define DATA_SIZE 15 int musle_data[DATA_SIZE]; int diff_data[DATA_SIZE]; #define MIN_VALUE 150 #define MAX_VALUE 600 #define STD_ABNORMAL 250 #define STD_VALUE 10 #define STD_EDGE 3 unsigned long press_timestamp=0; unsigned long release_timestamp=0; int show_press_real=0; #define PRESS_DELAY 40 #define RELEASE_DELAY 15 #define RIGHT_BT_DETECT 600 #define SAMPLE_DELAY 10 unsigned long last_sample_time=millis(); void musle_click_init() { Serial.begin(9600); pinMode(errorPin, OUTPUT); digitalWrite(errorPin, HIGH); errorTime = millis(); Mouse.begin(); } void handle_musle_click() { if(millis()-last_sample_time > SAMPLE_DELAY){ sensorData = analogRead(sensorPin); #if PRINT_RAW_DATA Serial.println(sensorData); #endif for(int i=0; i<DATA_SIZE-1; i++){ diff_data=musle_data[i+1]-musle_data; musle_data=musle_data[i+1]; } diff_data[DATA_SIZE-1]=sensorData-musle_data[DATA_SIZE-1]; musle_data[DATA_SIZE-1]=sensorData; handle_musle_data(); last_sample_time=millis(); } } void handle_musle_data(){ int show_press=0; long int std_value=0; std_value=get_std(); if(std_value>STD_VALUE){ show_press=30; } #if PRINT_STD_DATA Serial.print(show_press);Serial.print(" "); Serial.print(show_press_real);Serial.print(" "); Serial.println(std_value); #endif if(std_value>=STD_ABNORMAL || std_value<0){ errorTime = millis(); digitalWrite(errorPin, HIGH); errorHappen = true; calib_data=0; calib_count=0; } if(errorHappen){ if(std_value<STD_VALUE && std_value>=0){ //when error happened, make sure musle std data is below STD_VALUE for 1s. calib_data+=std_value; calib_count++; }else{ errorTime = millis(); } if(millis()-errorTime > 1500){ digitalWrite(errorPin, LOW); errorHappen = false; calib_data=calib_data/calib_count; //re-calibrate the musle average data. } } if(errorHappen){ release_timestamp=millis(); press_timestamp=0; show_press_real=0; if(Mouse.isPressed(MOUSE_LEFT)){ Mouse.release(MOUSE_LEFT); } } if(!errorHappen){ if(std_value>calib_data+STD_EDGE){ if(press_timestamp==0){ press_timestamp=millis(); }else{ unsigned long current_time = millis(); if (current_time-press_timestamp>PRESS_DELAY && current_time-press_timestamp<=RIGHT_BT_DETECT) { //make sure left-click can be detected. left_click=true; } if(sqrt(shake_x*shake_x+shake_y*shake_y)>SHAKE_LIMIT){ //when mouse moved, let left button "press" if(!Mouse.isPressed(MOUSE_LEFT)){ Mouse.press(MOUSE_LEFT); } left_click=false; }else{ if(current_time-press_timestamp>RIGHT_BT_DETECT && right_clicked==false){ //mouse did not move, and is pressed bigger than threshold value. Mouse.click(MOUSE_RIGHT); right_clicked=true; left_click=false; } } release_timestamp=0; show_press_real=calib_data+STD_EDGE; } }else{ if(release_timestamp==0){ release_timestamp=millis(); }else if (millis()-release_timestamp>RELEASE_DELAY) { //when musle released, clear all the flags. if(left_click==true){ Mouse.click(MOUSE_LEFT); } if(Mouse.isPressed(MOUSE_LEFT)){ Mouse.release(MOUSE_LEFT); } right_clicked=false; left_click = false; shake_x=0; shake_y=0; press_timestamp=0; show_press_real=calib_data; } } } } long int get_std(){ int average = 0; int sum = 0; long int std = 0; for(int i=0; i<DATA_SIZE; i++){ sum+=diff_data; if(musle_data < MIN_VALUE || musle_data > MAX_VALUE){ return STD_ABNORMAL; } } average = sum/DATA_SIZE; for(int i=0; i<DATA_SIZE; i++){ std+=(diff_data-average)*(diff_data-average); } std=sqrt(std/(DATA_SIZE-1)); return std; }[/mw_shl_code] tutorials |
DFB1M-nCHJB 发表于 2019-12-25 19:25 不能直接copy,需要在Arduino环境下导入几个特定的模块的库,具体你可以在我链接里找一下 |
mickey 发表于 2017-8-3 14:17 一看就是高手啊!有找到ASR这款板子,它正好集成了HID功能+语音识别。不过当时DF缺货,急着做,就用其他方式了啊~~ |
xiaoyanflora 发表于 2017-9-8 12:57 我是认识那家公司的人,所以优先拿到了肌电传感器。这款传感器DF上现在还没上线,不过应该快了~ |
xijiajie123 发表于 2017-9-13 15:53 我是完全零基础小白,要是不用这款肌电传感,买别家的没有你的案例示范肯定搞不定~~呜 |
xiaoyanflora 发表于 2017-9-14 08:57 有问过他们,大概十月份会上线,可以期待一下 哈哈 |
RickyW 发表于 2017-9-21 19:56 上线了~ https://www.dfrobot.com.cn/goods-1503.html |
© 2013-2024 Comsenz Inc. Powered by Discuz! X3.4 Licensed