1、靈感來源:
LZ是純宅男,一天從早上8:00起一直要呆在電腦旁到晚上12:00左右吧~平時也沒人來閒聊幾句,刷空間暑假也沒啥動態,聽音樂吧...~有些確實不好聽,於是就不得不打斷手頭的工作去點擊下一曲或是找個好聽的歌來聽...但是,[移動手鎖定鼠標-->移動鼠標關閉當前頁面選擇音樂軟件頁面-->選擇合適的音樂-->恢復原來的界面] 這一過程也會煩人不少,如果説軟件的設計要在用户體驗上做足功夫,感覺這一點是軟件設計人員很難管住的方面,畢竟操作系統也就這樣安排的嘛(當然,有些機智的開發人員加了幾個熱鍵,確實方便了不少!)。於是我想能不能設計一個軟件能儘量少打斷我們正常的工作簡單操作去觸發下一曲~
2、需求分析:
- 下圖左一是傳統的操作模式,在這裏要人的眼、手並用而且還必須等待記憶,可能我們平時感覺不到,但是這個過程卻是比較浪費時間且分散注意力!
- 下圖右一是想改為的操作模式,在這裏我們只需要外部觸發(如:搖一下頭或者微笑一下,甚至只要想一下就可以啦),讓切歌任務在後台進行,這樣就能不打斷前台工作(這裏的前台和後台只是當前工作窗口和非當前窗口,和專業的有差別!)
3、解決方案
根據上面分析我們需要這些條件:
- 外部硬件設備,可以接收特殊信號並傳給PC
- PC上的軟件能夠讀取硬件傳來的信號並分析信息,做出切歌任務
結合我現有設備,做出如下方案:
- 硬件採用STC89C52單片機最小系統佔用P1.0和P1.1兩個端口和超聲波測距模塊HC-SR04,通過根據遮擋物在超聲波測距範圍內停留的時間來發出觸發“下一曲”,“暫停”,“上一曲”事件的信號。
- 軟件採用C#從串口讀取單片機發送的觸發事件信號消息,然後調用WinAPI對音樂盒窗口進行識別計算以及發送點擊消息,來控制切換歌曲。
PS:這裏根據手在超聲波範圍內停留的時間來分出3種信號:
- 短暫停留在區域內-->下一曲信號
- 稍長停留在區域內-->上一曲信號
- 超長停留在區域內-->暫停信號
4、作品提前展示及相關介紹:
哈哈,秒懂啦吧!圖中那個像望遠鏡的東西就是超聲波測距模塊,它的前面輻射狀的空間(我設置為40cm)就是有效範圍,那個黑色的像蜈蚣的東西就是單片機(就相當於電腦裏的CPU),插在USB裏面的不用介紹就是USB轉TTL啦!主要就是負責採集傳感器信號然後將距離信息通過USB發送給電腦。最終達到達到的效果是:你的手只要在區域內揮一下,就能切歌啦!手停長一點時間就能暫停啦!這個玩法沒試過吧,哈哈!
下面這個圖就是基於C#的電腦端軟件,其主要功能就是連接串口進行數據接收、數據處理、以及查找音樂盒的窗口、計算該點擊的按鈕位置、發出點擊消息、在不同窗口中切換(因為要實現少打擾當前活動的目的)。這裏為了測試方便所以加了3個功能按鈕:上一曲、暫停、下一曲,通過點擊這些按鈕能實現控制酷我音樂盒歌曲的切換,然後右邊加了個下拉框用來枚舉當前可用串口,LINK按鈕就是連接該串口的觸發按鈕。下面一個文本顯示區是用來顯示串口傳過來的距離的數據的(便於調試哈~)
5、C#軟件部分技術詳解
該部分要用到很多Windows API,主要功能就是查找窗口句柄、控制窗口顯示、計算窗口位置、聚焦窗口、窗口切換....算是把窗口有關的常用API都用上啦~此外,還用到了鼠標光標位置設定、鼠標點擊消息發送最終達到模擬鼠標點擊事件。當然,串口通信絕對不能少滴!
5.1、C#串口通信
5.1.1、獲取當前可用串口列表
1 //Get all port list for selection
2 //獲得所有的端口列表,並顯示在列表內
3 PortList.Items.Clear();
4 string[] Ports = SerialPort.GetPortNames();
5
6 for (int i = 0; i < Ports.Length; i++)
7 {
8 string s = Ports[i].ToUpper();
9 Regex reg = new Regex("[^COM\\d]", RegexOptions.IgnoreCase | RegexOptions.Multiline);//正則表達式
10 s = reg.Replace(s, "");
11
12 PortList.Items.Add(s);
13 }
14 if (Ports.Length >1) PortList.SelectedIndex = 1;
- 調用串口要引用 using System.IO.Ports;
- 第9行的正則表達式要引用 using System.Text.RegularExpressions;
- 第3行的PortList是那個下拉框;
- 整體的功能就是通過第4行的函數獲取所有可用串口,然後加入下拉框顯示,如果有可用的就把第一個選中;
5.1.2、串口連接按鈕事件
1 private void btn_link_Click(object sender, EventArgs e)
2 {
3 if (!Connection.IsOpen)
4 {
5 //Start
6 Status = "正在連接...";
7 Connection = new SerialPort();
8 btn_link.Enabled = false;
9 Connection.PortName = PortList.SelectedItem.ToString();
10 Connection.Open();
11 Connection.ReadTimeout = 10000;
12 Connection.DataReceived += new SerialDataReceivedEventHandler(PortDataReceived);
13 Status = "連接成功";
14 }
15 }
PS:整體很好理解就是把下拉框選中的串口號連接上,這裏第12行比較重要,它調用SerialDataReceivedEventHandler(Func Name)來定義一個數據接收函數的句柄,這裏PortDataReceived你可以隨便寫,但是接下來你要寫對應的實現函數:(這裏説句柄比較難理解,你就理解成一個函數,綁定串口的函數,一旦串口有數據發動過來就執行這個函數....)
1 //接收串口數據
2 private int num=0; //障礙物進入範圍的時間
3 private bool enter=false; //是否有障礙物進入
4 private int signal=0; //對每次進入範圍的時間分段形成控制信號
5 private void PortDataReceived(object o, SerialDataReceivedEventArgs e)
6 {
7 int length = 1;
8 byte[] data = new byte[length];
9 Connection.Read(data, 0, length);
10 for (int i = 0; i < length; i++)
11 {
12 ReceivedData = string.Format("{0}",data[i]);
13 }
14
15 //數據濾波轉換為控制信號
16 if (data[0] != 136 && !enter){ //當有障礙物進入時,傳過來數據不是136並且是第一個
17 enter = true;
18 num = 1;
19 }else if (data[0] == 136 && enter){ //當障礙物離開時,傳過來數據變為136且是第一個
20 enter = false;
21 if (num > 1 && num < 6){
22 signal = 1;
23 }else if (num > 5 && num < 10){
24 signal = 2;
25 }else if (num > 9){
26 signal = 3;
27 }
28 num = 0;
29 }else if (data[0] != 136 && data[0] >= 0 && enter){
30 num++;
31 }
32 }
PS:這就是串口數據接收函數實現,先別看其他內容,因為裏面涉及濾波算法和控制信號生成的算法,只要看第7~13行的代碼核心部分就是第9行從緩衝區讀取串口數據放到data[]數組中,這樣串口數據就放在data[]中啦!怎麼處理是下面的事啦~
5.1.3、重量級功能函數:
1 /// <summary>
2 /// 模擬鼠標點擊函數
3 /// </summary>
4 /// <param name="n_control_type">0是上一曲,1是暫停,2是下一曲</param>
5 public void func(int n_control_type)
6 {
7 //bool isVisabled; //窗口原來狀態,隱藏還是顯示
8 IntPtr hCurWin = GetForegroundWindow(); //獲取當前激活窗口
9
10 IntPtr hMusic = FindWindow("kwmusicmaindlg", null); //找到窗口句柄
11 if (hMusic == null)
12 {
13 return;
14 }
15 Point pt; //獲取鼠標當前位置
16 GetCursorPos(out pt);
17 ShowWindow(hMusic,SW_SHOWNORMAL); //如果是隱藏的就讓他正常顯示出來
18 SetForegroundWindow(hMusic); //將音樂盒窗口放在最上層
19
20 RECT rect = new RECT(); //獲取窗口矩形
21 GetWindowRect(hMusic, ref rect);
22 int width = rect.Right - rect.Left; //窗口的寬度
23 int height = rect.Bottom - rect.Top; //窗口的高度
24 int x = rect.Right; //窗口的位置
25 int y = rect.Top;
26
27 int X=0,Y=0;
28 if(n_control_type==0)//座標[-20,200]:第3列表 [-120,200]:第2列表 [-220,200]第1列表
29 { //座標[-200,100]:上一曲 [-170,100]暫停 [-145,100]下一曲
30 X = x - 200;
31 Y = y + 100;
32 }
33 else if (n_control_type == 1)
34 {
35 X = x - 170;
36 Y = y + 100;
37 }
38 else
39 {
40 X = x - 145;
41 Y = y + 100;
42 }
43
44 SetCursorPos(X, Y); //移動鼠標
45 mouse_event(MOUSEEVENTF_LEFTDOWN, X * 65536 / 1024, X * 65536 / 768, 0, 0); //發送鼠標信息
46 mouse_event(MOUSEEVENTF_LEFTUP, Y * 65536 / 1024, Y * 65536 / 768, 0, 0);
47 SetCursorPos(pt.X, pt.Y); //移動鼠標回到原位置
48
49 //if (isVisabled == 24) ShowWindow(hMusic, SW_HIDE);
50 //SetParent(hMusic, this.Handle);
51 //EnableWindow((IntPtr)this.Handle, true);
52 SetWindowPos(hMusic, (IntPtr)this.Handle, x, y, width, height, SWP_NOMOVE); //使能窗口聚焦原窗口
53 SetForegroundWindow(hCurWin); //將原來窗口放在最上層
54 }
PS:這個函數負責找到酷我音樂盒的窗口(第10行)、頂層窗口切換(第18行、第52行、第53行)、鼠標位置設置(第16行、第44行、第47行)、鼠標點擊消息的生成(第45行、第46行)、點擊區域計算(第27~42行)
- GetForegroundWindow(); 獲取當前頂層窗口句柄,不懂百度一下,就windows API介紹很多,初學者知道怎麼用就行啦![在調用它之前要寫這些代碼,下面説的調用API都要這樣的!]
1 [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
2 public static extern IntPtr GetForegroundWindow();
- FindWindow("kwmusicmaindlg", null);根據窗口類名或者窗口名獲得窗口句柄。PS:該如何知道某個窗口的類名或者窗口名呢?一般是用VC6.0或者是VS系列軟件的Tool-->Spy++,具體請見我寫的一篇博文,裏面有詳細介紹:
1 [DllImport("user32.dll", EntryPoint = "FindWindow")]
2 public static extern IntPtr FindWindow(
3 string lpClassName,
4 string lpWindowName
5 );
- GetCursorPos(out pt);獲取當前鼠標的位置,保存在Point結構體內,這裏因為我們想讓鼠標點擊一下按鈕然後回到原來的位置,所以要保存原來的位置!
1 [DllImport("user32.dll")]
2 public static extern bool GetCursorPos(out Point pt);
- ShowWindow(hMusic,SW_SHOWNORMAL);根據句柄顯示窗口,這裏第二個參數是設定窗口以哪種方式顯示的,主要有以最小化顯示、最大化顯示、正常顯示.....具體參見度娘~我們這裏是為了避免有時候音樂盒最小化,我們得把它打開才能觸發點擊事件有效。(我本來想用個標記來標記它原來的狀態然後在處理之後恢復音樂盒自身的狀態,但是覺得還得寫些代碼,沒時間啦,調試這個浪費了很長時間~)
1 //private readonly int SW_HIDE = 0; //隱藏
2 private readonly int SW_SHOWNORMAL = 1; //還原
3 [DllImport("user32.dll", EntryPoint = "ShowWindow", SetLastError = true)]
4 private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
- SetForegroundWindow(hMusic); 將活動窗口切換到句柄所指窗口,這樣鼠標點擊對應區域窗口才能接收到鼠標點擊消息!
1 [DllImport("user32.dll")]
2 private static extern bool SetForegroundWindow(IntPtr hWnd);
- GetWindowRect(hMusic, ref rect); 獲取指定窗口的在桌面上的矩形座標(這樣就能根據這個值計算目標窗口的大小和位置啦:20~25行就是幹這個的)
1 [DllImport("user32.dll")]
2 [return: MarshalAs(UnmanagedType.Bool)]
3 static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect);
4
5 [StructLayout(LayoutKind.Sequential)]
6 public struct RECT
7 {
8 public int Left; //最左座標
9 public int Top; //最上座標
10 public int Right; //最右座標
11 public int Bottom; //最下座標
12 }
- SetCursorPos(X, Y); 設置鼠標光標位置(X,Y)
1 [DllImport("user32.dll", EntryPoint = "SetCursorPos")]
2 private static extern int SetCursorPos(int x, int y);
- mouse_event(MOUSEEVENTF_LEFTDOWN, X * 65536 / 1024, X * 65536 / 768, 0, 0); 發送消息函數,我們知道windows是消息機制的,你點一下鼠標其實就是光標移到指定位置,然後向系統發送一個鼠標按動消息,這裏我仿製一個鼠標左擊時間,第45行負責在指定位置發送個鼠標左鍵按下的消息,第46行發送個對應的鼠標左鍵抬起的消息,這樣一按一抬就組成了一個點擊事件。
1 private readonly int MOUSEEVENTF_LEFTDOWN = 0x2;
2 private readonly int MOUSEEVENTF_LEFTUP = 0x4;
3 [DllImport("user32")]
4 public static extern void mouse_event(int dwFlags, int dx, int dy, int dwData, int dwExtraInfo);
- SetWindowPos(hMusic, (IntPtr)this.Handle, x, y, width, height, SWP_NOMOVE); 這個函數和ShowWindow有點像,只是這個可以設置窗口的三維顯示,為什麼是三維?平面窗口還有一維是窗口的疊放順序,具體可以問度娘~(這裏刪了這句好像也沒啥影響,當初因為沒有下面那句,所以需要這個函數將焦點放到C#軟件窗口)
1 static readonly IntPtr HWND_TOP = new IntPtr(0);
2 const UInt32 SWP_NOMOVE = 0x0002;
3 [System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "SetWindowPos", SetLastError = true)]
4 private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
5.1.4、時間函數TImer
往窗口裏加一個Timer控件:[長下面那個模樣,屬性設置為Interval:100,然後給它一個消息函數,屬性中的那個閃電的標誌],C#比MFC要方便的多,MFC要自己寫這貨,有點麻煩,但是對於打基礎的童鞋還是建議從win32學起,然後再學MFC這樣你對windows消息機制會有比較清晰的理解!嘿嘿,撤遠啦!其實這個Timer對應的消息函數就像一個會定時執行的函數一樣,你只要在裏面寫些邏輯,它會每隔一定的時間執行的。比如你想做動畫效果,讓一個小球移動,那麼小球的座標的改變的計算可以放在這裏面寫。下面看一下我的這個函數中寫了什麼:
1 private string Status, ReceivedData;
2 private void timer1_Tick(object sender, EventArgs e)
3 {
4 StatusMessage.Text = Status;
5 StatusMessage.Text = ReceivedData;
6 //當有有效信號過來觸發控制
7 if (signal == 1) func(2);//下一曲
8 if (signal == 2) func(0);//上一曲
9 if (signal == 3) func(1);//暫 停
10 signal = 0;
11 }
PS:其實就是更新那個文本顯示區的內容和根據上面串口收來的數據進行處理然後產生的3種不同的控制命令,來調用func函數執行不同的點擊命令!
>_<:好啦,軟件部分終於説完啦(那其它3個功能按鈕直接調用func函數就行啦),其實硬件部分更多,剛才一直沒有説那個濾波算法,及對應的命令信號signal是如何產生的....下面就要介紹啦!
6、硬件部分及濾波、信號產生算法詳解:
其實硬件部分就是CPU採集超聲波測距儀採集的距離的信息通過串口發送給電腦,電腦再對發送過來的數據進行分析,來看看是要切歌還是暫停還是一些干擾(這裏在硬件和圖像處理中經常會談到的名詞:濾波)。這裏只貼一下硬件部分的代碼(難點是濾波,硬件是基於stc80c52的程序,包括與測距模塊的通信程序、串口通信程序兩大部分,具體細節裏面有很詳細的註釋,建議如果是新手最好看看《新概念51單片機C語言教程》不錯的哦~)
1 /***********************************************************************************************************/
2 //HC-SR04 超聲波測距模塊應用程序
3 //MCU: STC89C52/STC89C51
4 //晶振:11。0592
5 //接線:模塊TRIG接 P1.2 ECH0 接P1.1
6 //串口波特率9600
7 /***********************************************************************************************************/
8 #include <AT89X51.H>
9 #include <intrins.h>
10 #include <STDIO.H>
11
12 #define uchar unsigned char
13 #define uint unsigned int
14 #define RX P1_1
15 #define TX P1_2
16
17
18 unsigned int time=0;
19 unsigned int timer=0;
20 unsigned char S=0,a;
21 bit flag =0,usart_flag;
22
23
24 /*--------------------------------------------
25 USAR初始函數包括所有需要的中斷和時鐘,超聲波時鐘也在內]
26 ---------------------------------------------*/
27 void USRT_init()
28 {
29 TMOD=0x21; //設置T1定時器工作方式2,設T0為方式1,GATE=1;
30 SCON=0x50;
31 TH1=0xfd; //T1定時器裝初值
32 TL1=0xfd;
33 TH0=0; //超聲波測距計時器裝初始值
34 TL0=0;
35 TR1=1; //啓動T1定時器
36 TR0=1;
37 REN=1; //允許串口中斷接收、
38 ET0=1; //允許T0中斷
39 SM0=0; //設定串口工作方式
40 SM1=1;
41 EA=1; //開總中斷
42 ES=1; //開串口中斷
43 }
44 /*--------------------------------------------
45 串口發送函數
46 ---------------------------------------------*/
47 void SeriPushSend(unsigned send_data)
48 {
49 SBUF=send_data;
50 while(!TI);
51 TI=0;
52 }
53 /*--------------------------------------------
54 串口中斷程序
55 ---------------------------------------------*/
56 void ser()interrupt 4
57 {
58 RI=0;
59 a=SBUF;
60 usart_flag=1;
61 }
62 /*--------------------------------------------
63 超聲波距離計算函數
64 ---------------------------------------------*/
65 void Conut(void)
66 {
67 time=TH0*256+TL0;
68 TH0=0;
69 TL0=0;
70 S=(int)(time*1.87)/100; //算出來是CM
71 if(flag==1 || S>30) //超出測量或無效數據
72 {
73 flag=0;
74 SeriPushSend(0x88);
75 }
76 else
77 {
78 SeriPushSend(S);
79 }
80 }
81 /*--------------------------------------------
82 毫秒延時函數
83 ---------------------------------------------*/
84 void delayms(unsigned int ms)
85 {
86 unsigned char i=100,j;
87 for(;ms;ms--)
88 {
89 while(--i)
90 {
91 j=10;
92 while(--j);
93 }
94 }
95 }
96 /*--------------------------------------------
97 超聲波測距中斷函數[計時用]
98 ---------------------------------------------*/
99 void zd0() interrupt 1 //T0中斷用來計數器溢出,超過測距範圍
100 {
101 flag=1; //中斷溢出標誌
102 }
103 /*--------------------------------------------
104 超聲波測距啓動函數
105 ---------------------------------------------*/
106 void StartModule() //T1中斷用來掃描數碼管和計800MS啓動模塊
107 {
108 TX=1; //800MS 啓動一次模塊
109 _nop_();
110 _nop_();
111 _nop_();
112 _nop_();
113 _nop_();
114 _nop_();
115 _nop_();
116 _nop_();
117 _nop_();
118 _nop_();
119 _nop_();
120 _nop_();
121 _nop_();
122 _nop_();
123 _nop_();
124 _nop_();
125 _nop_();
126 _nop_();
127 _nop_();
128 _nop_();
129 _nop_();
130 TX=0;
131 }
132 /*--------------------------------------------
133 main函數
134 ---------------------------------------------*/
135 void main(void)
136 {
137 USRT_init();
138 while(1)
139 {
140 StartModule();
141 while(!RX); //當RX為零時等待
142 TR0=1; //開啓計數
143 while(RX); //當RX為1計數並等待
144 TR0=0; //關閉計數
145 Conut(); //計算
146 delayms(10); //10MS
147 }
148 }
TaoTao.c
>_<:下面將重點介紹如何從距離信息轉換為按鈕觸發消息的!
6.1、檢測手勢:
下圖是當有手進入測距區時超聲波測距儀採集到的數據,其中橫軸為時間,縱軸為距離單位釐米。從圖中可以看出當沒有障礙物時距離維持在42CM處(這是我在示波器軟件中故意設置的一個閾值,硬件代碼裏也設了閾值即:超出30cm就發送距離為0x88cm)。當手揮進對應區域時出現一個下降沿,當手離開時出現一個上升沿,當手在區域中停留的時間越長其對應跨度越大。(圖中共有4個凹槽,表示手4次揮進揮出區域,其中第3次停留時間較長)
6.2、干擾信號:
如下圖(不要管上面的圖標,當時用的時候沒修改圖表的單位和名稱,嘻嘻~)當沒有手進入區域時有時候硬件會出現干擾而產生一個很尖的下降和上升沿,其實這時並沒有手揮進區域,這個干擾會對結果造成影響,甚至出現錯誤的控制!!!
6.3、去除干擾:
如下圖最下面的窗口是距離-時間圖,其中第1、2、4為手揮進測距區,第3個是一次干擾。我是這樣轉換的:將距離-時間圖轉換為左上角的時長-時間圖,每個波的峯值就是對應距離時間圖中跳變時間,這樣我們就能將每次手進入或者是干擾持續的時間的值獲得!(由於干擾幾乎都是瞬間跳變,所以濾掉那個最小的第3個時長-時間波峯對應的距離-時間圖中的跳變就行啦)
6.4、時長分段產生將控制信號signal:
這裏將遮蔽時長進行分段產生3種不同的控制信號:[參見5.1.2串口數據接收函數的第21~27行](這裏num就是時長,可見:當時長為2~5時產生signal為1的信號,參看Timer部分可以發現這個信號控制點擊下一曲;當時長在6~9的時候觸發上一曲;當時長在10以上觸發暫停)因為我經常要下一曲所以設成手一揮就執行,暫停一般操作較少就讓它時長長一點(就像筆記本電腦的關機按鈕!),加入上一曲是為了防止失誤時能回到上面一個。!!!注意到這裏沒有把時長為1的包含在內,這就是上面分析的結果,即所謂的濾波!消除干擾~
1 if (num > 1 && num < 6){
2 signal = 1;
3 }else if (num > 5 && num < 10){
4 signal = 2;
5 }else if (num > 9){
6 signal = 3;
7 }
7、總結:
哈哈,終於寫完啦!>_<:快天亮啦~其實我本來想用腦電波來控制的,但是現在手頭有點吃緊,買不起腦電波呀~只能又一次玩廉價消費品啦~不過想一下連揮一揮手都不用的操作,是不是酷炫極啦!