65.9K
CodeProject 正在变化。 阅读更多。
Home

Sketch 框架和类库 - 第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (3投票s)

2016年7月27日

CPOL

11分钟阅读

viewsIcon

11784

downloadIcon

163

为不同的 Arduino 板提供标准接口

引言

第1部分 中,我们开发了一个标准的Arduino Sketch框架。现在我们将构建一个.NET类库,它将封装与开发板的所有通信。下载内容包含一个完整的Visual Studio (2015)解决方案,包括一个演示Windows Forms应用程序。演示应用将在第3部分讨论。

背景

在第1部分中,我们开发了一个Sketch框架,该框架响应定义的标准命令,并具有定义的响应格式。它能够返回有关Sketch响应的命令、Sketch提供的值和状态数据以及正在使用的输入和输出引脚的信息。

现在我们将切换到控制计算机端。我们特意使用Windows环境,因此我们使用Visual Studio 2015作为开发环境。

我不会为代码是VB.NET的事实而道歉。在我们开始使用Arduino时,VB仍然是一种比C#稍微更成熟的语言,而且对于那些主要兴趣是编写研究应用程序并发表结果而不是编写精细代码的兼职程序员来说,它也更容易阅读。

当然,最近C#取得了相当大的进展,而且微软也表明这是他们目前的主要关注点,所以实际上我们已经在工作中切换到使用C#了,但这个库仍然是VB.NET。如果你是从来没有用过VB的C#程序员,那么将VB转换为C#可能比一个没有用过C#的VB程序员进行反向转换要容易得多。

好的,那么我们要解决什么问题呢?

如果你需要在Windows PC上通过串行通信与Arduino对话,你会遇到的第一个问题是必须安装相应的串行驱动程序。安装Arduino IDE时,这会自动完成,或者如果你不能、或者不想在机器上安装IDE,也可以手动安装驱动程序。(我们有一些将发往中学的套件,这些PC的桌面被严格锁定——通常OneClick应用程序安装可以正常工作,但在这些情况下安装驱动程序可能是一场噩梦。)

假设你已经安装了驱动程序,接下来的问题是找到Windows分配给你的开发板的虚拟串行端口。  与其手动查找和更改Windows分配的端口号,不如我们第一个要实现的函数是扫描可用端口,并查找名称中包含“Arduino”的端口。通常所有开发板都报告一个形式为“Arduino [boardtype]”的名称。

由于你可能连接了多个开发板,所以我们希望获得所有找到的开发板的列表。

使用代码

在Visual Studio中启动一个类型为ClassLibrary的新项目。将其命名为“ArduinoSerialClassLib”。

在你的项目中添加一个类型为Class的新项。将其命名为“DetectArduinoClass”。

我们将使用System.Management命名空间来扫描Windows中的ManagementObjects,以查找名称匹配我们正在寻找的串行端口。

 Imports System.Management

你可能需要在“我的项目”下的“引用”部分添加对System.Management的引用。

这个类将有一个类型为Dictionary(Of String, String)的公共属性,它将包含一个Com端口列表作为项,以及它们的名称作为定义,如果名称与我们正在寻找的匹配。

除了构造函数New()之外,我们还将有一个函数Rescan(),如果第一次没有找到任何东西,我们可以调用它,而无需销毁并重新创建对象。我们还可以让它查找不同的名称模式,并将任何匹配项添加到现有字典中。

