使用 Azure 和 Arduino 控制模型赛车跑道






4.93/5 (15投票s)
这是一个使用 Arduino 和 Windows Azure 服务控制老式玩具赛车跑道上模型车的项目。
引言
在我看来,物联网(IoT)并非你用智能手机控制玩具直升机那么简单。这与使用无线控制器没有本质区别。在我看来,它更多地是关于机器在没有人为干预的情况下互相通知和控制。想到这一点,我回忆起父母阁楼里躺了几十年的旧模型车跑道。除了易碎之外,一个主要缺点是每条轨道上不能有不止一辆车,否则它们会在十字路口和交叉口互相碰撞,也仅仅因为速度差异而互相追尾。此外,当小车以恒定速度运行时,它们要么在直线部分跑得太慢,要么在弯道跑得太快,导致脱轨。
背景
通过 Arduino 计算机,将一个旧的 Faller Ams 模型车跑道通过互联网连接到 Windows Azure 服务。Azure 服务包含一个 AI 速度控制器,用于调整汽车的运行,以防止它们冲出弯道和互相碰撞。它还会调整汽车的速度,让它们在直道上跑得更快,在弯道上跑得更慢,并对特定汽车设置最高速度,使卡车比乘用车跑得慢。我将这个目标分为三个阶段。
阶段 I
在简单的椭圆形跑道上运行一辆车。控制速度并收集汽车运行或静止时的度量数据。
阶段 II
在分成六个独立部分的椭圆形跑道上运行两辆车。根据收集到的度量数据控制汽车的速度,以防止它们互相追尾。
阶段 III
建造一个更复杂的跑道,包含几十个部分、十字路口和交叉口,并在上面运行六辆车。不仅要控制汽车的速度,使其不互相追尾,还要根据收集到的度量数据,防止它们在十字路口和交叉口互相碰撞。
下面是我目前完成的第一阶段的一些细节。我目前正在进行第二阶段。在这篇文章中,我不会深入探讨代码细节,因为 Arduino 都是简单的 C++,Windows Azure 服务是 C#,Sql Azure 数据库是 SQL。
模型车跑道
对于不熟悉 Faller Ams 模型车跑道的人,请访问这个网站:http://www.everyoneweb.com/falleramsautos
Arduino 入门套件
我选择了 Arduino 微型计算机,因为它非常基础,提供了许多不同的连接可能性,而且很容易通过额外的组件进行扩展,比如我用来连接互联网的以太网扩展板。由于我不精通电子学,我购买了一个入门套件,其中包含许多基本组件和简单的示例。
Windows Azure 和 DataSpider
对于消息系统脚手架,我使用自己开发的 DataSpider 框架。该服务在 Windows Azure 中运行。为了记录和收集度量数据,有几个 SqlServer Azure 数据库。
进展和问题
2014 年 12 月,从父母阁楼里取出赛车跑道后,它需要清洗和对连接处的电路进行一些维修。花了很长时间才让赛车或多或少地重新跑起来。我不得不清洗和给电动机上油,并焊接一些损坏或断开的部件。
为了熟悉 Arduino,我构建了一些示例项目。这很有趣,能让你快速了解它是如何工作的。
将 Arduino 连接到网络。为此,你需要一个安装在 Arduino 顶部的以太网扩展板。
2015 年 1 月,将赛车跑道连接到 Arduino:Arduino 运行在 5 伏电压下,而赛车跑道需要大约 10 伏电压才能平稳运行。为了解决这个问题,我使用了可以通过 5 伏电压调节并能承受电源 10 伏电压通过的 MOSFET。为了从跑道收集电流数据,我使用了霍尔效应传感器,原因相同,它能承受 10 伏/200 毫安的电流,并向 Arduino 提供 2.5 伏的信号。正如我已经说过的,我不精通这些东西,所以花了很多时间在网上搜索才把这些东西组合起来。
第一个简单设置
连接好 Fleischmann 直流电源,并将数字 PWM 输出连接到 MOSFET 晶体管后,就可以通过在 PC 上运行的小命令行程序控制速度了。霍尔效应传感器 (Pololu ACS715) 允许读取电流,在下面的图表中,您可以看到电流在基线运行,直到有汽车在赛道上运行时,电流会上升。遇到的问题主要是由于电气连接问题、赛道不平整、脏污等原因导致汽车熄火。因此,霍尔效应传感器输出不均匀,导致计算不可靠。
(图中的数字不代表“真实”的电压或电流值。)
在图中,三辆不同的汽车在运行中进行了测量,中间有短暂的怠速间隙。从图中可以很容易地看出,收集到的数值范围相当广泛,因此无法可靠地将汽车彼此区分开来。然而,怠速状态下的间隙足够大,可以确定是否有任何物体在运行。绿色汽车的几次低读数是由于熄火和脱轨造成的。
基本设置
Faller Ams 赛道
Faller Ams 赛道通过电气元件连接到 Arduino。基本电源来自一个旧的 Fleischmann 模型火车轨道,提供 0-17 伏电压。
霍尔效应传感器
Arduino 嵌入式 C/C++ 代码
Arduino 的资源非常有限,使用 C/C++ 编程。我的 C/C++ 水平也并非特别好,所以我需要将 Arduino 上的处理量降到最低,并尽可能简单快速地将数据传输到 Windows Azure 服务。
#include <SPI.h> #include <Ethernet.h> // Enter a MAC address and IP address for your controller below. // The IP address will be dependent on your local network, and it's optional if DHCP is enabled byte mac[] = { 0x90, 0xA2, 0xDA, 0x0D, 0xBC, 0xAE }; //IPAddress ip(192,168,15,177); //We're not going to use this, it's just for reference static const byte deviceId[] = { 0x00, 0x00, 0x00, 0x01 }; EthernetClient client; // named constants for the switch and motor pins const int motorPin = 9; // the number of the motor pin const int sensorPin = A0; // the number of the motor pin float val = 0; float iloop = 0; byte bytes = 196; //The union is for converting the float to a byte array typedef union { float floatingPoint; byte binary[4]; } binaryFloat; binaryFloat v; //Serial.print statements are for debugging on the usb connection with the monitor on the notebook void setup() { // initialize serial communications at 9600 bps: Serial.begin(9600); Ethernet.begin(mac); //Ethernet.begin(mac, ip); } void loop(){ if (! client.connected()){ Serial.println("Trying to connect"); //char* host = "cloudspider.cloudapp.net"; char* host = "192.168.178.28"; client.setTimeout(1000); client.connect(host, 10100); if (client.connected()) { Serial.println("Connected to port, writing deviceId and waiting for commands..."); client.write(deviceId, sizeof(deviceId)); } } // write the bytes returned from the remote service on the motorpin // this will regulate the speed of the car analogWrite(motorPin, bytes); // read the analog in value from the sensor to send to the remote service // so it will know if the car is actually running if(iloop < 100){ val = val + analogRead(sensorPin); iloop++; delay(10); } else{ Serial.print("bytes = " ); Serial.print(bytes); Serial.print("\t amps = "); Serial.print(val/iloop-510); Serial.print("\t rest = "); Serial.print((val/iloop)/(bytes/100)); //we have very limited memory so keep track of what we're using Serial.print("\t ram = "); Serial.println(freeRam()); if(client.connected()){ // average a number of readings to flatten out the value a bit v.floatingPoint = val/iloop-510; Serial.print(v.binary[0]); Serial.print("\t"); Serial.print(v.binary[1]); Serial.print("\t"); Serial.print(v.binary[2]); Serial.print("\t"); Serial.println(v.binary[3]); //send the value to the remote service client.write(v.binary, sizeof(v.binary)); byte buff[1]; //read the reply back in if(client.read(buff, sizeof(buff)) > 0){ bytes = buff[0]; } } val = 0; iloop = 0; } } int freeRam () { extern int __heap_start, *__brkval; int v; return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); }
Windows Azure 服务 C# 代码
用于接收 Arduino 消息的 TCP 连接
public override void Listen() { _listener = new TcpListener(new IPEndPoint(IPAddress.Any,_cconfig.Port)); _listener.Start(); listenForClients(_listener); } private async void listenForClients(TcpListener tcpServer) { while (_running) { TcpClient tcpClient = await tcpServer.AcceptTcpClientAsync(); base.On_Signal(new DSSignalEventArgs(this.ToString(),"Message Received",(int)eLogLevel.Informational)); processClient(tcpClient); } } private async void processClient(TcpClient tcpClient) { NetworkStream stream = tcpClient.GetStream(); byte[] buffer = new byte[_cconfig.BufferSize]; int read = await stream.ReadAsync(buffer, 0, _cconfig.BufferSize); _tmpmessage = null; _waiting = true; DSMessage message = DSMessageHooks.InitializeDSMessage(); message.DSBody.Message.Body.Data = buffer; message.DSBody.Message.Header.ProcessorID = _cconfig.ProcessorId; base.On_Receipt(new ReceiptEventArgs(message)); while(_waiting && _running){} //wait for the return value will not take long message = _tmpmessage; await stream.WriteAsync((byte[])message.DSBody.Message.Body.Data,0,((byte[])message.DSBody.Message.Body.Data).Length); base.On_Signal(new DSSignalEventArgs(this.ToString(),"Message Send",(int)eLogLevel.Informational)); }
为了进一步分析,我希望将数据存储在 SqlAzure 数据库中,以便运行查询和报告。当然,Sql Azure 代码只是普通的 SqlServer 连接代码。
public override object SendReceive(object OutgoingMessage) { int reply = 0; DSMessage message = (DSMessage)OutgoingMessage; using(SqlConnection conn = new SqlConnection(_config.ConnectString)){ conn.Open(); SqlCommand command = conn.CreateCommand(); command.CommandType = System.Data.CommandType.Text; command.CommandText = (string)message.DSBody.Message.Body.Data; SqlDataReader reader = command.ExecuteReader(); if(reader.Read()){ reply = reader.GetInt32(0); } conn.Close(); } message.DSBody.Message.Body.Data = reply; return message; }
Windows Azure 服务目前除了将数据存储在 SqlServer 数据库中之外,并没有做太多事情。此逻辑将在下一项目阶段进行扩展。
阶段 II
现在赛道被分成 6 个独立的部分,包括两条直道和四个 90° 弯道。
多路复用器
为了驱动这 6 个区段,我使用了多路复用器。这是一种集成电路元件,旨在将一根引线分成多根引线(解复用器),或者反过来将多根引线合并成一根(多路复用器)。Arduino 有 6 个 PWM 端口,所以我可以将每个区段连接到其中一个。除了我在实验中烧毁了一个 PWM 端口,导致少了一个之外,这在第三阶段连接数十个区段时也会成为一个问题。多路复用器 4051 有 8 个端口,而且非常便宜,所以我将使用它。5 个 PWM 端口仍然正常工作,这将驱动 40 个区段,目前应该足够了。
多路复用器位于面包板的左侧,右侧是它控制的六个 MOSFET。这些下方是下一节讨论的光耦。如果您仔细观察附带的视频剪辑,您会看到使用此设置时汽车在弯道中已经减速。
霍尔效应传感器(再次)
这被证明是一个问题。在最初的设置中,我使用了第一阶段的传感器,并将多路复用器的六个端口的输出读取到每个部分的数组中。
不幸的是,这并没有给出合理的读数。目前我假定传感器无法跟上端口的高速切换。这意味着我将不得不使用 6 个单独的传感器,而且这些传感器并不便宜。对于这一阶段,我可以再订购 5 个,但对于第三阶段来说,这将变得非常昂贵,所以我必须找到另一种解决方案。一个可能的替代方法是在电流开始流动时测量电阻上的电压降。我为此困扰了一段时间,因为这两种解决方案都无法正常工作。其他通道上出现了很多干扰,所以我在 Arduino 论坛上提出了这个问题,并得到了很大的帮助。结果我使用的 ACS715 光耦不适合这种设置,所以我订购了半打 ACPL-P480 IC,它们产生了可靠的 0 或 50+ 信号,我可以轻松地将其读入 AI 模块。而且这些也便宜得多,这在未来扩展轨道时会很好。非常感谢 Arduino 论坛发帖人,现在原理图是这样的。
Sql Azure 数据库中收集到的度量数据
您可以很容易地从相应的区段电流读数中看到汽车从区段 1 运行到区段 2 和区段 3。传感器终于正常工作了,剩下的就是完成 Azure 服务逻辑了。
AI 模块
下面 AI 模块的代码仍然不完整,但它展示了我打算如何处理它。TrackSection 类作为所有区段的基类。目前我只有直线和弯道,但在第 3 阶段,还将有交叉口和十字路口,以及一些特殊区段,例如在加油站或公交车站等待(我有一辆小巴 :-)。这将提供扩展的灵活性。该类是可序列化的(引用的对象,汽车和度量数据也是)。TrackFactory 将反序列化从 Azure blob 中检索到的 xml 定义文件。
public override MemoryStream DataStreamGet(object DataLocation) { MemoryStream datastream = new MemoryStream(); CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_connstring); CloudBlobClient blobStorage = storageAccount.CreateCloudBlobClient(); string blobPath = (string)DataLocation; CloudBlob blob = blobStorage.GetBlobReference(blobPath); blob.DownloadToStream(datastream); datastream.Position = 0; return datastream; }
只有方法是抽象的,所以您必须在每个派生类中定义逻辑来更新状态并计算该部分上的实际速度(MaxPower)。
每个部分都有对前一个部分和下一个部分的引用。它将获取前一个部分中汽车对象的引用,以获取其最大速度(MaxPower)。没有其他方法可以跟踪哪辆车在哪里行驶,因为汽车本身没有连接,也无法进行通信。对下一个部分的引用将告知是否有汽车在前方向前行驶,以及以什么速度行驶。这将有助于确定新的部分状态和速度计算(尚未在下面的代码中实现)。
using System; using System.Collections.ObjectModel; using System.IO; using System.Xml.Serialization; namespace ModelCarTrack{ [SerializableAttribute()] public static class TrackFactory{ private static Collection<TrackSection> _tracksections; public static Collection<TrackSection> TrackSections { get { return _tracksections; } } static TrackFactory(){} public static TrackSection UpdateMetrics(TrackMetrics NewMetrics){ TrackSection _section = _tracksections[NewMetrics.SectionId]; _section.Metrics = NewMetrics; _section.UpdateSectionState(); _section.CalculatePower(); return _section; } public static void buildTrack(MemoryStream TrackConfiguration){ _tracksections = (Collection<TrackSection>)new XmlSerializer(typeof(Collection<TrackSection>)).Deserialize(TrackConfiguration); } } [SerializableAttribute()] public abstract class TrackSection{ private int _id; private ModelCar _modelcar; private TrackMetrics _metrics; private TrackSectionState _state; private TrackSectionType _type; private int _previoussectionid; private int _nextsectionid; private byte _maxpower; public int TrackSectionId { get { return _id; } set { _id = value; } } public byte MaxPower { get { return _maxpower; } set { _maxpower = value; } } public int NextSectionId { get { return _nextsectionid; } set { _nextsectionid = value; } } public int PreviousSectionId { get { return _previoussectionid; } set { _previoussectionid = value; } } public TrackSectionState SectionState { get { return _state; } set { _state = value; } } public TrackSectionType TrackSectionType { get { return _type; } set { _type = value; } } public TrackMetrics Metrics { get { return _metrics; } set { _metrics = value; } } public ModelCar Car { get { return _modelcar; } set { _modelcar = value; } } public abstract void UpdateSectionState(); public abstract void CalculatePower(); } [SerializableAttribute()] public class StraightSection:TrackSection { public override void UpdateSectionState() { if(base.Metrics.Current < 1.10) base.SectionState = TrackSectionState.Idle; else{ base.SectionState = TrackSectionState.Running; base.Car = TrackFactory.TrackSections[base.PreviousSectionId].Car; TrackFactory.TrackSections[base.PreviousSectionId].Car = null; TrackFactory.TrackSections[base.PreviousSectionId].SectionState = TrackSectionState.Idle; } } public override void CalculatePower() { switch(base.SectionState){ case TrackSectionState.Idle: case TrackSectionState.Waiting: base.Metrics.Power = 0; break; case TrackSectionState.WarmingUp: case TrackSectionState.Running: base.Metrics.Power = base.Car.MaxPower < base.MaxPower ? base.Car.MaxPower : base.MaxPower; break; default: break; } } } [SerializableAttribute()] public class CurvedSection:TrackSection { public override void UpdateSectionState() { if(base.Metrics.Current < 1.10) base.SectionState = TrackSectionState.Idle; else{ base.SectionState = TrackSectionState.Running; base.Car = TrackFactory.TrackSections[base.PreviousSectionId].Car; TrackFactory.TrackSections[base.PreviousSectionId].Car = null; TrackFactory.TrackSections[base.PreviousSectionId].SectionState = TrackSectionState.Idle; } } public override void CalculatePower() { switch(base.SectionState){ case TrackSectionState.Idle: case TrackSectionState.Waiting: base.Metrics.Power = 0; break; case TrackSectionState.WarmingUp: case TrackSectionState.Running: base.Metrics.Power = base.Car.MaxPower < base.MaxPower ? base.Car.MaxPower : base.MaxPower; break; default: break; } } } [SerializableAttribute()] public class ModelCar{ private int _carid; private byte _maxpower; public byte MaxPower { get { return _maxpower; } set { _maxpower = value; } } public int ModelCarId { get { return _carid; } set { _carid = value; } } } [SerializableAttribute()] public class TrackMetrics{ private byte _power; private float _current; private int _sectionid; public int SectionId { get { return _sectionid; } set { _sectionid = value; } } public float Current { get { return _current; } set { _current = value; } } public byte Power { get { return _power; } set { _power = value; } } } [SerializableAttribute()] public enum TrackSectionType { Straight, RightHandCurve, LeftHandCurve, Junction, Crossroad, Crossing } [SerializableAttribute()] public enum TrackSectionState { Idle, Waiting, WarmingUp, Running } }
Xml 定义
<?xml version="1.0" encoding="utf-8"?> <ArrayOfTrackSection xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <TrackSection> <TrackSectionId>0</TrackSectionId> <MaxPower>255</MaxPower> <NextSectionId>1</NextSectionId> <PreviousSectionId>5</PreviousSectionId> <TrackSectionType>Straight</TrackSectionType> <Car> <MaxPower>196</MaxPower> <ModelCarId>1</ModelCarId> </Car> </TrackSection> <TrackSection> <TrackSectionId>1</TrackSectionId> <MaxPower>128</MaxPower> <NextSectionId>2</NextSectionId> <PreviousSectionId>0</PreviousSectionId> <TrackSectionType>RightHandCurve</TrackSectionType> </TrackSection> <TrackSection> <TrackSectionId>2</TrackSectionId> <MaxPower>128</MaxPower> <NextSectionId>3</NextSectionId> <PreviousSectionId>1</PreviousSectionId> <TrackSectionType>RightHandCurve</TrackSectionType> </TrackSection> <TrackSection> <TrackSectionId>3</TrackSectionId> <MaxPower>255</MaxPower> <NextSectionId>4</NextSectionId> <PreviousSectionId>2</PreviousSectionId> <TrackSectionType>Straight</TrackSectionType> </TrackSection> <TrackSection> <TrackSectionId>4</TrackSectionId> <MaxPower>128</MaxPower> <NextSectionId>5</NextSectionId> <PreviousSectionId>3</PreviousSectionId> <TrackSectionType>RightHandCurve</TrackSectionType> </TrackSection> <TrackSection> <TrackSectionId>5</TrackSectionId> <MaxPower>128</MaxPower> <NextSectionId>0</NextSectionId> <PreviousSectionId>4</PreviousSectionId> <TrackSectionType>RightHandCurve</TrackSectionType> </TrackSection> </ArrayOfTrackSection>
整合所有内容
电子设备已就位,Azure 服务中的基本逻辑也已设置好,现在是时候将所有这些整合起来,让两辆车在椭圆形短跑道上运行了。在随附的视频中,您可以看到白色和蓝色的福特 Taunus 正在运行。
白车在相同电压下比蓝车快,所以它会逐渐追上蓝车。最终,当它非常接近时,逻辑会将其置于“等待”状态以保持距离。当蓝车前进到下一段时,白车会再次启动,您可以在下面的调试跟踪中看到。
这是在我本地开发环境中运行时 Wireshark 的跟踪
这是第二阶段的目标,并已完成。其代码逻辑如下:
public override Collection<ReplyMessage> UpdateSectionState(TrackMetrics NewMetrics) { Collection<ReplyMessage> replies = new Collection<ReplyMessage>(); ReplyMessage reply; if(!(NewMetrics.Current > 0 && NewMetrics.Current < 10)){ //values between 0 and 10 are artifacts if(base.Metrics.Current == 0 && NewMetrics.Current > 0 && this.SectionState != TrackSectionState.Running){ //a car has entered the section if(((TrackSection)TrackFactory.TrackSections[base.NextSectionId]).Car == null || (this.Car != null && ((TrackSection)TrackFactory.TrackSections[base.NextSectionId]).Car.ModelCarId == this.Car.ModelCarId)){ //the next section is free //set the section to Running this.SectionState = TrackSectionState.Running; this.CalculatePower(); reply = new ReplyMessage(); reply.SectionId = (byte)base.TrackSectionId; reply.Power = base.Metrics.Power; replies.Add(reply); //set the previous section to Idle ((TrackSection)TrackFactory.TrackSections[base.PreviousSectionId]).SectionState = TrackSectionState.Idle; ((TrackSection)TrackFactory.TrackSections[base.PreviousSectionId]).Car = null; ((TrackSection)TrackFactory.TrackSections[base.PreviousSectionId]).CalculatePower(); reply = new ReplyMessage(); reply.SectionId = (byte)((TrackSection)TrackFactory.TrackSections[base.PreviousSectionId]).TrackSectionId; reply.Power = ((TrackSection)TrackFactory.TrackSections[base.PreviousSectionId]).Metrics.Power; replies.Add(reply); //set the next section to WarmingUp ((TrackSection)TrackFactory.TrackSections[base.NextSectionId]).SectionState = TrackSectionState.WarmingUp; ((TrackSection)TrackFactory.TrackSections[base.NextSectionId]).Car = base.Car; ((TrackSection)TrackFactory.TrackSections[base.NextSectionId]).CalculatePower(); reply = new ReplyMessage(); reply.SectionId = (byte)((TrackSection)TrackFactory.TrackSections[base.NextSectionId]).TrackSectionId; reply.Power = ((TrackSection)TrackFactory.TrackSections[base.NextSectionId]).Metrics.Power; replies.Add(reply); } else{ //the next section is occupied if(this.Car != null){ //set the section to Waiting this.SectionState = TrackSectionState.Waiting; this.CalculatePower(); reply = new ReplyMessage(); reply.SectionId = (byte)base.TrackSectionId; reply.Power = base.Metrics.Power; replies.Add(reply); //set the previous section to Idle ((TrackSection)TrackFactory.TrackSections[base.PreviousSectionId]).SectionState = TrackSectionState.Idle; ((TrackSection)TrackFactory.TrackSections[base.PreviousSectionId]).Car = null; ((TrackSection)TrackFactory.TrackSections[base.PreviousSectionId]).CalculatePower(); reply = new ReplyMessage(); reply.SectionId = (byte)((TrackSection)TrackFactory.TrackSections[base.PreviousSectionId]).TrackSectionId; reply.Power = ((TrackSection)TrackFactory.TrackSections[base.PreviousSectionId]).Metrics.Power; replies.Add(reply); } } } this.Metrics.Current = NewMetrics.Current; } return replies; //return the Collection with commands to send to the Arduino }
我对这段代码不太满意,因为它包含太多的 if 条件。这样一来,当复杂性增加时,它就无法扩展。但目前它能用,我会在准备第三阶段时考虑重构它。
到目前为止,这对我来说是一个非常有成果的项目。我学到了很多以前不了解的技术,例如电子学方面。我发现真正的挑战是如何连接和控制一个过去从未为此设计过的设备。而且这很有趣。
第三阶段(准备工作)
我将不得不着手解决第三阶段的几个准备问题。
- 利用我现有的赛道部件设计一个更具吸引力的赛道。
- 通过多路复用器或更大的 Arduino 扩展可用引脚:挑战在于在有限的预算下完成这项工作,并且不增加电子复杂性。
- 重构当前的 C# 云逻辑以应对日益增加的复杂性
- 创建更好的跟踪信息,以便随时监控赛道的状态。
这些赛道正在等待连接
这是那些仍然能用的汽车
这个项目还会持续很长时间。我已经克服了几个障碍,尽管结果仍然不确定,但我相信我总会成功的。