[LattePanda] 使用C# 來做藍牙4.0 iBeacon的門鎖系統

在學這篇教學前必須要先會一些事情,首先必須要會在Lattepanda上使用visual studio

(LattePanda 拿鐵熊貓教學:Arduino與Visual Studio環境設定)。

再來必須已經了解如何在visual studio裡使用C#控制Arduino的I/O

(LattePanda 拿鐵熊貓教學#1:LED 閃爍,使用Visual Studio)。

在這篇教學中會使用visual studio的C#來開發簡單的門鎖系統,一般在藍牙使用上都是使用藍牙傳輸資料,但在這篇是使用藍牙4.0中的iBeacon模式,在iBeacon模式中可以得知接收器與發射器的距離,而這個距離就可以當作我們決定要不要開門的條件,所以這樣連按按鈕的動作都可以省去。

作者/攝影   黃品叡
時間   3 小時 以上
難度   * * * * * * *
材料表
  • Lattepanda
  • USB to UART模組*1
  • HM-10模組(2個以上)*1
  • 電磁鎖*1

[請注意] 這邊教學是必須有一些基礎,首先前情提要中的Lattapanda上使用Visual Studio是基本,再來是必須要會開啟Window Forms C#、會使用基本的工具箱,並且可以編譯程式。最後是對於電路有基本的認識,能夠認識基本電路符號,並且能將簡單電路圖在麵包版上實現。

 

在這篇教學中分成硬體、軟體兩個部分,(1)硬體部分是電路的連接, (2)軟體部分是Visual Studio C#的程式撰寫。

 

(1)=====硬體部分:=====

在硬體連接的電路圖如下圖。

使用的有

  • 兩個<LED>燈(顯示門鎖是開啟還是關閉)
  • 一個12V的<LOCK>電磁鎖
  • 一個<USB to UART模組>
  • 兩個<HM10>的BLE模組,一個安裝至LattePanda,一個由使用者持有

下圖Lattepanda上左方USB孔上接上USB to UART模組(紅色),可接上HM10 BLE模組。

對應電路圖,接在麵包版上電路如下圖

整體看起來會是像下圖

換個角度再放一張圖,如下

在電磁鎖的部分基本上可以任意替換,在這裡我將它鎖在木板上,能夠表示他有鎖住或打開而已,其安裝後的圖如下(上方是一塊木板連結鐵塊,下面則是一個「ㄇ」字形的木板連結電磁鐵的線圈部分)。

(2)=====軟體部分:=====

我使用的程式開發環境是Windows 10、Visual studio 2017、C#,那我們就直接進入程式部分。

程式我分成(1)介面、(2)初始化、(3)Serial連接、(4)開啟backgroundWorker、(5)讀取iBeacon資訊、(6)iBeacon資訊解碼、(7)連結Arduino,這幾個部分。

1.介面

Form1中的介面配置如下圖

在最上面有一個textBox 命名為 textbox_door
中間是一個listView 命名為 listView_door
按鈕左邊是 button_connect
按鈕中間是button_findBeacon
按鈕右邊是button_loopFind
最下面的listBox 命名為 listBox_msg
在背景工具中有
serialPort 命名為 serialPort_beacon
backgroundWorker 命名為 backgroundWorker_findBeacon
timer 命名為 timer1

 

各個介面的命名可以參考下圖:

 

命名如果不一樣,請記得在程式中自行修改。

 

2.初始化

初始化的部分先從 include開始:

using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.IO.Ports; //serial
using LattePanda.Firmata; //Arduino

以上是所有要用到的namespace。

 

在全域變數裡:

Arduino arduino = new Arduino("COM3", 57600);
bool[] openlist = new bool[3];

(全域變數記得是寫在 class Form1 裡面,但是在一般函式外面)

 

在Form1()建構式裡:

public Form1()
{
InitializeComponent();
ColumnHeader header1, header2;
header1 = new ColumnHeader();
header2 = new ColumnHeader();

header1.Text = "--Time--";
header1.TextAlign = HorizontalAlignment.Center;
header1.Width = 90;
header2.Text = "--ID--";
header2.TextAlign = HorizontalAlignment.Center;
header2.Width = 70;

listView_door.Columns.Add(header1);
listView_door.Columns.Add(header2);

//which ID can open the lock
openlist[0] = false;
openlist[1] = true;
openlist[2] = true;

//arduino setting
arduino.pinMode(5, Arduino.OUTPUT);//pin of red LED
arduino.pinMode(6, Arduino.OUTPUT);//pin of green LED
arduino.pinMode(10, Arduino.OUTPUT);//pin of the lock

//set the door close first
doorOpen(false);
textBox_door.BackColor = Color.Red;
}

(這裡面設定了一些介面的東西,一些初始值還有門鎖狀態)

 

3.Serial連接

Serial 連接在C#中非常簡單,我是用一個按鈕,按下後連結Serial程式如下:

private void button_connect_Click(object sender, EventArgs e)
{
serialPort_beacon = new SerialPort("COM6", 9600, Parity.None, 8, StopBits.One);

if (!serialPort_beacon.IsOpen)
{
try
{
serialPort_beacon.Open();
listBox_msg.Items.Add("Connect");
}
catch
{
MessageBox.Show("Serial open error!");
}
}
else
listBox_msg.Items.Add("Opened");
}

 

(這個COM是USB to URAT晶片的COM角)

 

4.開啟backgroundWorker

首先是按鈕觸發尋找Beacon:

private void button_findBeacon_Click(object sender, EventArgs e)
{
//send DISC
string msgSend = "AT+DISI?";
byte[] buffer = System.Text.Encoding.Default.GetBytes(msgSend);
if (serialPort_beacon.IsOpen)
if (backgroundWorker_findBeacon.IsBusy != true)
{serialPort_beacon.Write(buffer, 0, buffer.Length);
backgroundWorker_findBeacon.RunWorkerAsync();
}
else
MessageBox.Show("already finding");
else
MessageBox.Show("Serial is close");
}

 

(在這裡是開啟一個backgroundWorker,而這裡只是單純地尋找一次Beacon而已)

若是要持續尋找,可以開啟一個timer來持續觸發,程式如下:

private void button_loopFind_Click(object sender, EventArgs e)
{
timer1.Start();
}
private void timer1_Tick(object sender, EventArgs e)
{
//send DISC
string msgSend = "AT+DISI?";
byte[] buffer = System.Text.Encoding.Default.GetBytes(msgSend);
if (serialPort_beacon.IsOpen)
if (backgroundWorker_findBeacon.IsBusy != true)
{
serialPort_beacon.DiscardInBuffer();
serialPort_beacon.Write(buffer, 0, buffer.Length);
backgroundWorker_findBeacon.RunWorkerAsync();
}
else
Console.Write("already finding");
else
MessageBox.Show("Serial is close");
}

(這裡開一個Timer讓我們可以每一段時間就開啟backgroundWorker)

 

5.讀取iBeacon資訊

這裡就是backgroundWorker裡所做的事情

private void backgroundWorker_findBeacon_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;

char[] buffer = new char[256];
string msg_read = "";
bool stringCheck = false;
//read data
byte loopCount = 0;

while (true)
{
if (serialPort_beacon.BytesToRead > 0)
{
//serial read to msg_read
serialPort_beacon.Read(buffer, 0, buffer.Length);
for (int i = 0; i < buffer.Length && buffer[i] != '\0'; i++)
msg_read += buffer[i];

//if msg_read end with "OK+DISCE" then stop reading
if (msg_read.Length > 8 && msg_read.IndexOf("OK+DISCE") != -1)
{
stringCheck = true;
break;

}
}
else
{
//timeout
System.Threading.Thread.Sleep(500);
loopCount++;
string dot = "";
for (int i = 1; i <= loopCount; i++)
dot = dot + ". ";

if (loopCount > 1)
this.Invoke((MethodInvoker)(() => listBox_msg.Items.RemoveAt(listBox_msg.Items.Count - 1)));
if (loopCount > 15)
break;
this.Invoke((MethodInvoker)(() => listBox_msg.Items.Add(dot)));
}
}

//if didn't read anything than prient "time out"
if (msg_read == "")
this.Invoke((MethodInvoker)(() => listBox_msg.Items.Add("Time out")));
else
{
if (stringCheck == false)
//if have read something but not iBeacon info
this.Invoke((MethodInvoker)(() => listBox_msg.Items.Add(msg_read)));
else
{
Tuple<int[], int[], int[], int> result = deCodeDISI(msg_read, 2);
this.Invoke((MethodInvoker)(() => listBox_msg.Items.Add("minor : " + result.Item2[0].ToString())));
this.Invoke((MethodInvoker)(() => listBox_msg.Items.Add("RSS : " + result.Item3[0].ToString())));
//if is close enough and it's in open list then open the lock
if (result.Item3[0] > -40 && openlist[result.Item2[0]] == true)
{
this.Invoke((MethodInvoker)(() => textBox_door.BackColor = Color.Green));
ListViewItem item1 = new ListViewItem(DateTime.Now.ToShortTimeString());
item1.SubItems.Add(result.Item2[0].ToString());
this.Invoke((MethodInvoker)(() => listView_door.Items.Add(item1)));
doorOpen(true);
}
else
{
this.Invoke((MethodInvoker)(() => textBox_door.BackColor = Color.Red));
doorOpen(false);
}
}
}
}

 