Public Class DetectArduinoClass

	Private _portList As New Dictionary(Of  String, String)

	Public ReadOnly Property PortList As Dictionary(Of String, String)
		Get
			Return _portlist
		End Get
	End Property

	Public Sub New(Optional target As String = "Arduino")
		ScanPorts(target)
	End Sub

	Public Sub Rescan(Optional target As String = "Arduino", Optional clear As Boolean = False)
		If clear Then _portList.Clear
		ScanPorts(target)
	End Sub

	Private Sub ScanPorts(ByVal target As String)
    'target will be matched against the start of the ManagementObject name
		Try
			Dim searcher As New ManagementObjectSearcher("root\cimv2", "SELECT * FROM Win32_SerialPort")
			Dim name, pname As String

			For Each queryObj As ManagementObject In searcher.Get()
				name = queryObj("Name")
				If name.Substring(0, Len(target)) = target Then  
                    pname = queryObj("DeviceID")
					_portList.Add(pname,name)
				End If
			Next

        Catch err As ManagementException
            'Do something, possibly report err.Message and carry on ...
		End Try
	End Sub

End Class

就这样。我们现在有了一个属性PortList,其中包含一个Com端口名称和设备名称的集合。

正如你在构造函数New()和Rescan()中看到的,它们都接受一个可选的字符串参数来匹配名称的开头,所以如果你想查找特定类型的开发板,可以调用New(“Arduino Leonardo”)或任何其他名称来搜索其他串行端口设备。

Windows的一个奇怪之处在于,如果Arduino在计算机启动时已经连接,那么它就无法从设备读取名称,而只是将其报告为“USB Serial Device”。在这种情况下,你需要断开并重新连接开发板,以便正确读取名称,或者你可以搜索“USB Serial”。你可以修改上面的ScanPorts()函数,以便在找不到“Arduino”时自动查找“USB Serial Device”。

一旦编译完成,并将DLL作为引用添加到你的主项目中,你就可以简单地包含类似的代码,例如在Form.Load()中。

    Dim det As New DetectArduinoClass()
	If det.PortList.Count > 0 Then
		'Yay we’ve found at least one board, lets do some stuff
	Else
		Det.Rescan("USB Serial")
		'Have we found one yet ...
	End If

别忘了添加对新类库的引用并包含

    Imports ArduinoSerialClassLib

在代码的顶部。

好的,现在我们已经确定了哪个端口连接了我们的开发板,让我们向库中添加一个新类来封装与选定开发板的连接和通信。

向项目添加一个名为“ArduinoSerialClass”的新类。

我们将处理IO端口,所以我们需要导入System.IO.Ports命名空间,并且还要注意何时需要将它们声明为Shared,以便它们可以在不同线程中使用。

Imports System.IO.Ports

Public Class ArdunioSerialClass

	Private Shared WithEvents _comPort As New SerialPort
	'... 

当我们创建ArduinoSerialClass类型的对象时,我们将告诉它使用哪个COM端口,因为我们已经使用DetectArduinoClass对象获取了设备和端口列表。我们也可能想告诉它使用什么波特率,以防我们期望一些非标准的东西,但我们将默认使用115,200波特。

Shared Private _baud As integer

Public Sub New(ByVal pname As String, Optional ByVal baud As Integer = 115200)
	ClearProperties()
	_baud = baud
	If SetupPort(pname) Then
		_portFound = True
		_comPortName = pname
		GetFullStatus()
	Else
		_portFound = False
		_comPortName = ""
	End If
End Sub

Private Function SetupPort(pname As String) As Boolean
	With _comPort
		.BaudRate = _baud
		.Parity = Parity.None
		.DataBits = 8
		.StopBits = StopBits.One
        '.Handshake = Handshake.RequestToSend. NB we need None on WIndows 8+
        .Handshake = Handshake.None
		.DtrEnable = True
		.RtsEnable = True
		.PortName = pname
		.WriteTimeout = 2000 ' Max time to wait for CTS =2sec
		.ReadTimeout = 500
		.NewLine = chr(10)
        .ReadBufferSize = 16384
	End With
	_comPort.Close()
	If Not _comPort.IsOpen Then
		Try
			_comPort.Open()
			Return True
		Catch ex As Exception
			'Do something about the error .. ex.Message
			Return False
		End Try
	End If
	Return False
End Function

好的,我们有私有变量来标记我们是否已连接到串行端口以及端口名称。这些都将作为ReadOnly Public属性可用。

	Private Shared _portFound As Boolean = False
	Public ReadOnly Property PortFound As Boolean
		Get
			Return _portFound
		End Get
	End Property

	Private _comPortName As String = "n/a"
	Public ReadOnly Property ComPortName As String
		Get
			Return _comPortName
		End Get
	End Property

我们将有许多其他公共属性要公开,其中许多将是只读状态报告,但LoopTime、SendSerialValues和SendIntValues将是读写的。

ClearProperties()将清除任何现有值,以防我们重新扫描,而GetFullStatus()将发出适当的命令来填充所有命令、状态、值和引脚的定义。

    Private Sub ClearProperties()
		'clear all public & private properties
		_comPortName = ""
		_loopTime = -1
		_fstatus.Sketch="n/a"
		_fstatus.Author="n/a"
		_fstatus.CompDate="n/a"
		_fstatus.Ver="n/a"
		_fstatus.UnitID="n/a"
		_cmds.Clear()
		_vals.Clear()
		_valDesc.Clear()
		_stats.Clear()
		_statDesc.Clear()
		_ins.Clear()
		_outs.Clear()
	End Sub

	Private Sub GetFullStatus()
		_comPort.Write(qSketch) 
		_comPort.Write(qCmdDefn)
		_comPort.Write(qStatusDefn)
		_comPort.Write(qValDefn)
		_comPort.Write(qIns)
		_comPort.Write(qOuts)
		_comPort.WriteLine(cmdLoopTime)
	End Sub

传入的串行数据将完全由一个响应_comPort.DataReceived事件的私有函数处理。

Private Shared Sub Receiver(ByVal sender As Object, ByVal e As SerialDataReceivedEventArgs) Handles _comPort.DataReceived
	Try
		Do
			Dim gotsome As String = _comPort.ReadLine
'There is a potential problem here if we miss an end of line, this will hang until we get one, or timeout occurs – you might like to fix this by reading byte by byte until you’ve got a complete line or the buffer is empty.
            If Not IsNothing(gotsome) Then
				ParseRxString(gotsome)
			End If
        Loop Until (_comPort.BytesToRead = 0)  
' Don't return if more bytes have become available in the meantime
        Catch ex As Exception 'Typically a TimeoutException
            'Handle this error nicely ... ex.Message 
        End Try
End Sub

因此,我们将有一个函数ParseRxString()来处理接收数据的完整行。本质上,它将识别响应字符串并将更新相关属性的数据结构。

标准命令和响应字符串是预定义的,但Sketch特定的命令和响应将通过GetFullStatus()读取,并将适当的数据结构填充为类对象的属性来公开它们。

在查看ParseRxString()之前,我们需要做两件事。首先,为属性定义数据结构,其次,定义所有标准命令和响应,以匹配我们在第1部分中在Sketch中创建的常量。

属性背后的私有变量需要是Shared的,以便它们可以在跨线程访问——更新响应的是一个DataRecieved事件,该事件可能在与主程序中的类对象实例不同的线程上。

例如,处理获取和设置Arduino上的主程序loopTime:

Public Const cmdLoopTime As Char = Chr(20)
Shared Private _loopTime As integer
	Public Property LoopTime As Integer
		Get 
			Return _loopTime
		End Get
	    Set(value As Integer)
		    _comPort.WriteLine(cmdLoopTime & value)
	    End Set
	End Property

当我们设置新值时,我们不直接更新本地私有变量。我们只是向开发板发出命令,它会响应新值,串行数据接收处理程序将执行本地更新。

同样,对于属性SendSerialValuesSendIntValues,它们都是布尔类型。

大多数情况下,我们将使用字典来存储标识符和值或描述的集合。对于Sketch信息,我们将创建一个简单的结构:

	Public Structure FirmwareInfo
		Dim Sketch As String
		Dim Ver As String
		Dim Author As String
		Dim CompDate As String
		Dim UnitID As String
	End Structure

	Shared Private _fstatus As New FirmwareInfo
	Public ReadOnly Property FStatus As FirmwareInfo
		Get
			Return _fstatus
		End Get
	End Property