(這裡的程式包含好多東西,上半部分是在處理Serial read的問題,並且在讀取中有動態的「…」做顯示,若沒有收到資料則會有「time out」的資訊出現。下半部分是將得到的資訊做判斷,判斷要不要開門)

 

6.iBeacon資訊解碼

這裡是「deCodeDISI」副函式的程式部分:

private Tuple<int[], int[], int[], int> deCodeDISI(string serialData, int maxDiviceCount)
{

//OK+DISIS  OK+DISC : Factory ID : iBeacon UUID : Major+Minor+Measured : MAC : RSSI  OK+DISCE
//OK+DISISOK+DISC:4C000215:74278BDAB64445208F0C720EAF059935:11110001C5:88C25532ED1E:-032OK+DISCE

string DataRemain = serialData;
int[] FactoryID = new int[maxDiviceCount];
string[] UUID = new string[maxDiviceCount];
int[] Major = new int[maxDiviceCount];
int[] Minor = new int[maxDiviceCount];
string[] MAC = new string[maxDiviceCount];
int[] RSSvalue = new int[maxDiviceCount];
DataRemain = DataRemain.Substring(0, serialData.IndexOf("OK+DISCE"));
int count = 0;
while (true)
{
int findNum = DataRemain.IndexOf(":");
if (findNum == -1)
{
Console.Write("deCode done!");
break;
}
else
{
//Factory ID (length 8)
string FactoryID_str = DataRemain.Substring(findNum + 1, 8);
DataRemain = DataRemain.Substring(findNum + 9);
FactoryID[count] = Convert.ToInt32(FactoryID_str, 16);

//iBeacon UUID
findNum = DataRemain.IndexOf(":");
string UUID_str = DataRemain.Substring(findNum + 1, 32);
DataRemain = DataRemain.Substring(findNum + 33);
UUID[count] = UUID_str;

//Major
findNum = DataRemain.IndexOf(":");
string Major_str = DataRemain.Substring(findNum + 1, 4);
DataRemain = DataRemain.Substring(findNum + 5);
Major[count] = Convert.ToInt32(Major_str);
//Minor
string Minor_str = DataRemain.Substring(0, 4);
DataRemain = DataRemain.Substring(findNum + 4);
Minor[count] = Convert.ToInt32(Minor_str);

//MAC
findNum = DataRemain.IndexOf(":");
string MAC_str = DataRemain.Substring(findNum + 1, 12);
DataRemain = DataRemain.Substring(findNum + 13);
MAC[count] = MAC_str;

//RSS
findNum = DataRemain.IndexOf(":");
string RSS_str = DataRemain.Substring(findNum + 1, 4);
DataRemain = DataRemain.Substring(findNum + 5);
RSSvalue[count] = Convert.ToInt32(RSS_str);

count++;
}
}

return Tuple.Create(Major, Minor, RSSvalue, count);
}