如果我们想更聪明一点,可以将版本字符串解析为Version数据类型,以便应用程序可以轻松地检查所需的最低或最高版本。同样,我们也可以将编译日期字符串解析为DateTime类型——但如前所述,它的格式很糟糕,所以我就留给你来处理了。

这是保存Sketch返回的实际实时值以及它们的描述符的结构定义,这两个都可以被控制程序作为它创建的ArduinoSerialClass对象的属性访问。

	Shared Private _vals As New Dictionary(Of String, String) ' this will contain the actual values 
	Public ReadOnly Property Vals As Dictionary(Of String, String) 
		Get
			Return _vals
		End Get
	End Property
	Shared Private _valDesc As New Dictionary(Of String, String) ' this will contain the descriptions for the values
	Public ReadOnly Property ValDesc As Dictionary(Of String, String) 
		Get
			Return _valDesc
		End Get
	End Property

类似的结构将用于Sketch状态和描述、Sketch响应的命令(除了标准命令之外)以及输入和输出引脚。

现在我们可以看看ParseRxString()函数的基础。我们将接受一个由一行或多行串行输入组成的字符串。我们将根据任何有效的换行符或回车符将其分解成单独的行。

对于每一行,我们将尝试将第一个字符解析为标识符,第二个字符解析为“=”表示状态字符串跟随,或者“:”表示动态值跟随,字符串的其余部分作为有用部分。

如果它是状态行,那么我们可以查找标准标识符并更新适当的状态属性,或者如果标识符作为键存在于Sketch状态定义字典_stats中,那么我们就更新那里的值。

对于返回命令、值、引脚或状态定义的标准状态行,形式为“C=L=Enable LED”,然后我们将数据部分“L=Enable LED”进一步分解为键“L”和定义“Enable LED”,并将其添加到有效Sketch命令字典_cmds中。

Imports System.Text.RegularExpressions

Private Shared Sub ParseRxString(rxstr As String)
Dim lines As String() = rxstr.Split(New String() {"\n\r", "\r", "\n"}, StringSplitOptions.RemoveEmptyEntries)   
Dim item As String
Dim ky, op, cont As String
For i = 0 To lines.Count - 1
'remove any remaining control chars from start or end of the line
	item = Regex.Replace(lines(i).Trim, "\p{C}+", "") 
	If Len(item) > 2 Then
		'do we have a valid identifier character (ky) and is it a value or a status (op)?
		ky = item.Substring(0, 1)  ' this be the identifier
		op = item.Substring(1, 1)   ' this should be either = or :
		data = item.Substring(2)
		Select Case op
			Case "="	
'this is a status so do status stuff
				Select Case ky
					Case lblSendSerialValues
'here we are simply updating the property directly
						_sendSerialEnabled = (data="1")
					Case lblCmdDesc	
'in this case we have to identify the command
'and then either update or add to the dictionary
						Dim tmp As String() = data.Split("=")
						If _cmds.ContainsKey(tmp(0))
							_cmds(tmp(0)) = tmp(1)
						Else 
							_cmds.Add(tmp(0),tmp(1))
						End If
'and so on for all the other definition returns...

					Case lblSketch 
						_fstatus.Sketch = data
'and similarly for Version, Date, Author and UnitID...

					Case lblLoopTime
						Try
							_loopTime = CInt(data)
						Catch ex As Exception
							'ignore it or report the error	
						End Try
'and likewise for the standard boolean properties SendSerialValues and SendIntValues ...

					Case Else
	'this should be a sketch specific status so we can update the value in _stats
						If _stats.ContainsKey(ky) Then
							_stats(ky) = data
						Else 
	'we have an unrecognised status ky so maybe we report it as an error or something...
						End If
				End Select
				Case ":"	'this is a value
				If _vals.ContainsKey(ky) Then  'update the value in the dictionary
					_vals(ky) = data
				Else 
	'we have an unrecognised value so do something with it ...
				End If 
			Case Else
	'this line had an op char that was not = or : - was it a fragment, is it an error?
			End Select
		Else 
	'this was some fragment of length less than 2 - is it an error?
		End If
Next
End Sub

除了所有这些之外,我们还需要定义所有标识符和命令的常量。下载完整的类库以查看详细信息。

一个Close()函数可以很好地处理ComPort的销毁,而不是等待垃圾回收,这将很有用,我们还有一个DoCmd()函数,它将简单地将一个有效命令写入串行端口。这将是控制程序执行Sketch特定命令的主要方式。

	Public Sub Close()
		_comPort.Close()
		_comPort.Dispose()
		_portFound = False
		_comPortName = ""
	End Sub

	Public Sub DoCmd(ByVal cmd As String)
	' check that we are being send a valid command string
	    If cmds.ContainsKey(Left(cmd(1)) Then
		    _comPort.WriteLine(cmd)
		End If
	End Sub

也可以提供一个SendRawString()函数来向开发板发送任意数据——但这会打破封装整个东西和让开发板上的Sketch定义它将识别哪些命令的意义。

在下载内容中有一个Visual Studio解决方案,包含两个项目——类库和演示控制程序,它将使用类库来查找连接的Arduino,连接到它并下载其功能,并显示其状态和值。

简要看一下演示应用程序,只有一个Form。当它加载时,它会创建一个DetectArduinoClass对象,如果找到任何开发板,则创建一个ArduinoSerialClass对象来与找到的第一个开发板通信。

然后它会动态创建针对特定开发板的控件,并显示接收到的值。我们将在第3部分中更详细地讨论这一点。

关于轮询和中断事件的说明。

在Arduino开发板上,主程序循环正在轮询输入引脚并报告值。间隔由Sketch中的loopTime变量定义,该变量可以使用ArduinoSerialClass对象的LoopTime属性进行设置。

控制程序需要轮询ArduinoSerialClass对象以读取所需的值。如果我们不想错过任何值变化,那么我们必须以至少是Arduino中轮询速率两倍的速率进行轮询——有关背景信息,请参阅Nyquist频率

在这个简单的演示中,我们使用Forms.Timer对象进行轮询,所以每次Arduino loopTime改变时,我们都会将定时器间隔调整为略小于它的一半。

另一种未包含在此演示中的技术是让ArduinoSerialClass对象在任何我们关心的值发生变化时触发一个事件。这对于缓慢变化的值(例如心率)非常有效,并且意味着我们不需要轮询ArduinoSerialClass中的value属性。它也适用于产生Arduino中断的输入,在这种情况下,我们的loopTime可能很长,但我们希望在检测到触发器时立即响应。

演示应用

示例代码中包含了一个简单的演示应用程序,以说明ArduinoSerialClass的用法。描述、代码示例、屏幕截图和改进建议将构成该系列的第三部分。

关注点

我们创建了一个包含两个对象的类库。第一个扫描Windows管理中列出的可用com端口,并识别与我们正在查找的名称匹配的端口。

找到连接了Arduino的端口后,我们使用另一个对象来处理与开发板的所有通信。它会自动读取开发板的功能,并将属性提供给应用程序。

演示应用程序展示了如何使用这些对象。完整的类库代码还包括应用程序定义特定值以在它们发生变化时引发事件的能力,而不是必须持续轮询它们。

未来发展

你如何使用这个取决于你正在接口的具体设备。显而易见的改进是Sketch和类库中的错误处理——所有这些代码都为了这些文章的目的而被剥离了。

历史

首次发布于2016年7月28日

8月2日更新了代码文件,现在全部经过测试并能正常工作

© . All rights reserved.