(這就是解碼iBeacon的資訊的部分)

 

7.連結Arduino

跟Arduino的部分非常簡單,只有控制I/O的部分,D10腳位控制繼電器、D5控制紅色LED、D6控制綠色LED燈,副程式如下:

private void doorOpen(bool open)
{
if (open == false)//door close
{
//lock the door and red LED on
arduino.digitalWrite(10, Arduino.HIGH);
arduino.digitalWrite(5, Arduino.HIGH);
arduino.digitalWrite(6, Arduino.LOW);
}
else//door open
{
//unlock the door and green LED on
arduino.digitalWrite(10, Arduino.LOW);
arduino.digitalWrite(5, Arduino.LOW);
arduino.digitalWrite(6, Arduino.HIGH);
}
}

 

軟體部分就是以上程式了,其中包含了非常多的細節,像是處理Serial的部分,或是執行續(就是backgroundWorker)裡的處理,為什麼用Invoke,Tuple的用法,解碼的處理…等,這些初學的話建議複製副函式,直接用就好,裡面是什麼等對於程式更清楚後再慢慢回來看。

而當然這些副函式並不能稱作完美,例如並沒有對於不完整的iBeacon資訊做處理的地方,只有稍微確認,和在資訊結尾做非常簡單的處理,在這裡主要就是帶大家入門,在深入的部分,就請大家自己研究瞜~

 

 

相關文章:

 

Leave a comment

Your email address will not be published.


*